Skip to content

Commit

Permalink
feat: ⚡️ server side rendering at the dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
lucca180 committed Jan 25, 2025
1 parent 96a7270 commit 30ca869
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 97 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"axios": "^1.7.9",
"chance": "^1.1.12",
"color": "^4.2.3",
"cookies-next": "^5.0.2",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0",
"ejs": "^3.1.9",
"firebase": "^11.1.0",
Expand Down
140 changes: 82 additions & 58 deletions pages/api/v1/restock/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Prisma } from '@prisma/client';
import { NextApiRequest, NextApiResponse } from 'next';
import { ItemData, RestockChart, RestockSession, RestockStats } from '../../../../types';
import { ItemData, RestockChart, RestockSession, RestockStats, User } from '../../../../types';
import { CheckAuth } from '../../../../utils/googleCloud';
import prisma from '../../../../utils/prisma';
import { differenceInMilliseconds } from 'date-fns';
Expand Down Expand Up @@ -32,56 +32,18 @@ const GET = async (req: NextApiRequest, res: NextApiResponse) => {
const { user } = await CheckAuth(req);

if (!user || user.banned) return res.status(401).json({ error: 'Unauthorized' });
let newStartDate = startDate;

if (startDate && !endDate) {
const diff = differenceInMilliseconds(Date.now(), new Date(Number(startDate)));
newStartDate = (Number(startDate) - diff).toString();
}

const sessions = await prisma.restockSession.findMany({
where: {
user_id: user.id,
startedAt: {
gte: newStartDate ? new Date(Number(newStartDate)) : undefined,
lte: endDate ? new Date(Number(endDate)) : undefined,
},
shop_id: shopId ? Number(shopId) : undefined,
},
orderBy: {
startedAt: 'desc',
},
take: limit ? Number(limit) : undefined,
select: {
sessionText: true,
startedAt: true,
endedAt: true,
shop_id: true,
},
const result = await getRestockStats({
startDate,
endDate,
shopId,
limit,
user,
});

const currentStats = sessions.filter((x) => x.startedAt >= new Date(Number(startDate)));

if (!currentStats.length) return res.status(200).json(null);

const pastStats = sessions.filter((x) => x.startedAt < new Date(Number(startDate)));
if (!result) return res.status(200).json(null);

const [currentResult, pastResult] = await Promise.all([
calculateStats(
currentStats,
currentStats.at(0)?.startedAt.getTime() ?? 0,
currentStats.at(-1)?.endedAt?.getTime() ?? 0
),
pastStats.length
? calculateStats(
pastStats,
pastStats.at(0)?.startedAt?.getTime() ?? 0,
pastStats.at(-1)?.endedAt?.getTime() ?? 0
)
: null,
]);

return res.status(200).json({ currentStats: currentResult?.[0], pastStats: pastResult?.[0] });
return res.status(200).json(result);
} catch (e) {
console.error(e);
return res.status(500).json({ error: 'Internal Server Error' });
Expand Down Expand Up @@ -129,6 +91,67 @@ const POST = async (req: NextApiRequest, res: NextApiResponse) => {

// --------- //

type GetRestockStatsParams = {
startDate?: string | number;
endDate?: string | number;
shopId?: string | number;
limit?: string;
user: User;
};
export const getRestockStats = async (params: GetRestockStatsParams) => {
const { startDate, endDate, shopId, limit, user } = params;
let newStartDate = startDate;

if (startDate && !endDate) {
const diff = differenceInMilliseconds(Date.now(), new Date(Number(startDate)));
newStartDate = (Number(startDate) - diff).toString();
}

const sessions = await prisma.restockSession.findMany({
where: {
user_id: user.id,
startedAt: {
gte: newStartDate ? new Date(Number(newStartDate)) : undefined,
lte: endDate ? new Date(Number(endDate)) : undefined,
},
shop_id: shopId ? Number(shopId) : undefined,
},
orderBy: {
startedAt: 'desc',
},
take: limit ? Number(limit) : undefined,
select: {
sessionText: true,
startedAt: true,
endedAt: true,
shop_id: true,
},
});

const currentStats = sessions.filter((x) => x.startedAt >= new Date(Number(startDate)));

if (!currentStats.length) return null;

const pastStats = sessions.filter((x) => x.startedAt < new Date(Number(startDate)));

const [currentResult, pastResult] = await Promise.all([
calculateStats(
currentStats,
currentStats.at(0)?.startedAt.getTime() ?? 0,
currentStats.at(-1)?.endedAt?.getTime() ?? 0
),
pastStats.length
? calculateStats(
pastStats,
pastStats.at(0)?.startedAt?.getTime() ?? 0,
pastStats.at(-1)?.endedAt?.getTime() ?? 0
)
: null,
]);

return { currentStats: currentResult?.[0], pastStats: pastResult?.[0] };
};

type ValueOf<T> = T[keyof T];
export const calculateStats = async (
rawSessions: {
Expand All @@ -155,7 +178,7 @@ export const calculateStats = async (
const revenuePerDay: { [date: string]: number } = {};
const lostPerDay: { [date: string]: number } = {};
const refreshesPerDay: { [date: string]: number } = {};
let fastestBuy: RestockStats['fastestBuy'] = undefined;
let fastestBuy: RestockStats['fastestBuy'] = null;

let refreshTotalTime: number[] = [];
let reactionTotalTime: number[] = [];
Expand Down Expand Up @@ -350,13 +373,14 @@ export const calculateStats = async (
stats.fastestBuy = fastestBuy;
if (allBought.length) {
const favBuy = findMostFrequent(allBought.map((x) => x.item));
if (favBuy.item) {
stats.buyCount = favBuy.frequencyMap;

stats.buyCount = favBuy.frequencyMap;

stats.favoriteItem = {
item: favBuy.item,
count: favBuy.count,
};
stats.favoriteItem = {
item: favBuy.item,
count: favBuy.count,
};
}
}

stats.hottestRestocks = Object.values(allItemsData)
Expand Down Expand Up @@ -411,11 +435,11 @@ export const defaultStats: RestockStats = {
durationCount: 0,
},
totalSessions: 0,
mostExpensiveBought: undefined,
mostExpensiveLost: undefined,
fastestBuy: undefined,
mostExpensiveBought: null,
mostExpensiveLost: null,
fastestBuy: null,
favoriteItem: {
item: undefined,
item: null,
count: 0,
},
totalRefreshes: 0,
Expand Down
100 changes: 71 additions & 29 deletions pages/restock/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import NextLink from 'next/link';
import { StatsCard } from '../../../components/Hubs/Restock/StatsCard';
import ItemCard from '../../../components/Items/ItemCard';
import ImportRestockModal from '../../../components/Modal/ImportRestock';
import { RestockChart, RestockSession, RestockStats } from '../../../types';
import { RestockChart, RestockSession, RestockStats, User } from '../../../types';
import { useAuth } from '../../../utils/auth';
import axios from 'axios';
import { restockShopInfo } from '../../../utils/utils';
Expand All @@ -52,6 +52,10 @@ import dynamic from 'next/dynamic';
import { FaArrowTrendUp, FaArrowTrendDown } from 'react-icons/fa6';
import { DashboardOptionsModalProps } from '../../../components/Modal/DashboardOptionsModal';
import { RestockedCTACard } from '../../../components/Hubs/Wrapped2024/CTACard';
import { NextApiRequest } from 'next';
import { CheckAuth } from '../../../utils/googleCloud';
import { setCookie } from 'cookies-next/client';
import { getRestockStats } from '../../api/v1/restock';

const RestockWrappedModal = dynamic<RestockWrappedModalProps>(
() => import('../../../components/Modal/RestockWrappedModal')
Expand All @@ -71,26 +75,40 @@ type AlertMsg = {
type: 'loading' | 'info' | 'warning' | 'success' | 'error' | undefined;
};

type PeriodFilter = { timePeriod: number; shops: number | string; timestamp?: number };
type PeriodFilter = { timePeriod: number; shops: number | string; timestamp: number | null };
const intl = new Intl.NumberFormat();

const defaultFilter: PeriodFilter = { timePeriod: 7, shops: 'all', timestamp: undefined };
const defaultFilter: PeriodFilter = { timePeriod: 30, shops: 'all', timestamp: null };

const RestockDashboard = () => {
type RestockDashboardProps = {
messages: Record<string, string>;
locale: string;
initialFilter: PeriodFilter;
initialCurrentStats?: RestockStats | null;
initialPastStats?: RestockStats | null;
user?: User;
};

const RestockDashboard = (props: RestockDashboardProps) => {
const { user } = props;
const t = useTranslations();
const formatter = useFormatter();
const { user, userPref, authLoading } = useAuth();
const { userPref } = useAuth();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isOpenOptions, onOpen: onOpenOptions, onClose: onCloseOptions } = useDisclosure();
const { isOpen: isWrappedOpen, onOpen: onWrappedOpen, onClose: onWrappedClose } = useDisclosure();
const [openImport, setOpenImport] = useState<boolean>(false);
const [sessionStats, setSessionStats] = useState<RestockStats | null>(null);
const [pastSessionStats, setPastSessionStats] = useState<RestockStats | null>(null);
const [sessionStats, setSessionStats] = useState<RestockStats | null>(
props.initialCurrentStats ?? null
);
const [pastSessionStats, setPastSessionStats] = useState<RestockStats | null>(
props.initialPastStats ?? null
);
const [alertMsg, setAlertMsg] = useState<AlertMsg | null>(null);
const [importCount, setImportCount] = useState<number>(0);
const [shopList, setShopList] = useState<number[]>([]);
const [noScript, setNoScript] = useState<boolean>(false);
const [filter, setFilter] = useState<PeriodFilter | null>(null);
const [filter, setFilter] = useState<PeriodFilter | null>(props.initialFilter);
const [chartData, setChartData] = useState<RestockChart | null>(null);

const revenueDiff = useMemo(() => {
Expand All @@ -106,18 +124,9 @@ const RestockDashboard = () => {
}, [sessionStats, pastSessionStats]);

useEffect(() => {
if (!authLoading && user && !!filter) {
handleImport();
init();
}
}, [user, authLoading, !!filter]);
handleImport();

useEffect(() => {
const storageFilter = localStorage.getItem('restockFilter');
let timePeriod = storageFilter ? JSON.parse(storageFilter)?.timePeriod || 7 : 7;
if (timePeriod === 90) timePeriod = 30;
if (storageFilter) setFilter({ ...defaultFilter, timePeriod: timePeriod });
else setFilter(defaultFilter);
if (!sessionStats) init();
}, []);

const init = async (customFilter?: PeriodFilter) => {
Expand Down Expand Up @@ -220,14 +229,16 @@ const RestockDashboard = () => {
init();
};

const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const handleSelectChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
init({ ...(filter ?? defaultFilter), [name]: value, timestamp: undefined });
setFilter((prev) => ({ ...(prev ?? defaultFilter), [name]: value, timestamp: undefined }));
localStorage.setItem(
'restockFilter',
JSON.stringify({ ...filter, [name]: value, timestamp: undefined })
);

init({ ...(filter ?? defaultFilter), [name]: value, timestamp: null });

setFilter((prev) => ({ ...(prev ?? defaultFilter), [name]: value, timestamp: null }));

setCookie('restockFilter2025', JSON.stringify({ ...filter, [name]: value, timestamp: null }), {
expires: new Date('2030-01-01'),
});
};

// const setCustomTimestamp = (timestamp: number) => {
Expand Down Expand Up @@ -271,7 +282,6 @@ const RestockDashboard = () => {
bg="blackAlpha.300"
size="xs"
borderRadius={'sm'}
defaultValue={30}
name="timePeriod"
value={(filter ?? defaultFilter).timePeriod}
onChange={handleSelectChange}
Expand All @@ -285,6 +295,7 @@ const RestockDashboard = () => {
<option value={7}>{t('General.last-x-days', { x: 7 })}</option>
<option value={30}>{t('General.last-x-days', { x: 30 })}</option>
<option value={60}>{t('General.last-x-days', { x: 60 })}</option>
<option value={90}>{t('General.last-x-days', { x: 90 })}</option>
{/* <option>All Time</option> */}
</Select>
<Select
Expand Down Expand Up @@ -610,7 +621,6 @@ const RestockDashboard = () => {
))}
</Flex>
<Text fontSize={'sm'}>
<Badge colorScheme="green">{t('Layout.new')}</Badge>{' '}
{t.rich('Restock.history-dashboard-cta', {
Link: (chunk) => (
<Link
Expand Down Expand Up @@ -720,11 +730,43 @@ const RestockDashboard = () => {

export default RestockDashboard;

export async function getStaticProps(context: any) {
export async function getServerSideProps(context: any): Promise<{ props: RestockDashboardProps }> {
let res;
const filter: PeriodFilter = {
...defaultFilter,
...JSON.parse(context.req.cookies.restockFilter2025 || '{}'),
};

try {
res = await CheckAuth(context.req as NextApiRequest);
} catch (e) {}

if (!res || !res.user) {
return {
props: {
messages: (await import(`../../../translation/${context.locale}.json`)).default,
initialFilter: filter,
locale: context.locale,
},
};
}

const user = res.user;

const data = await getRestockStats({
user: user,
startDate: filter.timestamp ?? Date.now() - (filter.timePeriod ?? 7) * 24 * 60 * 60 * 1000,
endDate: filter.timestamp ? endOfDay(new UTCDate(filter.timestamp)).getTime() : undefined,
shopId: filter.shops === 'all' ? undefined : filter.shops,
});

return {
props: {
messages: (await import(`../../../translation/${context.locale}.json`)).default,
locale: context.locale,
initialFilter: filter,
initialCurrentStats: data?.currentStats ?? null,
initialPastStats: data?.pastStats ?? null,
},
};
}
Expand Down
Loading

0 comments on commit 30ca869

Please sign in to comment.