From 30ca869dc158eee931f7853937f7d4cd249a9b1b Mon Sep 17 00:00:00 2001 From: Lucca Date: Sat, 25 Jan 2025 19:34:57 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9A=A1=EF=B8=8F=20server=20side=20re?= =?UTF-8?q?ndering=20at=20the=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pages/api/v1/restock/index.ts | 140 +++++++++++++++++------------- pages/restock/dashboard/index.tsx | 100 ++++++++++++++------- types.d.ts | 8 +- yarn.lock | 11 +-- 5 files changed, 164 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index f4e3ab2..aa9c758 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/api/v1/restock/index.ts b/pages/api/v1/restock/index.ts index 05dff5f..6d80cd8 100644 --- a/pages/api/v1/restock/index.ts +++ b/pages/api/v1/restock/index.ts @@ -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'; @@ -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' }); @@ -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[keyof T]; export const calculateStats = async ( rawSessions: { @@ -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[] = []; @@ -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) @@ -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, diff --git a/pages/restock/dashboard/index.tsx b/pages/restock/dashboard/index.tsx index c2a6cfa..3a7640e 100644 --- a/pages/restock/dashboard/index.tsx +++ b/pages/restock/dashboard/index.tsx @@ -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'; @@ -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( () => import('../../../components/Modal/RestockWrappedModal') @@ -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; + 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(false); - const [sessionStats, setSessionStats] = useState(null); - const [pastSessionStats, setPastSessionStats] = useState(null); + const [sessionStats, setSessionStats] = useState( + props.initialCurrentStats ?? null + ); + const [pastSessionStats, setPastSessionStats] = useState( + props.initialPastStats ?? null + ); const [alertMsg, setAlertMsg] = useState(null); const [importCount, setImportCount] = useState(0); const [shopList, setShopList] = useState([]); const [noScript, setNoScript] = useState(false); - const [filter, setFilter] = useState(null); + const [filter, setFilter] = useState(props.initialFilter); const [chartData, setChartData] = useState(null); const revenueDiff = useMemo(() => { @@ -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) => { @@ -220,14 +229,16 @@ const RestockDashboard = () => { init(); }; - const handleSelectChange = (e: React.ChangeEvent) => { + const handleSelectChange = async (e: React.ChangeEvent) => { 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) => { @@ -271,7 +282,6 @@ const RestockDashboard = () => { bg="blackAlpha.300" size="xs" borderRadius={'sm'} - defaultValue={30} name="timePeriod" value={(filter ?? defaultFilter).timePeriod} onChange={handleSelectChange} @@ -285,6 +295,7 @@ const RestockDashboard = () => { + {/* */}