From 81d73fd99d3b2585bb91109e03e6e4e94ed8da64 Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Tue, 26 Aug 2025 00:26:30 +0200 Subject: [PATCH] feat: add BJRepas credentials --- app/components/repas/credentials.tsx | 148 +++++++++++++++++++++++++++ app/components/repas/index.tsx | 4 +- app/components/repas/menu-card.tsx | 80 ++++++++++++--- app/components/repas/menus.tsx | 19 ++-- app/components/repas/wip.tsx | 23 ----- app/lib/api.ts | 52 ++++++++-- 6 files changed, 274 insertions(+), 52 deletions(-) create mode 100644 app/components/repas/credentials.tsx delete mode 100644 app/components/repas/wip.tsx diff --git a/app/components/repas/credentials.tsx b/app/components/repas/credentials.tsx new file mode 100644 index 0000000..9d927a5 --- /dev/null +++ b/app/components/repas/credentials.tsx @@ -0,0 +1,148 @@ +import { Github, Loader2, Save } from "lucide-react"; +import { Button } from "../ui/button"; +import { Card } from "../ui/card"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Link } from "react-router"; +import { getCredentialsStatus, testCredentials } from "~/lib/api"; +import { useState } from "react"; +import { toast } from "sonner"; + +export default function BJRepas() { + const credentials = getSavedCredentials(); + const [username, setUsername] = useState(credentials?.username || ""); + const [password, setPassword] = useState(credentials?.password || ""); + + let intervalId: NodeJS.Timeout | null = null; + const [isLoading, setLoading] = useState(false); + + const handleSaveCredentials = async () => { + try { + if (!username || !password) { + toast.error( + "Veuillez renseigner vos identifiants BJColle avant de continuer." + ); + return; + } + + await testCredentials(username, password); + setLoading(true); + + // Try every second to check if the credentials are valid + intervalId = setInterval(async () => { + try { + const res = await getCredentialsStatus(); + + // Credentials are valid + if (res.authenticated === "SUCCESS") { + saveCredentials(username, password); + toast.success("Vos identifiants ont été enregistrés avec succès."); + setLoading(false); + if (intervalId) clearInterval(intervalId); + } + // Credentials are invalid + if (res.authenticated === "ERROR") { + toast.error( + "Identifiants invalides. Veuillez vérifier votre nom d'utilisateur et mot de passe." + ); + setUsername(""); + setPassword(""); + setLoading(false); + if (intervalId) clearInterval(intervalId); + } + } catch (error) { + console.error("Error testing credentials:", error); + } + }, 1000); + } catch (error) { + console.error("Error testing credentials:", error); + toast.error( + "Une erreur est survenue lors de la validation des identifiants. Veuillez réessayer plus tard." + ); + } + }; + return ( +
+

Inscription aux repas

+ +

+ Pour vous inscrire aux repas (via BJ Repas), veuillez renseigner vos + identifiants BJColle ci-dessous. +

+

+ Vos identifiants sont enregistrés localement sur votre appareil et ne + sont jamais stockés sur nos serveurs. +

+ + +
+

Vos identifiants BJColle

+
+ + setUsername(e.target.value)} + placeholder="Entrez votre nom d'utilisateur" + className="mt-1" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Entrez votre mot de passe" + className="mt-1" + /> +
+ +
+
+
+

+ Pour en savoir plus sur la sécurité et la confidentialité, consultez + le code source du projet.{" "} +

+ + + +
+
+ ); +} + +const saveCredentials = (username: string, password: string) => { + localStorage.setItem("bj_username", username); + localStorage.setItem("bj_password", password); +}; +export const getSavedCredentials = () => { + const username = localStorage.getItem("bj_username"); + const password = localStorage.getItem("bj_password"); + if (username && password) { + return { username, password }; + } + return null; +}; diff --git a/app/components/repas/index.tsx b/app/components/repas/index.tsx index a559c35..c87acb3 100644 --- a/app/components/repas/index.tsx +++ b/app/components/repas/index.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { Tabs, TabsList, tabsStyle, TabsTrigger } from "~/components/ui/tabs"; import BottomNavigation from "~/components/bottom-nav"; import { CalendarCheck, ChefHat } from "lucide-react"; -import WIP from "./wip"; import Menus from "./menus"; +import BJRepas from "./credentials"; const tabs = [ { @@ -16,7 +16,7 @@ const tabs = [ value: "bjrepas", label: "BJ Repas", icon: , - content: , + content: , }, ]; diff --git a/app/components/repas/menu-card.tsx b/app/components/repas/menu-card.tsx index ab96bac..c1851ea 100644 --- a/app/components/repas/menu-card.tsx +++ b/app/components/repas/menu-card.tsx @@ -1,16 +1,23 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Utensils, Salad, Cake, CookingPot, Icon, Carrot } from "lucide-react"; +import { + Utensils, + Salad, + Cake, + CookingPot, + Icon, + Carrot, + LogIn, + CalendarCheck, + Check, + CheckCheck, + Loader2, +} from "lucide-react"; import { cheese } from "@lucide/lab"; - -interface Course { - name: string; - description: string; -} - -interface Meal { - name: string; - courses: Course[]; -} +import { registerForMeal, type Meal } from "~/lib/api"; +import { Button } from "../ui/button"; +import { toast } from "sonner"; +import { useState } from "react"; +import { getSavedCredentials } from "./credentials"; const getCourseIcon = (courseName: string) => { const name = courseName.toLowerCase(); @@ -23,7 +30,7 @@ const getCourseIcon = (courseName: string) => { return ; }; -export function DailyMenu({ meals }: { meals: Meal[] }) { +export function DailyMenu({ meals, refetchMeals }: { meals: Meal[]; refetchMeals: () => Promise }) { if (meals.length === 0) { return (
@@ -34,6 +41,28 @@ export function DailyMenu({ meals }: { meals: Meal[] }) { ); } + const [isLoading, setLoading] = useState(false); + const credentials = getSavedCredentials(); + + const handleRegister = async (id: number) => { + try { + if (!credentials) { + toast.error( + "Veuillez d'abord enregistrer vos identifiants dans l'onglet BJRepas." + ); + return; + } + await registerForMeal(id, credentials.username, credentials.password); + await refetchMeals(); + setLoading(false); + } catch (error) { + console.error("Error registering for meal:", error); + toast.error( + "Une erreur est survenue lors de l'inscription au repas. Veuillez réessayer." + ); + } + }; + return (

@@ -73,6 +102,33 @@ export function DailyMenu({ meals }: { meals: Meal[] }) {

))}
+ + {meal.submittable && + (meal.isRegistered ? ( + + ) : isLoading ? ( + + ) : ( + + ))} ))} diff --git a/app/components/repas/menus.tsx b/app/components/repas/menus.tsx index 03330c1..805d8f3 100644 --- a/app/components/repas/menus.tsx +++ b/app/components/repas/menus.tsx @@ -1,31 +1,34 @@ -import { useMenus } from "~/lib/api"; +import { useMeals, type Meal } from "~/lib/api"; import { DailyMenu } from "./menu-card"; import { DateTime } from "luxon"; import { useState } from "react"; import DayNavigation from "./day-navigation"; export default function Menus() { - const { isLoading, error, menus } = useMenus(); + const { isLoading, error, meals, refetch } = useMeals(); const [date, setDate] = useState(DateTime.now().startOf("day")); const findMenuForDate = (date: DateTime) => { - return menus?.find((menu: any) => { - const menuDate = DateTime.fromISO(menu.date).startOf("day"); - return menuDate.equals(date); + return meals?.filter((meal) => { + const mealDate = DateTime.fromISO(meal.date).startOf("day"); + return mealDate.equals(date); }); }; return (
- {isLoading &&

Chargement des menus...

} + {isLoading &&

Chargement du menu...

} {error && (

Erreur lors du chargement des menus.

)} - {menus && menus.length === 0 &&

Aucun menu disponible.

} + {meals && meals.length === 0 &&

Aucun menu disponible.

} {/* Menus */} - +
); } diff --git a/app/components/repas/wip.tsx b/app/components/repas/wip.tsx deleted file mode 100644 index 57ee55f..0000000 --- a/app/components/repas/wip.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Github } from "lucide-react"; -import { Button } from "../ui/button"; - -export default function WIP() { - return ( -
-

- ⚠️ Cette fonctionnalité n’est pas encore implémentée. -

-

- Vous pouvez contribuer au développement du projet ici : -

- -
- ); -} diff --git a/app/lib/api.ts b/app/lib/api.ts index 8b87684..f7a8767 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -461,22 +461,60 @@ export const useAverages = (period: string) => { /** * === REPAS API === */ -const fetchMenus = async () => { - return makeRequest(`/menus`, "Échec de la récupération des menus"); +const fetchMeals = async () => { + return makeRequest(`/meals`, "Échec de la récupération des menus"); }; -export const useMenus = () => { +export const useMeals = () => { const { data, ...props } = useQuery({ - queryKey: ["menus"], - queryFn: fetchMenus, + queryKey: ["meals"], + queryFn: fetchMeals, staleTime: Duration.fromObject({ - hours: 6, // 6 hours + hours: 0, // 6 hours }).toMillis(), gcTime: Duration.fromObject({ days: 1, // 1 day }).toMillis(), }); return { - menus: data || [], + meals: (data as Meal[]) || [], ...props, }; }; + +export interface Meal { + id: number; + date: string; + name: string; + courses: { name: string; description: string }[]; + submittable: boolean; + isRegistered?: boolean; // Optional field to indicate if the user is registered for this meal +} + +export const registerForMeal = async ( + mealId: number, + username: string, + password: string +) => { + return makePostRequest( + `/meals/${mealId}`, + { username, password }, + "Échec de l'inscription au repas", + "POST" + ); +}; + +export const testCredentials = async (username: string, password: string) => { + return makePostRequest( + `/auth/test`, + { username, password }, + "Échec de la vérification des identifiants", + "POST" + ); +}; + +export const getCredentialsStatus = async () => { + return makeRequest( + `/auth/status`, + "Échec de la récupération du statut des identifiants" + ); +};