feat: add last sync and health

This commit is contained in:
Nathan Lamy 2025-08-19 19:32:03 +02:00
parent 74f31a723a
commit a89546c54e
7 changed files with 170 additions and 146 deletions

View file

@ -24,6 +24,9 @@ import Error from "~/components/error";
import { useSearchParams } from "react-router";
import { useColles, type User } from "~/lib/api";
import TabContent from "~/components/home/tab-content";
import { MainLayout } from "~/layout";
import { forceReload } from "~/lib/utils";
import { SyncButton } from "../sync-status";
export default function Home({ user }: { user: User }) {
// Handle query parameters
@ -62,8 +65,15 @@ export default function Home({ user }: { user: User }) {
};
// Fetch colles from API
const { studentColles, classColles, favoriteColles, error, isLoading } =
useColles(startDate);
const {
studentColles,
classColles,
favoriteColles,
healthyUntil,
lastSync,
error,
isLoading,
} = useColles(startDate);
// Error handling (after all hooks)
if (error)
@ -143,106 +153,116 @@ export default function Home({ user }: { user: User }) {
};
return (
<div className="space-y-6 pb-20 md:pb-0">
{/* Tabs */}
<Tabs
defaultValue="all"
value={activeTab}
onValueChange={setActiveTab}
className="max-w-md w-full"
>
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
<TabsTrigger value="you" className={tabsStyle}>
<UserIcon className="h-4 w-4" />
Vous
</TabsTrigger>
{/* <TabsTrigger value="favorites" className={tabsStyle}>
<MainLayout
header={
<>
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
<SyncButton healthyUntil={healthyUntil} lastSync={lastSync} />
</>
}
>
<div className="space-y-6 pb-20 md:pb-0">
{/* Tabs */}
<Tabs
defaultValue="all"
value={activeTab}
onValueChange={setActiveTab}
className="max-w-md w-full"
>
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
<TabsTrigger value="you" className={tabsStyle}>
<UserIcon className="h-4 w-4" />
Vous
</TabsTrigger>
{/* <TabsTrigger value="favorites" className={tabsStyle}>
<Star className="h-4 w-4" />
Favoris
</TabsTrigger> */}
<TabsTrigger value="class" className={tabsStyle}>
<Users className="h-4 w-4" />
Classe
</TabsTrigger>
</TabsList>
</Tabs>
<TabsTrigger value="class" className={tabsStyle}>
<Users className="h-4 w-4" />
Classe
</TabsTrigger>
</TabsList>
</Tabs>
{/* Week Navigation */}
<div className="mb-0">
<div className="flex flex-row items-center justify-between gap-2">
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
<ChevronLeft className="h-10 w-10" />
</Button>
<div className="flex-1">
<DatePickerWithRange
startDate={startDate}
setStartDate={setStartDate}
/>
{/* Week Navigation */}
<div className="mb-0">
<div className="flex flex-row items-center justify-between gap-2">
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
<ChevronLeft className="h-10 w-10" />
</Button>
<div className="flex-1">
<DatePickerWithRange
startDate={startDate}
setStartDate={setStartDate}
/>
</div>
<Button variant="outline" size="sm" onClick={handleNextWeek}>
<ChevronRight className="h-10 w-10" />
</Button>
</div>
<Button variant="outline" size="sm" onClick={handleNextWeek}>
<ChevronRight className="h-10 w-10" />
</div>
{/* Filter component */}
<div className="flex gap-2 pb-0 pt-2">
<Select value={subjectFilter} onValueChange={setSubject}>
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
<SelectValue placeholder="Matière" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
{subjects.map((subject) => (
<SelectItem key={subject} value={subject}>
{subject}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={examinerFilter} onValueChange={setExaminer}>
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
<SelectValue placeholder="Colleur" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous</SelectItem>
{examiners.map((examiner) => (
<SelectItem key={examiner} value={examiner}>
{examiner}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
className="rounded-full dark:bg-input/30 text-primary font-normal"
onClick={toggleSort}
>
{sorted == "asc" ? (
<SortAsc className="h-5 w-5" />
) : (
<SortDesc className="h-5 w-5" />
)}
Trier
</Button>
</div>
</div>
{/* Filter component */}
<div className="flex gap-2 pb-0 pt-2">
<Select value={subjectFilter} onValueChange={setSubject}>
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
<SelectValue placeholder="Matière" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
{subjects.map((subject) => (
<SelectItem key={subject} value={subject}>
{subject}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Your Colles Tab */}
{activeTab === "you" && (
<TabContent
tabTitle="Vos colles"
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
isLoading={isLoading}
isSorted={sorted === "desc"}
colles={applyFilters(studentColles)}
preferences={user.preferences}
/>
)}
<Select value={examinerFilter} onValueChange={setExaminer}>
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
<SelectValue placeholder="Colleur" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous</SelectItem>
{examiners.map((examiner) => (
<SelectItem key={examiner} value={examiner}>
{examiner}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
className="rounded-full dark:bg-input/30 text-primary font-normal"
onClick={toggleSort}
>
{sorted == "asc" ? (
<SortAsc className="h-5 w-5" />
) : (
<SortDesc className="h-5 w-5" />
)}
Trier
</Button>
</div>
{/* Your Colles Tab */}
{activeTab === "you" && (
<TabContent
tabTitle="Vos colles"
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
isLoading={isLoading}
isSorted={sorted === "desc"}
colles={applyFilters(studentColles)}
preferences={user.preferences}
/>
)}
{/* Favorites Tab
{/* Favorites Tab
{activeTab === "favorites" && (
<TabContent
tabTitle="Vos favoris"
@ -252,20 +272,21 @@ export default function Home({ user }: { user: User }) {
/>
)} */}
{/* Class Colles Tab */}
{activeTab === "class" && (
<TabContent
tabTitle="Les colles de la classe"
emptyCollesText="Aucune colle trouvée."
isLoading={isLoading}
isSorted={sorted === "desc"}
colles={applyFilters(classColles)}
preferences={user.preferences}
/>
)}
{/* Class Colles Tab */}
{activeTab === "class" && (
<TabContent
tabTitle="Les colles de la classe"
emptyCollesText="Aucune colle trouvée."
isLoading={isLoading}
isSorted={sorted === "desc"}
colles={applyFilters(classColles)}
preferences={user.preferences}
/>
)}
{/* Bottom Navigation for Mobile */}
<BottomNavigation activeId="colles" />
</div>
{/* Bottom Navigation for Mobile */}
<BottomNavigation activeId="colles" />
</div>
</MainLayout>
);
}

View file

@ -8,10 +8,18 @@ import {
import { Cloud, CloudOff, Wifi, WifiOff } from "lucide-react";
import { cn } from "~/lib/utils";
export function SyncButton() {
const [isSync, setIsSync] = useState(true);
export function SyncButton({
healthyUntil,
lastSync,
}: {
healthyUntil: Date;
lastSync: Date;
}) {
const healthyUntilDate = new Date(healthyUntil) || new Date(0);
const lastSyncDate = new Date(lastSync) || new Date(0);
// Determine if the sync is healthy based on the healthyUntil date
const isSync = healthyUntilDate > new Date();
const [isOpen, setIsOpen] = useState(false);
const lastSync = new Date(); // TODO: Replace with actual last sync date
const getIcon = () => {
if (isSync) return <Cloud className="w-6! h-6!" />;
@ -28,11 +36,12 @@ export function SyncButton() {
const now = new Date();
const diff = now.getTime() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (minutes < 1) return "A l'instant";
if (minutes < 1) return `il y a ${seconds}s`;
if (minutes < 60) return `il y a ${minutes}m`;
if (hours < 24) return `il y a ${hours}h`;
return `il y a ${days}j`;
@ -61,26 +70,26 @@ export function SyncButton() {
) : (
<WifiOff className="h-4 w-4" />
)}
<span className="font-medium">
Status
</span>
<span className="font-medium">Status</span>
</div>
<div
className={cn(
"ml-auto px-2 py-1 rounded-full text-xs font-medium",
isSync
? "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"
: "bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400",
)}
>
{isSync ? "Connecté" : "Erreur"}
{isSync ? "Connecté" : "Hors ligne"}
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Dernière mis à jour :</span>
<span>{formatLastSync(lastSync)}</span>
<span className="text-muted-foreground">
Dernière mis à jour :
</span>
<span>{formatLastSync(lastSyncDate)}</span>
</div>
</div>
{/* TODO:

View file

@ -51,18 +51,18 @@ export function AuthLayout({
export function MainLayout({
children,
page
header
}: {
children: React.ReactNode;
page: React.ReactNode;
header: React.ReactNode;
}) {
return (
<main className="container mx-auto py-8 px-4">
<div className="flex justify-between items-center mb-4">
{children}
{header}
</div>
{page}
{children}
</main>
);
}

View file

@ -189,6 +189,8 @@ interface CollePayload {
classColles: Colle[];
studentColles: Colle[];
favoriteColles: Colle[];
healthyUntil: Date;
lastSync: Date;
}
export const useColles = (startDate: DateTime) => {
@ -206,7 +208,9 @@ export const useColles = (startDate: DateTime) => {
gcTime: 0,
}
: {
staleTime: 0,
staleTime: Duration.fromObject({
hours: 1, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
@ -222,6 +226,8 @@ export const useColles = (startDate: DateTime) => {
classColles: [],
studentColles: [],
favoriteColles: [],
healthyUntil: new Date(0),
lastSync: new Date(0),
},
data || {}
) as CollePayload;

View file

@ -16,13 +16,11 @@ import {
ChevronUp,
ChevronDown,
MapPinHouse,
Users,
RefreshCw,
Share2,
ExternalLink,
} from "lucide-react";
import { Separator } from "~/components/ui/separator";
import UserDropdown from "~/components/user-dropdown";
import ColleDetailsSkeleton from "~/components/details/skeleton-details";
import AttachmentItem from "~/components/details/attachment";
import Error from "~/components/error";
@ -134,9 +132,6 @@ export default function ColleDetailPage() {
<ArrowLeft className="h-4 w-4 mr-2" />
Retour
</Button>
<div className="hidden md:block">
<UserDropdown user={user} />
</div>
<div className="flex md:hidden items-center gap-2">
<Button
variant="outline"
@ -150,7 +145,6 @@ export default function ColleDetailPage() {
/>
<span className="sr-only">Recharger</span>
</Button>
<UserDropdown user={user} />
</div>
</div>

View file

@ -2,14 +2,11 @@ import { Navigate } from "react-router";
import Error from "~/components/error";
import HomePage from "~/components/home";
import Loader from "~/components/loader";
import { SyncButton } from "~/components/sync-status";
import { MainLayout } from "~/layout";
import { AUTH_ERROR, useUser } from "~/lib/api";
import { forceReload } from "~/lib/utils";
export default function Home() {
const { user, isLoading, error } = useUser();
if (isLoading) {
return <Loader />;
}
@ -20,12 +17,5 @@ export default function Home() {
return <Error message={error.message} />;
}
return (
<MainLayout page={<HomePage user={user} />}>
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
<SyncButton />
</MainLayout>
);
return <HomePage user={user} />;
}

View file

@ -8,7 +8,7 @@ import { forceReload } from "~/lib/utils";
export default function Home() {
const { user, isLoading, error } = useUser();
if (isLoading) {
return <Loader />;
}
@ -20,10 +20,14 @@ export default function Home() {
}
return (
<MainLayout page={<SettingsPage user={user} />}>
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
<MainLayout
header={
<h1 className="text-2xl font-bold" onClick={forceReload}>
Khollis&eacute; - {user.className}
</h1>
}
>
<SettingsPage user={user} />
</MainLayout>
);
}