diff --git a/src/App.tsx b/src/App.tsx index 0ebfbf6..872bfe5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,10 +25,7 @@ import { LoginPage } from "./pages/secondary/login"; import { AgendaPage } from "./pages/secondary/agenda"; import { WelcomePage } from "./pages/secondary/welcome"; import * as Sentry from "@sentry/react"; -import { useEffect, useState } from "react"; -import { overrideStorage, saveFromApp } from "./lib/utils/storage"; -import { Loader } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { InitialGetter } from "./contexts/initialGetter"; if (import.meta.env.PROD) { console.log("Initializing Sentry in production mode..."); @@ -41,7 +38,7 @@ if (import.meta.env.PROD) { Sentry.replayIntegration(), Sentry.browserTracingIntegration(), ], - replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysSessionSampleRate: 0, // This sets the sample rate at 0%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. tracesSampleRate: 1.0, // Adjust this value in production as needed }); @@ -95,82 +92,21 @@ function AppRoutes() { } function App() { - const [isLoading, setIsLoading] = useState(true); - const { t } = useTranslation(); - - useEffect(() => { - const timer = setTimeout(() => { - setIsLoading(false); - }, 750); - - return () => { - clearTimeout(timer); - }; - }, []); - - useEffect(() => { - // Écouter la réponse d'Ionic pour récupérer une donnée - const handleMessage = async (event: MessageEvent) => { - const { type, key, payload } = event.data; - if (type === "DATA_RESPONSE" && key) { - console.log(`Donnée reçue pour ${key}: `, payload); - saveFromApp(key, payload); - } - if (type === "ALL_DATA_RESPONSE" && payload) { - console.log("Toutes les données reçues: ", payload); - for (const [k, v] of Object.entries(payload)) { - await new Promise((resolve) => { - setTimeout(() => { - overrideStorage({ [k]: v } as Record< - string, - string - >); - resolve(); - }, 0); - }); - } - setTimeout(() => { - setIsLoading(false); - }, 200); - } - }; - - console.log("Requesting all data from parent..."); - window.parent.postMessage({ type: "REQUEST_ALL_DATA" }, "*"); - - window.addEventListener("message", handleMessage); - - return () => { - window.removeEventListener("message", handleMessage); - }; - }, []); - - if (isLoading) { - return ( -
-

- {t("common.loading")} -
- {t("common.loadingWeb")} -

- -
- ); - } - return ( - - - - - - - + + + + + + + + + diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 6246626..5c98d1e 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -197,6 +197,13 @@ export default function Sidebar() { handleNavigate("/login"); }; + const handleModeBeta = () => { + window.parent.postMessage( + { type: "MODE_BETA", payload: preprodURL }, + "*" + ); + }; + return ( @@ -373,9 +380,7 @@ export default function Sidebar() { variant="link" size="sm" className="p-0 ml-2" - onClick={() => - window.parent.location.assign(preprodURL) - } + onClick={handleModeBeta} > mode Beta diff --git a/src/contexts/initialGetter.tsx b/src/contexts/initialGetter.tsx new file mode 100644 index 0000000..d6e9a3f --- /dev/null +++ b/src/contexts/initialGetter.tsx @@ -0,0 +1,102 @@ +import { ReactNode, useEffect, useRef, useState } from "react"; +import { overrideStorage, saveFromApp } from "@/lib/utils/storage"; +import { Loader } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface Props { + children: ReactNode; +} + +export const InitialGetter = ({ children }: Props) => { + const [isLoading, setIsLoading] = useState(true); + const nbTryRef = useRef(0); + const hasTimedOut = useRef(false); + const { t } = useTranslation(); + + useEffect(() => { + const MAX_RETRIES = 3; + const TIMEOUT_MS = 1500; // global timeout 1.5s + + const requestData = () => { + console.log("Requesting all data from parent..."); + window.parent.postMessage({ type: "REQUEST_ALL_DATA" }, "*"); + }; + + const handleMessage = async (event: MessageEvent) => { + const { type, key, payload } = event.data ?? {}; + + if (type === "DATA_RESPONSE" && key) { + console.log("Data received for %s:", key, payload); + saveFromApp(key, payload); + return; + } + + if (type === "ALL_DATA_RESPONSE") { + console.log("All data received:", payload); + + if (payload["email"]) { + overrideStorage(payload as Record); + setIsLoading(false); + } else { + nbTryRef.current += 1; + console.warn( + `No email found (attempt ${nbTryRef.current}/${MAX_RETRIES})` + ); + + if ( + nbTryRef.current < MAX_RETRIES && + !hasTimedOut.current + ) { + const delay = 200 * nbTryRef.current; // 200ms, 400ms, 600ms + setTimeout(requestData, delay); + } else { + console.warn("Giving up after 3 failed attempts."); + setIsLoading(false); + } + } + } + }; + + // Initial request + requestData(); + + // Global timeout in case parent never answers + const timeout = setTimeout(() => { + if (isLoading) { + hasTimedOut.current = true; + console.warn("Global timeout reached, skipping data sync."); + setIsLoading(false); + } + }, TIMEOUT_MS); + + window.addEventListener("message", handleMessage); + return () => { + window.removeEventListener("message", handleMessage); + clearTimeout(timeout); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Minimum splash duration (smooth UX) + useEffect(() => { + const timer = setTimeout(() => { + if (isLoading) return; + }, 750); + return () => clearTimeout(timer); + }, [isLoading]); + + if (isLoading) { + return ( +
+

+ {t("common.loading")} +
+ {t("common.loadingWeb")} +

+ +
+ ); + } + + return <>{children}; +}; diff --git a/src/pages/absences/page.tsx b/src/pages/absences/page.tsx index 9bfcfb2..4cfb0e7 100644 --- a/src/pages/absences/page.tsx +++ b/src/pages/absences/page.tsx @@ -39,7 +39,8 @@ export function AbsencesPage() { isFetching, } = useQuery({ queryKey: ["absences"], - queryFn: () => fetchAbsences().then((res) => res?.data || []), + queryFn: (): Promise => + fetchAbsences().then((res) => res?.data || absences), staleTime: 1000 * 60 * 5, // 5 min frais gcTime: 1000 * 60 * 60 * 24, // 24h cache refetchOnWindowFocus: true, // refresh background si focus fenêtre diff --git a/src/pages/grades/page.tsx b/src/pages/grades/page.tsx index 9c266ae..334cda8 100644 --- a/src/pages/grades/page.tsx +++ b/src/pages/grades/page.tsx @@ -48,7 +48,8 @@ export function GradesPage() { isFetching, } = useQuery({ queryKey: ["grades"], - queryFn: () => fetchGrades().then((res) => res?.data || []), + queryFn: (): Promise => + fetchGrades().then((res) => res?.data || grades), staleTime: 1000 * 60 * 5, // 5 min frais gcTime: 1000 * 60 * 60 * 24, // 24h cache refetchOnWindowFocus: true, // refresh background si focus fenêtre diff --git a/src/pages/home/page.tsx b/src/pages/home/page.tsx index 7ce3674..735bbcf 100644 --- a/src/pages/home/page.tsx +++ b/src/pages/home/page.tsx @@ -31,7 +31,8 @@ export function HomePage() { isFetching, } = useQuery({ queryKey: ["planning"], - queryFn: () => fetchPlanning().then((res) => res?.data || []), + queryFn: (): Promise => + fetchPlanning().then((res) => res?.data || lessons), staleTime: 1000 * 60 * 5, // 5 min frais gcTime: 1000 * 60 * 60 * 24, // 24h cache refetchIntervalInBackground: true, diff --git a/src/pages/home/sections.tsx b/src/pages/home/sections.tsx index 79fdc58..42cb9f5 100644 --- a/src/pages/home/sections.tsx +++ b/src/pages/home/sections.tsx @@ -164,7 +164,7 @@ export const ImportantMessage = ({ message }: { message?: MessageEntry }) => ( initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, ease: "easeOut", delay: 0.05 }} - className="bg-white rounded-lg" + className="rounded-lg bg-white dark:bg-mauria-alert oled:bg-black" > diff --git a/src/pages/planning/page.tsx b/src/pages/planning/page.tsx index 422c0a4..cbaafda 100644 --- a/src/pages/planning/page.tsx +++ b/src/pages/planning/page.tsx @@ -44,7 +44,8 @@ export function PlanningPage() { dataUpdatedAt, } = useQuery({ queryKey: ["planning"], - queryFn: () => fetchPlanning().then((res) => res?.data || []), + queryFn: (): Promise => + fetchPlanning().then((res) => res?.data || lessons), staleTime: 1000 * 60 * 5, // 5 min frais gcTime: 1000 * 60 * 60 * 24, // 24h cache refetchOnWindowFocus: true, // refresh background si focus fenêtre diff --git a/src/pages/secondary/welcome.tsx b/src/pages/secondary/welcome.tsx index f211364..4cce43d 100644 --- a/src/pages/secondary/welcome.tsx +++ b/src/pages/secondary/welcome.tsx @@ -14,6 +14,7 @@ import { useQuery } from "@tanstack/react-query"; import { fetchPlanning } from "@/lib/api/aurion"; import { saveToStorage } from "@/lib/utils/storage"; import { useTranslation } from "react-i18next"; +import { Lesson } from "@/types/aurion"; type WelcomeSection = { title: string; @@ -61,9 +62,14 @@ const WELCOME_SECTIONS: WelcomeSection[] = [ export function WelcomePage() { const navigate = useNavigate(); const { t } = useTranslation(); - const { isLoading, isFetching } = useQuery({ + const { + isLoading, + isFetching, + data: lessons = [], + } = useQuery({ queryKey: ["planning"], - queryFn: () => fetchPlanning().then((res) => res?.data || []), + queryFn: (): Promise => + fetchPlanning().then((res) => res?.data || lessons), staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 60 * 24, refetchOnWindowFocus: false,