feat: add stress
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m16s
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m16s
This commit is contained in:
parent
db1a7bca57
commit
fbb9158684
8 changed files with 848 additions and 0 deletions
45
app/components/ui/radio-group.tsx
Normal file
45
app/components/ui/radio-group.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
180
app/components/ui/text-typing.tsx
Normal file
180
app/components/ui/text-typing.tsx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
|
||||
import { useIsInView, type UseIsInViewOptions } from "~/lib/use-is-in-view";
|
||||
import { getStrictContext } from "~/lib/get-strict-context";
|
||||
|
||||
type TypingTextContextType = {
|
||||
isTyping: boolean;
|
||||
setIsTyping: (isTyping: boolean) => void;
|
||||
};
|
||||
|
||||
const [TypingTextProvider, useTypingText] =
|
||||
getStrictContext<TypingTextContextType>("TypingTextContext");
|
||||
|
||||
type TypingTextProps = React.ComponentProps<"span"> & {
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
loop?: boolean;
|
||||
holdDelay?: number;
|
||||
text: string | string[];
|
||||
} & UseIsInViewOptions;
|
||||
|
||||
function TypingText({
|
||||
ref,
|
||||
children,
|
||||
duration = 100,
|
||||
delay = 0,
|
||||
inView = false,
|
||||
inViewMargin = "0px",
|
||||
inViewOnce = true,
|
||||
loop = false,
|
||||
holdDelay = 1000,
|
||||
text,
|
||||
...props
|
||||
}: TypingTextProps) {
|
||||
const { ref: localRef, isInView } = useIsInView(
|
||||
ref as React.Ref<HTMLElement>,
|
||||
{
|
||||
inView,
|
||||
inViewOnce,
|
||||
inViewMargin,
|
||||
}
|
||||
);
|
||||
|
||||
const [isTyping, setIsTyping] = React.useState(false);
|
||||
const [started, setStarted] = React.useState(false);
|
||||
const [displayedText, setDisplayedText] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInView) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setStarted(true);
|
||||
}, delay);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [isInView, delay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!started) return;
|
||||
|
||||
const timeoutIds: Array<ReturnType<typeof setTimeout>> = [];
|
||||
const texts: string[] = typeof text === "string" ? [text] : text;
|
||||
|
||||
const typeText = (str: string, onComplete: () => void) => {
|
||||
setIsTyping(true);
|
||||
let currentIndex = 0;
|
||||
const type = () => {
|
||||
if (currentIndex <= str.length) {
|
||||
setDisplayedText(str.substring(0, currentIndex));
|
||||
currentIndex++;
|
||||
const id = setTimeout(type, duration);
|
||||
timeoutIds.push(id);
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
type();
|
||||
};
|
||||
|
||||
const eraseText = (str: string, onComplete: () => void) => {
|
||||
setIsTyping(true);
|
||||
let currentIndex = str.length;
|
||||
const erase = () => {
|
||||
if (currentIndex >= 0) {
|
||||
setDisplayedText(str.substring(0, currentIndex));
|
||||
currentIndex--;
|
||||
const id = setTimeout(erase, duration);
|
||||
timeoutIds.push(id);
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
erase();
|
||||
};
|
||||
|
||||
const animateTexts = (index: number) => {
|
||||
typeText(texts[index] ?? "", () => {
|
||||
const isLast = index === texts.length - 1;
|
||||
if (isLast && !loop) {
|
||||
return;
|
||||
}
|
||||
const id = setTimeout(() => {
|
||||
eraseText(texts[index] ?? "", () => {
|
||||
const nextIndex = isLast ? 0 : index + 1;
|
||||
animateTexts(nextIndex);
|
||||
});
|
||||
}, holdDelay);
|
||||
timeoutIds.push(id);
|
||||
});
|
||||
};
|
||||
|
||||
animateTexts(0);
|
||||
|
||||
return () => {
|
||||
timeoutIds.forEach(clearTimeout);
|
||||
};
|
||||
}, [text, duration, started, loop, holdDelay]);
|
||||
|
||||
return (
|
||||
<TypingTextProvider value={{ isTyping, setIsTyping }}>
|
||||
<span ref={localRef} data-slot="typing-text" {...props}>
|
||||
<motion.span>{displayedText}</motion.span>
|
||||
{children}
|
||||
</span>
|
||||
</TypingTextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type TypingTextCursorProps = Omit<HTMLMotionProps<"span">, "children">;
|
||||
|
||||
function TypingTextCursor({
|
||||
style,
|
||||
variants,
|
||||
...props
|
||||
}: TypingTextCursorProps) {
|
||||
const { isTyping } = useTypingText();
|
||||
|
||||
return (
|
||||
<motion.span
|
||||
data-slot="typing-text-cursor"
|
||||
variants={{
|
||||
blinking: {
|
||||
opacity: [0, 0, 1, 1],
|
||||
transition: {
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 0,
|
||||
ease: "linear",
|
||||
times: [0, 0.5, 0.5, 1],
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
},
|
||||
...variants,
|
||||
}}
|
||||
animate={isTyping ? "visible" : "blinking"}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
height: "16px",
|
||||
transform: "translateY(2px)",
|
||||
width: "1px",
|
||||
backgroundColor: "currentColor",
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
TypingText,
|
||||
TypingTextCursor,
|
||||
type TypingTextProps,
|
||||
type TypingTextCursorProps,
|
||||
};
|
||||
36
app/lib/get-strict-context.tsx
Normal file
36
app/lib/get-strict-context.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
|
||||
function getStrictContext<T>(
|
||||
name?: string
|
||||
): readonly [
|
||||
({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: T;
|
||||
children?: React.ReactNode;
|
||||
}) => React.JSX.Element,
|
||||
() => T
|
||||
] {
|
||||
const Context = React.createContext<T | undefined>(undefined);
|
||||
|
||||
const Provider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: T;
|
||||
children?: React.ReactNode;
|
||||
}) => <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
|
||||
const useSafeContext = () => {
|
||||
const ctx = React.useContext(Context);
|
||||
if (ctx === undefined) {
|
||||
throw new Error(`useContext must be used within ${name ?? "a Provider"}`);
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
return [Provider, useSafeContext] as const;
|
||||
}
|
||||
|
||||
export { getStrictContext };
|
||||
25
app/lib/use-is-in-view.tsx
Normal file
25
app/lib/use-is-in-view.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react";
|
||||
import { useInView, type UseInViewOptions } from "motion/react";
|
||||
|
||||
interface UseIsInViewOptions {
|
||||
inView?: boolean;
|
||||
inViewOnce?: boolean;
|
||||
inViewMargin?: UseInViewOptions["margin"];
|
||||
}
|
||||
|
||||
function useIsInView<T extends HTMLElement = HTMLElement>(
|
||||
ref: React.Ref<T>,
|
||||
options: UseIsInViewOptions = {}
|
||||
) {
|
||||
const { inView, inViewOnce = false, inViewMargin = "0px" } = options;
|
||||
const localRef = React.useRef<T>(null);
|
||||
React.useImperativeHandle(ref, () => localRef.current as T);
|
||||
const inViewResult = useInView(localRef, {
|
||||
once: inViewOnce,
|
||||
margin: inViewMargin,
|
||||
});
|
||||
const isInView = !inView || inViewResult;
|
||||
return { ref: localRef, isInView };
|
||||
}
|
||||
|
||||
export { useIsInView, type UseIsInViewOptions };
|
||||
|
|
@ -9,4 +9,5 @@ export default [
|
|||
route("/settings", "routes/settings.tsx"),
|
||||
route("/grades", "routes/grades.tsx"),
|
||||
route("/repas", "routes/repas.tsx"),
|
||||
route("/stress", "routes/stress.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
|
|
|||
435
app/routes/stress.tsx
Normal file
435
app/routes/stress.tsx
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Info } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
|
||||
import { TypingText, TypingTextCursor } from "~/components/ui/text-typing";
|
||||
|
||||
const ENCOURAGING_MESSAGES = [
|
||||
// --- Les Classiques (Gardés) ---
|
||||
"Tu es plus fort(e) que tu ne le penses. Continue comme ça ! 💪",
|
||||
"Chaque jour est un pas de plus vers ton objectif. Bravo ! 🌟",
|
||||
"Ta persévérance est admirable. Tu vas réussir ! 🎯",
|
||||
"N'oublie pas de te reposer, le sommeil fixe la mémoire ! 😌",
|
||||
"Les meilleurs résultats viennent avec le temps. Patience ! ⏰",
|
||||
"Tu fais un travail incroyable. Continue ainsi ! 🚀",
|
||||
"Crois en toi, tu as tout pour réussir ! ✨",
|
||||
"Chaque exercice résolu te rapproche de ton rêve ! 📚",
|
||||
"Ton travail acharné paiera, garde confiance ! 🌈",
|
||||
"Tu es sur la bonne voie. Reste concentré(e) ! 🎓",
|
||||
"Prends soin de toi, ta santé mentale est ta meilleure arme ! 💙",
|
||||
"Ta détermination est inspirante ! 🔥",
|
||||
"Les concours ne sont qu'une étape, tu es déjà quelqu'un de formidable ! 🌸",
|
||||
"N'aie pas peur des erreurs, c'est là que tu apprends le plus ! 📝",
|
||||
"Tu progresses chaque jour, même les jours où tu as l'impression de reculer ! 🌱",
|
||||
"Ta famille et tes amis sont fiers de toi ! 💖",
|
||||
"Le meilleur est à venir, l'école de tes rêves t'attend ! 🌅",
|
||||
"Chaque difficulté surmontée te rend plus fort(e) ! 💎",
|
||||
"Tu as choisi un chemin difficile, sois fier(e) de le parcourir ! 🏔️",
|
||||
"N'oublie pas pourquoi tu as commencé. Tu vas y arriver ! 🎪",
|
||||
|
||||
// --- Les Nouveaux (Spécial Prépa) ---
|
||||
"C'est un marathon, pas un sprint. Respire. 🏃",
|
||||
"Même les major de promo doutent. Toi, tu avances. 🛡️",
|
||||
"Une mauvaise note en khôlle ne définit pas ta valeur. Relève-toi. 🦋",
|
||||
"Rappelle-toi la sensation de comprendre enfin une démo complexe. Cherche ça ! 💡",
|
||||
"Tu es en train de te forger une capacité de travail pour la vie. 🧠",
|
||||
"L'effort d'aujourd'hui est le sourire de toi-même en juillet. ☀️",
|
||||
"Fais-le pour le 'toi' qui recevra son affectation SIGEM. 📩",
|
||||
"La douleur de la discipline pèse moins lourd que celle du regret. ⚖️",
|
||||
"Tu n'as pas besoin d'être un génie, juste d'être constant. 🐢",
|
||||
"Visualise ton nom sur la liste des admis. Ça arrive. 👁️",
|
||||
"Ce chapitre qui te résiste finira par céder. Insiste. 🔨",
|
||||
"Tu n'es pas seul(e), des milliers d'autres galèrent aussi. Courage ! 🤝",
|
||||
"Prends 5 minutes pour t'aérer, tu seras plus efficace après. 🌳",
|
||||
"La prépa, c'est l'école de l'humilité et de la résilience. Tu gères. 🧘",
|
||||
"Chaque théorème appris est une munition pour le jour J. 🔫",
|
||||
"Ne te compare pas aux autres, compare-toi à toi hier. 📈",
|
||||
"Tu construis ton avenir, brique par brique, DM par DM. 🏗️",
|
||||
"C'est normal d'être fatigué(e), tu accomplis quelque chose de grand. 🛌",
|
||||
"Garde la tête froide et le cœur chaud. ❤️",
|
||||
"Il n'y a pas de question bête, il n'y a que des points à gagner. 🙋",
|
||||
"Le déclic arrive souvent quand on s'y attend le moins. ⚡",
|
||||
"Sois bienveillant(e) avec toi-même, tu es ton propre moteur. 🚗",
|
||||
"Regarde tout le chemin parcouru depuis le début de l'année ! 🗺️",
|
||||
"Tu es capable de comprendre des choses que 99% des gens ignorent. 🌌",
|
||||
"L'intégration n'est pas une question de chance, c'est une question de ténacité. 🍀",
|
||||
"Buvard ou Major, l'important c'est de tout donner. 🦁",
|
||||
"Ton cerveau est un muscle, et là, tu fais de la muscu de haut niveau. 🏋️",
|
||||
"Rien n'est joué jusqu'à la dernière épreuve. Fonce ! 🏁",
|
||||
"La réussite, c'est tomber 7 fois et se relever 8. 🩹",
|
||||
"Fais confiance à ta préparation. Tu as bossé pour ça. 🧱",
|
||||
"Un jour, tu riras de cette période avec tes potes d'école. 🍻",
|
||||
"Tu n'étudies pas pour le prof, tu étudies pour ta liberté future. 🕊️",
|
||||
"Accroche-toi, la vue est superbe depuis le sommet. ⛰️",
|
||||
"Ta capacité à encaisser la charge de travail est impressionnante. 🔋",
|
||||
"Ne laisse pas un DS raté gâcher ta semaine. Reset. 🔄",
|
||||
"Tu es l'architecte de ta propre réussite. 📐",
|
||||
"Chaque minute investie maintenant te rapportera des années de confort. 🏦",
|
||||
"Tu as le droit de craquer, mais tu as le devoir de revenir. 🥊",
|
||||
"Pense à la fierté de porter le tricorne, le képi ou l'uniforme. 🧢",
|
||||
"La prépa t'apprend à réfléchir, personne ne pourra t'enlever ça. 🗝️",
|
||||
"C'est dur ? C'est que tu es en train de monter de niveau. 🆙",
|
||||
"Garde le cap, la tempête finit toujours par passer. ⛵",
|
||||
"Tu es bien plus intelligent(e) que tu ne le crois sous stress. 🧠",
|
||||
"Fais-toi un bon thé/café et remets-toi en selle. ☕",
|
||||
"Le succès est la somme de petits efforts répétés jour après jour. ➕",
|
||||
"Ne lâche rien, l'admissibilité se joue parfois à des détails. 🔍",
|
||||
"Tu as le potentiel pour intégrer l'école qui te correspond. 🧩",
|
||||
"Respire par le ventre. Tout va bien se passer. 🌬️",
|
||||
"Ta volonté est ton plus grand talent. ⭐",
|
||||
"Allez, encore un petit effort. Tu es un(e) guerrier(e). ⚔️",
|
||||
];
|
||||
|
||||
const STRESSFUL_MESSAGES = [
|
||||
// --- Les Classiques (Gardés) ---
|
||||
"Pendant que tu scroll, tes concurrents bossent les intégrales. 📉",
|
||||
"Netflix ou les concours ? Choisis vite. ⏳",
|
||||
"Les places à l'X ne se gagnent pas sur TikTok. 📱",
|
||||
"Ton futur toi te regarde avec déception là. 👀",
|
||||
"Les autres sont déjà au chapitre suivant... 📖",
|
||||
"Ce DM ne va pas se faire tout seul. Allez hop ! 📝",
|
||||
"Chaque minute perdue est une place de perdue au classement. ⚠️",
|
||||
"Tu veux vraiment passer 5/2 ? Bouge-toi ! 🏃",
|
||||
"Tes notes ne vont pas s'améliorer par magie. 📊",
|
||||
"La médiocrité est confortable, mais l'X c'est mieux. 🤔",
|
||||
"Tes concurrents ne prennent pas de pause, eux. ⚡",
|
||||
"Le niveau monte, et toi tu stagnes. Réagis ! 📈",
|
||||
"À ce rythme, tu vas finir à la fac (non je rigole... ou pas). 🔄",
|
||||
"Les colles arrivent, es-tu prêt(e) ? Spoiler : non. 😬",
|
||||
"Tu sais combien il reste de jours ? C'est terrifiant, non ? ⏰",
|
||||
"Pendant que tu lis ce message, 3 exos auraient pu être pliés. 🤷",
|
||||
"La motivation c'est pour les faibles, la discipline c'est pour les forts. 💼",
|
||||
"Tu te rappelles pourquoi tu es en prépa ? Prouve-le ! 🎯",
|
||||
"Les profs ont raison : tu gâches ton potentiel là. 📚",
|
||||
"C'est pas avec des 'demain je m'y mets' que tu vas intégrer Centrale. 🚫",
|
||||
|
||||
// --- Les Nouveaux (Spécial "Violence") ---
|
||||
"Tu vises l'ENS ou l'E3A là ? Ton attitude me met le doute. 🤨",
|
||||
"Ton major de promo a déjà fini le sujet de 2018, lui. 🏎️",
|
||||
"C'est pas en regardant le plafond que le théorème va rentrer. 🧱",
|
||||
"Si tu ne bosses pas maintenant, prépare ton dossier Parcoursup. 📂",
|
||||
"Dors ou travaille. Tout le reste est une perte de temps. 💤",
|
||||
"Arrête de te mentir, tu n'es pas en 'pause', tu procrastines. 🤥",
|
||||
"L'examinateur s'en fiche que tu sois fatigué. Il veut le résultat. 👹",
|
||||
"Tu crois que Cauchy a procrastiné pour ses suites ? Non. 🔢",
|
||||
"Ta mention au bac ne te sauvera pas aux concours. 🔥",
|
||||
"La 5/2, c'est bien, mais intégrer en 3/2 c'est mieux. 🦄",
|
||||
"Imagine ta tête devant le sujet si tu continues comme ça. 😱",
|
||||
"Ton téléphone est ton pire ennemi. Éteins-le. 📵",
|
||||
"Moins de story, plus de théorie. 📸",
|
||||
"Tu veux finir ingénieur ou touriste ? 👷",
|
||||
"Les Mines de Paris ne recrutent pas sur Candy Crush. 🍬",
|
||||
"C'est le moment de souffrir pour ne pas pleurer en juillet. 😭",
|
||||
"Tu connais tes formules de trigo ? J'en doute. Vérifie. 📐",
|
||||
"Le jury ne fera pas de cadeau. Pourquoi tu t'en fais ? 🎁",
|
||||
"Tu as vu le sujet de l'an dernier ? T'es pas prêt. 📉",
|
||||
"Arrête de rêver ta vie, va étudier ta physique. ⚛️",
|
||||
"La chimie orga ne s'apprend pas par osmose. 🧪",
|
||||
"Chaque heure de perdue est une victoire pour ton concurrent direct. 🤺",
|
||||
"C'est le moment de passer en mode machine. 🤖",
|
||||
"Tu te reposeras après les écrits. Pour l'instant, au charbon. ⛏️",
|
||||
"Si c'était facile, tout le monde serait à Polytechnique. 🏰",
|
||||
"Ton excuse pour ne pas bosser est nulle. Retourne travailler. 🚮",
|
||||
"L'avenir appartient à ceux qui se lèvent tôt (et qui font des maths). 🌅",
|
||||
"Tu préfères la douleur de l'effort ou la douleur de l'échec ? 🎭",
|
||||
"C'est pas le moment de flancher, c'est le money time ! 💰",
|
||||
"Regarde tes notes de la dernière khôlle. Ça te suffit ? 📉",
|
||||
"Tu n'es pas fatigué, tu es juste démotivé. Change ça. 🔋",
|
||||
"La chance ne sourit qu'aux esprits bien préparés. 🍀",
|
||||
"Tu veux voir tes amis intégrer pendant que tu redoubles ? 👯",
|
||||
"Fais-le pour humilier ceux qui n'ont pas cru en toi. 😈",
|
||||
"Pas de bras, pas de chocolat. Pas de travail, pas d'école. 🍫",
|
||||
"Ton avenir se joue dans les 30 prochaines minutes. ⏳",
|
||||
"L'excellence est une habitude, pas un acte isolé. 🏆",
|
||||
"Arrête de chouiner et va faire un DL. 🔢",
|
||||
"Tu as déjà oublié ton cours d'hier ? C'est grave. 🧠",
|
||||
"Le concours blanc approche. La panique aussi. 🚨",
|
||||
"Tu penses que tu as le niveau ? Prouve-le sur une feuille blanche. 📄",
|
||||
"C'est pas le moment de faire du social. C'est la guerre. ⚔️",
|
||||
"Tu veux une médaille ? Va la chercher avec tes dents. 🏅",
|
||||
"La prépa c'est la jungle. Mange ou sois mangé. 🦁",
|
||||
"Ton lit est confortable, mais le chômage l'est moins. 🛋️",
|
||||
"Si tu lis ça, c'est que tu ne bosses pas. Allez, oust ! 👉",
|
||||
"Tu as 5 minutes pour t'y mettre, sinon c'est mort pour aujourd'hui. ⏲️",
|
||||
"Ne sois pas celui qui dit 'j'aurais dû'. Sois celui qui l'a fait. ✅",
|
||||
"La physique quantique t'attend. Elle n'est pas patiente. 🌌",
|
||||
"Arrête de scroller, ça ne va pas augmenter ton admissibilité. 🛑",
|
||||
];
|
||||
|
||||
export default function StressPrepaPage() {
|
||||
const [timeLeft, setTimeLeft] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [messageMode, setMessageMode] = useState<
|
||||
"encouraging" | "stressful" | "none"
|
||||
>("none");
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const examDate = new Date("2026-04-13T07:00:00");
|
||||
const schoolYearStart = new Date("2025-09-01T00:00:00");
|
||||
const storedMode = localStorage.getItem("stress-prepa-message-mode");
|
||||
if (
|
||||
storedMode === "encouraging" ||
|
||||
storedMode === "stressful" ||
|
||||
storedMode === "none"
|
||||
) {
|
||||
setMessageMode(storedMode);
|
||||
}
|
||||
|
||||
const updateCountdown = () => {
|
||||
const now = new Date();
|
||||
const difference = examDate.getTime() - now.getTime();
|
||||
|
||||
if (difference > 0) {
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((difference / (1000 * 60 * 60)) % 24);
|
||||
const minutes = Math.floor((difference / 1000 / 60) % 60);
|
||||
const seconds = Math.floor((difference / 1000) % 60);
|
||||
|
||||
setTimeLeft({ days, hours, minutes, seconds });
|
||||
|
||||
// Calculate progress percentage
|
||||
const totalDuration = examDate.getTime() - schoolYearStart.getTime();
|
||||
const elapsed = now.getTime() - schoolYearStart.getTime();
|
||||
const progressPercent = Math.min(
|
||||
Math.max((elapsed / totalDuration) * 100, 0),
|
||||
100
|
||||
);
|
||||
setProgress(progressPercent);
|
||||
} else {
|
||||
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
setProgress(100);
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageMode === "none") {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
const messages =
|
||||
messageMode === "encouraging" ? ENCOURAGING_MESSAGES : STRESSFUL_MESSAGES;
|
||||
setMessages(messages);
|
||||
}, [messageMode]);
|
||||
|
||||
const formatNumber = (num: number) => num.toString().padStart(2, "0");
|
||||
|
||||
return (
|
||||
<div className="relative min-h-full flex flex-col items-center justify-center bg-background px-4 overflow-hidden">
|
||||
{/* Animated gradient background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-background via-muted/20 to-background animate-gradient" />
|
||||
|
||||
{/* Enhanced grid pattern */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, currentColor 1px, transparent 1px),
|
||||
linear-gradient(to bottom, currentColor 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "64px 64px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute z-100 top-4 right-4 rounded-full bg-card/80 backdrop-blur-sm hover:bg-card border-border/50 shadow-lg"
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
<span className="sr-only">Informations</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Stress Prépa
|
||||
</DialogTitle>
|
||||
<DialogContent className="space-y-4 pt-4 text-base">
|
||||
<div className="space-y-2">
|
||||
<p className="text-foreground/90">
|
||||
Compte à rebours jusqu'aux concours
|
||||
</p>
|
||||
<div className="h-px bg-border/50" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
Début des concours
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-foreground">
|
||||
13 Avril 2026
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
Début de l'année
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-foreground">
|
||||
1er Sept. 2025
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-4">
|
||||
<div className="h-px bg-border/50" />
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold text-foreground">
|
||||
Mode des messages
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={messageMode}
|
||||
onValueChange={(value) => {
|
||||
localStorage.setItem("stress-prepa-message-mode", value);
|
||||
setMessageMode(
|
||||
value as "encouraging" | "stressful" | "none"
|
||||
);
|
||||
}}
|
||||
className="space-y-0"
|
||||
>
|
||||
<div className="flex items-center space-x-3 px-3 pt-3 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<RadioGroupItem value="none" id="none" />
|
||||
<Label
|
||||
htmlFor="none"
|
||||
className="font-normal cursor-pointer flex-1"
|
||||
>
|
||||
Aucun message 🤐
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 px-3 py-1 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<RadioGroupItem value="encouraging" id="encouraging" />
|
||||
<Label
|
||||
htmlFor="encouraging"
|
||||
className="font-normal cursor-pointer flex-1"
|
||||
>
|
||||
Encouragement 🙌
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 px-3 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<RadioGroupItem value="stressful" id="stressful" />
|
||||
<Label
|
||||
htmlFor="stressful"
|
||||
className="font-normal cursor-pointer flex-1"
|
||||
>
|
||||
Trash talk 🤡
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<div className="h-px bg-border/50 mb-4" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Tous droits réservés Khollisé © {new Date().getFullYear()} —
|
||||
Messages générés avec l'intelligence artificielle.
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="relative w-full max-w-6xl mx-auto text-center space-y-16">
|
||||
{/* Enhanced title section */}
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-5xl md:text-7xl font-bold tracking-tight text-balance leading-tight">
|
||||
Temps avant les concours
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||
<div className="group bg-card border-2 border-border/50 rounded-2xl p-6 md:p-10 transition-all hover:border-border hover:shadow-xl hover:scale-[1.02]">
|
||||
<div className="text-6xl md:text-8xl font-bold tabular-nums tracking-tight leading-none">
|
||||
{timeLeft.days}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-muted-foreground mt-3 font-medium uppercase tracking-wider">
|
||||
Jour{timeLeft.days > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="group bg-card border-2 border-border/50 rounded-2xl p-6 md:p-10 transition-all hover:border-border hover:shadow-xl hover:scale-[1.02]">
|
||||
<div className="text-6xl md:text-8xl font-bold tabular-nums tracking-tight leading-none">
|
||||
{formatNumber(timeLeft.hours)}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-muted-foreground mt-3 font-medium uppercase tracking-wider">
|
||||
Heure{timeLeft.hours > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="group bg-card border-2 border-border/50 rounded-2xl p-6 md:p-10 transition-all hover:border-border hover:shadow-xl hover:scale-[1.02]">
|
||||
<div className="text-6xl md:text-8xl font-bold tabular-nums tracking-tight leading-none">
|
||||
{formatNumber(timeLeft.minutes)}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-muted-foreground mt-3 font-medium uppercase tracking-wider">
|
||||
Minute{timeLeft.minutes > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="group bg-card border-2 border-border/50 rounded-2xl p-6 md:p-10 transition-all hover:border-border hover:shadow-xl hover:scale-[1.02]">
|
||||
<div className="text-6xl md:text-8xl font-bold tabular-nums tracking-tight leading-none">
|
||||
{formatNumber(timeLeft.seconds)}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-muted-foreground mt-3 font-medium uppercase tracking-wider">
|
||||
Seconde{timeLeft.seconds > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-10 flex items-center justify-center px-4">
|
||||
{messages.length > 0 && (
|
||||
<TypingText
|
||||
text={messages}
|
||||
duration={25}
|
||||
holdDelay={5000}
|
||||
className="text-lg md:text-xl text-muted-foreground/90 italic max-w-3xl leading-relaxed"
|
||||
>
|
||||
<TypingTextCursor className="!h-6 !w-1 rounded-full ml-1 -mb-1" />
|
||||
</TypingText>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
<div className="relative w-full h-4 bg-muted/50 rounded-full overflow-hidden border border-border/30 shadow-inner">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-primary rounded-full transition-all duration-1000 ease-out shadow-lg"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Progression de l'année
|
||||
</span>
|
||||
<span className="font-mono font-bold text-lg text-foreground tabular-nums">
|
||||
{progress.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
"katex": "^0.16.22",
|
||||
"lucide-react": "^0.511.0",
|
||||
"luxon": "^3.7.1",
|
||||
"motion": "^12.23.26",
|
||||
"netlify-cli": "^22.1.3",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "^9.8.1",
|
||||
|
|
|
|||
124
pnpm-lock.yaml
generated
124
pnpm-lock.yaml
generated
|
|
@ -38,6 +38,9 @@ importers:
|
|||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.14
|
||||
version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.2.5
|
||||
version: 2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
|
|
@ -101,6 +104,9 @@ importers:
|
|||
luxon:
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
motion:
|
||||
specifier: ^12.23.26
|
||||
version: 12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
netlify-cli:
|
||||
specifier: ^22.1.3
|
||||
version: 22.4.0(@types/node@20.19.9)(idb-keyval@6.2.2)(picomatch@4.0.3)(rollup@2.79.2)
|
||||
|
|
@ -1965,6 +1971,19 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-radio-group@1.3.8':
|
||||
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.10':
|
||||
resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
|
||||
peerDependencies:
|
||||
|
|
@ -1978,6 +1997,19 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.11':
|
||||
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.2.5':
|
||||
resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==}
|
||||
peerDependencies:
|
||||
|
|
@ -3871,6 +3903,20 @@ packages:
|
|||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
framer-motion@12.23.26:
|
||||
resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -4876,6 +4922,26 @@ packages:
|
|||
resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
motion-dom@12.23.23:
|
||||
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
motion@12.23.26:
|
||||
resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
move-file@3.1.0:
|
||||
resolution: {integrity: sha512-4aE3U7CCBWgrQlQDMq8da4woBWDGHioJFiOZ8Ie6Yq2uwYQ9V2kGhTz4x3u6Wc+OU17nw0yc3rJ/lQ4jIiPe3A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
|
@ -8694,6 +8760,24 @@ snapshots:
|
|||
'@types/react': 19.1.8
|
||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||
|
||||
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.8
|
||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
|
|
@ -8711,6 +8795,23 @@ snapshots:
|
|||
'@types/react': 19.1.8
|
||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1)
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.8
|
||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||
|
||||
'@radix-ui/react-select@2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.1
|
||||
|
|
@ -10755,6 +10856,15 @@ snapshots:
|
|||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
framer-motion@12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
motion-dom: 12.23.23
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
from2@2.3.0:
|
||||
|
|
@ -11740,6 +11850,20 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
motion-dom@12.23.23:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
motion@12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
framer-motion: 12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
move-file@3.1.0:
|
||||
dependencies:
|
||||
path-exists: 5.0.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue