Files
AnthoLume/frontend/src/pages/HomePage.tsx
2026-03-22 17:21:34 -04:00

264 lines
9.2 KiB
TypeScript

import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useGetHome } from '../generated/anthoLumeAPIV1';
import type {
HomeResponse,
LeaderboardData,
LeaderboardEntry,
UserStreak,
} from '../generated/model';
import ReadingHistoryGraph from '../components/ReadingHistoryGraph';
import { formatNumber, formatDuration } from '../utils/formatters';
interface InfoCardProps {
title: string;
size: string | number;
link?: string;
}
function InfoCard({ title, size, link }: InfoCardProps) {
if (link) {
return (
<Link to={link} className="w-full">
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
<p className="text-sm text-gray-400">{title}</p>
</div>
</div>
</Link>
);
}
return (
<div className="w-full">
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
<p className="text-sm text-gray-400">{title}</p>
</div>
</div>
</div>
);
}
interface StreakCardProps {
window: 'DAY' | 'WEEK';
currentStreak: number;
currentStreakStartDate: string;
currentStreakEndDate: string;
maxStreak: number;
maxStreakStartDate: string;
maxStreakEndDate: string;
}
function StreakCard({
window,
currentStreak,
currentStreakStartDate,
currentStreakEndDate,
maxStreak,
maxStreakStartDate,
maxStreakEndDate,
}: StreakCardProps) {
return (
<div className="w-full">
<div className="relative w-full rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
{window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'}
</p>
<div className="my-6 flex items-end space-x-2">
<p className="text-5xl font-bold text-black dark:text-white">{currentStreak}</p>
</div>
<div className="dark:text-white">
<div className="mb-2 flex items-center justify-between border-b border-gray-200 pb-2 text-sm">
<div>
<p>{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}</p>
<div className="flex items-end text-sm text-gray-400">
{currentStreakStartDate} {currentStreakEndDate}
</div>
</div>
<div className="flex items-end font-bold">{currentStreak}</div>
</div>
<div className="mb-2 flex items-center justify-between pb-2 text-sm">
<div>
<p>{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}</p>
<div className="flex items-end text-sm text-gray-400">
{maxStreakStartDate} {maxStreakEndDate}
</div>
</div>
<div className="flex items-end font-bold">{maxStreak}</div>
</div>
</div>
</div>
</div>
);
}
interface LeaderboardCardProps {
name: string;
data: LeaderboardData;
}
type TimePeriod = 'all' | 'year' | 'month' | 'week';
function LeaderboardCard({ name, data }: LeaderboardCardProps) {
const [selectedPeriod, setSelectedPeriod] = useState<TimePeriod>('all');
const formatValue = (value: number): string => {
switch (name) {
case 'WPM':
return `${value.toFixed(2)} WPM`;
case 'Duration':
return formatDuration(value);
case 'Words':
return formatNumber(value);
default:
return value.toString();
}
};
const currentData = data[selectedPeriod];
const handlePeriodChange = (period: TimePeriod) => {
setSelectedPeriod(period);
};
return (
<div className="w-full">
<div className="flex size-full flex-col justify-between rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
<div>
<div className="flex justify-between">
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
{name} Leaderboard
</p>
<div className="flex items-center gap-2 text-xs text-gray-400">
<button
type="button"
onClick={() => handlePeriodChange('all')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'all' ? '!text-black dark:!text-white' : ''}`}
>
all
</button>
<button
type="button"
onClick={() => handlePeriodChange('year')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'year' ? '!text-black dark:!text-white' : ''}`}
>
year
</button>
<button
type="button"
onClick={() => handlePeriodChange('month')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'month' ? '!text-black dark:!text-white' : ''}`}
>
month
</button>
<button
type="button"
onClick={() => handlePeriodChange('week')}
className={`cursor-pointer hover:text-black dark:hover:text-white ${selectedPeriod === 'week' ? '!text-black dark:!text-white' : ''}`}
>
week
</button>
</div>
</div>
</div>
{/* Current period data */}
<div className="my-6 flex items-end space-x-2">
{currentData?.length === 0 ? (
<p className="text-5xl font-bold text-black dark:text-white">N/A</p>
) : (
<p className="text-5xl font-bold text-black dark:text-white">
{currentData[0]?.user_id || 'N/A'}
</p>
)}
</div>
<div className="dark:text-white">
{currentData?.slice(0, 3).map((item: LeaderboardEntry, index: number) => (
<div
key={index}
className={`flex items-center justify-between py-2 text-sm ${index > 0 ? 'border-t border-gray-200' : ''}`}
>
<div>
<p>{item.user_id}</p>
</div>
<div className="flex items-end font-bold">{formatValue(item.value)}</div>
</div>
))}
</div>
</div>
</div>
);
}
export default function HomePage() {
const { data: homeData, isLoading: homeLoading } = useGetHome();
const homeResponse = homeData?.status === 200 ? (homeData.data as HomeResponse) : null;
const dbInfo = homeResponse?.database_info;
const streaks = homeResponse?.streaks?.streaks;
const graphData = homeResponse?.graph_data?.graph_data;
const userStats = homeResponse?.user_statistics;
if (homeLoading) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
return (
<div className="flex flex-col gap-4">
{/* Daily Read Totals Graph */}
<div className="w-full">
<div className="relative w-full rounded bg-white shadow-lg dark:bg-gray-700">
<p className="absolute left-5 top-3 w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
Daily Read Totals
</p>
<ReadingHistoryGraph data={graphData || []} />
</div>
</div>
{/* Info Cards */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<InfoCard title="Documents" size={dbInfo?.documents_size || 0} link="./documents" />
<InfoCard title="Activity Records" size={dbInfo?.activity_size || 0} link="./activity" />
<InfoCard title="Progress Records" size={dbInfo?.progress_size || 0} link="./progress" />
<InfoCard title="Devices" size={dbInfo?.devices_size || 0} />
</div>
{/* Streak Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{streaks?.map((streak: UserStreak, index: number) => (
<StreakCard
key={index}
window={streak.window as 'DAY' | 'WEEK'}
currentStreak={streak.current_streak}
currentStreakStartDate={streak.current_streak_start_date}
currentStreakEndDate={streak.current_streak_end_date}
maxStreak={streak.max_streak}
maxStreakStartDate={streak.max_streak_start_date}
maxStreakEndDate={streak.max_streak_end_date}
/>
))}
</div>
{/* Leaderboard Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<LeaderboardCard
name="WPM"
data={userStats?.wpm || { all: [], year: [], month: [], week: [] }}
/>
<LeaderboardCard
name="Duration"
data={userStats?.duration || { all: [], year: [], month: [], week: [] }}
/>
<LeaderboardCard
name="Words"
data={userStats?.words || { all: [], year: [], month: [], week: [] }}
/>
</div>
</div>
);
}