Feat/polish-intro #11

Merged
math-pixel merged 20 commits from feat/polisth-intro into develop 2026-05-31 09:01:18 +00:00
15 changed files with 907 additions and 0 deletions
Showing only changes of commit a2cff0567e - Show all commits
+52
View File
@@ -0,0 +1,52 @@
import { useState } from "react";
interface SiteButtonProps {
label: string;
disabled?: boolean;
onClick: () => void;
}
export function SiteButton({
label,
disabled = false,
onClick,
}: SiteButtonProps): React.JSX.Element {
const [isPressed, setIsPressed] = useState(false);
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onMouseLeave={() => setIsPressed(false)}
style={{
display: "inline-flex",
padding: "12px 20px",
justifyContent: "center",
alignItems: "center",
gap: 10,
background: disabled ? "#b0b0b0" : "#FFF",
boxShadow: disabled
? "none"
: isPressed
? "0 4px 10px 0 rgba(0, 0, 0, 0.35)"
: "0 7px 14.4px 0 rgba(0, 0, 0, 0.25)",
border: "none",
cursor: disabled ? "not-allowed" : "pointer",
color: disabled ? "#888888" : "#000",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(18px, 3vw, 26px)",
fontStyle: "normal",
fontWeight: 700,
lineHeight: "normal",
letterSpacing: "-1.3px",
textTransform: "uppercase",
transition: "box-shadow 0.15s ease",
}}
>
{label}
</button>
);
}
+68
View File
@@ -0,0 +1,68 @@
import type { SiteCardConfig } from "@/data/site/siteConfig";
interface SiteCardProps {
config: SiteCardConfig;
selected: boolean;
onSelect: () => void;
}
export function SiteCard({
config,
selected,
onSelect,
}: SiteCardProps): React.JSX.Element {
const { label, imagePath, disabled } = config;
const getBackground = (): string => {
if (imagePath) return `url(${imagePath}) center/cover`;
if (disabled) return "#b8b8b8";
if (selected) return "#d9d9d9";
return "#e8e8e8";
};
const getBorder = (): string => {
if (selected) return "3px solid #a8d5a2";
if (disabled) return "none";
return "2px solid #ffffff";
};
const getTextColor = (): string => {
if (disabled) return "#888888";
return "#666666";
};
return (
<button
type="button"
onClick={onSelect}
disabled={disabled}
style={{
width: "clamp(120px, 15vw, 160px)",
height: "clamp(140px, 18vw, 180px)",
border: getBorder(),
background: getBackground(),
cursor: disabled ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.15s ease",
outline: "none",
flexShrink: 0,
}}
>
{!imagePath && (
<span
style={{
color: getTextColor(),
fontSize: "clamp(10px, 1.5vw, 14px)",
fontWeight: 500,
textAlign: "center",
padding: 8,
}}
>
{label}
</span>
)}
</button>
);
}
@@ -0,0 +1,83 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useSiteStore } from "@/managers/stores/useSiteStore";
const DISCLAIMER_TEXT =
"Ce site a été conçu pour être utilisé sur ordinateur.\nPour une meilleure expérience, assurez-vous d'avoir une bonne connexion internet et une machine performante.";
const TEXT_DISPLAY_DURATION = 5000;
const FADE_OUT_DURATION = 1000;
const TRANSITION_DELAY = 250;
/**
* Screen 0: Disclaimer
*/
export function SiteDisclaimerScreen(): React.JSX.Element {
const setStep = useSiteStore((state) => state.setStep);
const [textOpacity, setTextOpacity] = useState(0);
const hasSkipped = useRef(false);
const handleSkip = useCallback(() => {
if (hasSkipped.current) return;
hasSkipped.current = true;
setStep("welcome");
}, [setStep]);
useEffect(() => {
// Fade in text
const fadeInTimeout = window.setTimeout(() => {
setTextOpacity(1);
}, 100);
// Start fade out after display duration
const fadeOutTimeout = window.setTimeout(() => {
setTextOpacity(0);
}, TEXT_DISPLAY_DURATION);
// Transition to welcome after fade out + delay
const transitionTimeout = window.setTimeout(
() => {
handleSkip();
},
TEXT_DISPLAY_DURATION + FADE_OUT_DURATION + TRANSITION_DELAY,
);
return () => {
window.clearTimeout(fadeInTimeout);
window.clearTimeout(fadeOutTimeout);
window.clearTimeout(transitionTimeout);
};
}, [handleSkip]);
return (
<div
onClick={handleSkip}
style={{
position: "fixed",
inset: 0,
background: "#000",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 48,
cursor: "pointer",
}}
>
<p
style={{
color: "#F2F2F2",
textAlign: "center",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 20,
fontWeight: 400,
lineHeight: 1.6,
maxWidth: 800,
opacity: textOpacity,
transition: `opacity ${FADE_OUT_DURATION}ms ease-in-out`,
whiteSpace: "pre-wrap",
}}
>
{DISCLAIMER_TEXT}
</p>
</div>
);
}
+33
View File
@@ -0,0 +1,33 @@
import type { ReactNode } from "react";
import { SITE_CONFIG } from "@/data/site/siteConfig";
import { Subtitles } from "@/components/ui/Subtitles";
interface SiteLayoutProps {
children: ReactNode;
}
export function SiteLayout({ children }: SiteLayoutProps): React.JSX.Element {
return (
<div
style={{
position: "fixed",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#87CEEB",
backgroundImage: `url(${SITE_CONFIG.backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#fff",
overflow: "hidden",
}}
>
{children}
<Subtitles />
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { SITE_CONFIG } from "@/data/site/siteConfig";
const MOBILE_TEXT =
"Ce site a été conçu pour être utilisé sur ordinateur. Veuillez réessayer sur votre ordinateur pour une expérience optimale.";
/**
* Mobile blocker screen
*/
export function SiteMobileBlocker(): React.JSX.Element {
return (
<div
style={{
position: "fixed",
inset: 0,
backgroundColor: "#87CEEB",
backgroundImage: `url(${SITE_CONFIG.backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: 32,
gap: 48,
}}
>
<img
src="public/assets/logo/logo.jpg"
alt="Logo"
style={{
width: 120,
height: "auto",
}}
/>
<p
style={{
color: "#F2F2F2",
textAlign: "center",
textShadow: "0 4px 10px rgba(0, 0, 0, 0.4)",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 18,
fontWeight: 500,
lineHeight: 1.6,
maxWidth: 320,
margin: 0,
}}
>
{MOBILE_TEXT}
</p>
</div>
);
}
+133
View File
@@ -0,0 +1,133 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { SiteButton } from "@/components/site/SiteButton";
import { SITE_CONFIG } from "@/data/site/siteConfig";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue";
/**
* Screen 3: Name input
*/
export function SiteNamingScreen(): React.JSX.Element {
const setStep = useSiteStore((state) => state.setStep);
const setPlayerName = useGameStore((state) => state.setPlayerName);
const [charIndex, setCharIndex] = useState(0);
const dialogueStarted = useRef(false);
const inputRef = useRef<HTMLInputElement>(null);
const forcedName = SITE_CONFIG.forcedName;
const displayValue = forcedName.slice(0, charIndex);
const isComplete = charIndex >= forcedName.length;
// Play dialogue when screen appears (with subtitles)
useEffect(() => {
if (dialogueStarted.current) return;
dialogueStarted.current = true;
void (async () => {
const manifest = await loadDialogueManifest();
if (manifest) {
await playDialogueById(manifest, "narrateur_intro_prenom");
}
})();
}, []);
// Focus input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>): void => {
e.preventDefault();
// Only process if not complete and it's a letter key
if (!isComplete && e.key.length === 1 && /[a-zA-Z]/.test(e.key)) {
setCharIndex((prev) => Math.min(prev + 1, forcedName.length));
}
},
[isComplete, forcedName.length],
);
const handleConfirm = (): void => {
if (isComplete) {
setPlayerName(forcedName);
setStep("transition");
}
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 80,
padding: 24,
width: "100%",
maxWidth: 950,
}}
>
<div
style={{
display: "flex",
width: "100%",
flexDirection: "column",
alignItems: "center",
gap: 48,
}}
>
<h2
style={{
color: "#F2F2F2",
textAlign: "center",
textShadow: "0 7px 14.4px rgba(0, 0, 0, 0.25)",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(18px, 3vw, 26px)",
fontStyle: "normal",
fontWeight: 700,
lineHeight: "normal",
letterSpacing: "-1.3px",
margin: 0,
}}
>
Quel est votre prénom ?
</h2>
<input
ref={inputRef}
type="text"
value={displayValue}
onKeyDown={handleKeyDown}
readOnly
placeholder="Écrivez votre prénom ici"
style={{
display: "flex",
padding: "clamp(8px, 1.5vw, 10px)",
alignItems: "center",
width: "100%",
maxWidth: 800,
minWidth: 280,
gap: 10,
border: "4px solid #FFF",
background: "#D9D9D9",
outline: "none",
color: "#333",
caretColor: "transparent",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(16px, 2.5vw, 20px)",
textAlign: "left",
boxSizing: "border-box",
}}
/>
</div>
<SiteButton
label="CONFIRMER"
disabled={!isComplete}
onClick={handleConfirm}
/>
</div>
);
}
@@ -0,0 +1,82 @@
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { SiteCard } from "@/components/site/SiteCard";
import { SiteButton } from "@/components/site/SiteButton";
import { SITUATION_CARDS } from "@/data/site/siteConfig";
/**
* Screen 2: Situation selection
*/
export function SiteSituationScreen(): React.JSX.Element {
const selectedSituation = useSiteStore((state) => state.selectedSituation);
const setSelectedSituation = useSiteStore(
(state) => state.setSelectedSituation,
);
const setStep = useSiteStore((state) => state.setStep);
const canProceed = selectedSituation !== null;
const handleConfirm = (): void => {
if (canProceed) {
setStep("naming");
}
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 40,
padding: 24,
width: "100%",
maxWidth: 1208,
}}
>
<h2
style={{
color: "#F2F2F2",
textAlign: "center",
textShadow: "0 7px 14.4px rgba(0, 0, 0, 0.25)",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(20px, 4vw, 32px)",
fontStyle: "normal",
fontWeight: 700,
lineHeight: "normal",
letterSpacing: "-1.6px",
margin: 0,
}}
>
Quelle est votre situation ?
</h2>
<div
style={{
display: "flex",
gap: 16,
flexWrap: "wrap",
justifyContent: "center",
}}
>
{SITUATION_CARDS.map((card, index) => (
<SiteCard
key={card.id}
config={card}
selected={selectedSituation === index}
onSelect={() => {
if (!card.disabled) {
setSelectedSituation(index);
}
}}
/>
))}
</div>
<SiteButton
label="CONFIRMER"
disabled={!canProceed}
onClick={handleConfirm}
/>
</div>
);
}
@@ -0,0 +1,109 @@
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { Subtitles } from "@/components/ui/Subtitles";
import { setSiteVisited } from "@/utils/cookies/siteVisitCookie";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue";
const FADE_DURATION_MS = 1000;
/**
* Transition overlay: black screen (fade in) + logo (fade in/out) + dialogue with subtitles + redirect to /
*/
export function SiteTransitionOverlay(): React.JSX.Element {
const navigate = useNavigate();
const reset = useSiteStore((state) => state.reset);
const [screenOpacity, setScreenOpacity] = useState(0);
const [logoOpacity, setLogoOpacity] = useState(0);
const transitionStarted = useRef(false);
useEffect(() => {
if (transitionStarted.current) return;
transitionStarted.current = true;
// Fade in black screen
setScreenOpacity(1);
// Set cookie
setSiteVisited();
// Fade in logo after the black screen transition delay.
setLogoOpacity(1);
// Play transition dialogue (with subtitles) then fade out logo and redirect
void (async () => {
const manifest = await loadDialogueManifest();
if (manifest) {
const dialogueAudio = await playDialogueById(
manifest,
"narrateur_intro_apresprenom",
);
if (dialogueAudio) {
dialogueAudio.addEventListener(
"ended",
() => {
// Fade out logo
setLogoOpacity(0);
// Redirect after logo fade out
setTimeout(() => {
reset();
navigate({ to: "/" });
}, FADE_DURATION_MS);
},
{ once: true },
);
return;
}
}
// Fallback: redirect after 3s if dialogue fails
setTimeout(() => {
setLogoOpacity(0);
setTimeout(() => {
reset();
navigate({ to: "/" });
}, FADE_DURATION_MS);
}, 3000);
})();
}, [navigate, reset]);
return (
<div
style={{
position: "fixed",
inset: 0,
zIndex: 1000,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: "#000",
zIndex: 0,
opacity: screenOpacity,
transition: `opacity ${FADE_DURATION_MS}ms ease-in-out`,
}}
/>
<img
src="/assets/logo/logo.jpg"
alt="Logo"
style={{
position: "relative",
zIndex: 1,
width: "min(300px, 45vw)",
height: "auto",
objectFit: "contain",
opacity: logoOpacity,
transition: `opacity ${FADE_DURATION_MS}ms ease-in-out`,
transitionDelay: logoOpacity === 1 ? `${FADE_DURATION_MS}ms` : "0ms",
}}
/>
<Subtitles />
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { SiteCard } from "@/components/site/SiteCard";
import { SiteButton } from "@/components/site/SiteButton";
import { EXPERIENCE_CARDS } from "@/data/site/siteConfig";
/**
* Screen 1: Welcome
*/
export function SiteWelcomeScreen(): React.JSX.Element {
const selectedExperience = useSiteStore((state) => state.selectedExperience);
const setSelectedExperience = useSiteStore(
(state) => state.setSelectedExperience,
);
const setStep = useSiteStore((state) => state.setStep);
const canProceed = selectedExperience !== null;
const handleNext = (): void => {
if (canProceed) {
setStep("situation");
}
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 40,
padding: 24,
width: "100%",
maxWidth: 1208,
}}
>
<div
style={{
display: "flex",
width: "100%",
flexDirection: "column",
alignItems: "center",
}}
>
<h1
style={{
color: "#F2F2F2",
textShadow: "0 7px 14.4px rgba(0, 0, 0, 0.25)",
fontFamily: '"Nersans One", system-ui, sans-serif',
fontSize: "clamp(40px, 8vw, 64px)",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "normal",
letterSpacing: "-3px",
margin: 0,
textAlign: "center",
}}
>
BIENVENUE A ALTERA
</h1>
<p
style={{
color: "#F2F2F2",
textAlign: "center",
textShadow: "0 7px 14.4px rgba(0, 0, 0, 0.25)",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(18px, 3vw, 26px)",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "normal",
letterSpacing: "-1.3px",
margin: 0,
}}
>
Communauté convivialiste
</p>
</div>
<h2
style={{
color: "#F2F2F2",
textAlign: "center",
textShadow: "0 7px 14.4px rgba(0, 0, 0, 0.25)",
fontFamily: "Inter, system-ui, sans-serif",
fontSize: "clamp(20px, 4vw, 32px)",
fontStyle: "normal",
fontWeight: 700,
lineHeight: "normal",
letterSpacing: "-1.6px",
margin: 0,
}}
>
Choisissez une expérience :
</h2>
<div
style={{
display: "flex",
gap: 16,
flexWrap: "wrap",
justifyContent: "center",
}}
>
{EXPERIENCE_CARDS.map((card, index) => (
<SiteCard
key={card.id}
config={card}
selected={selectedExperience === index}
onSelect={() => {
if (!card.disabled) {
setSelectedExperience(index);
}
}}
/>
))}
</div>
<SiteButton label="SUIVANT" disabled={!canProceed} onClick={handleNext} />
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
export const SITE_CONFIG = {
backgroundImage: "/assets/bg-site.png",
forcedName: "Danyl",
} as const;
export interface SiteCardConfig {
id: string;
label: string;
imagePath?: string;
disabled: boolean;
}
/**
* Cards for screen 1: "Choisissez une expérience"
*/
export const EXPERIENCE_CARDS: readonly SiteCardConfig[] = [
{ id: "exp-fabrik", label: "La Fabrik", disabled: false },
{ id: "exp-ferme", label: "La Ferme verticale", disabled: true },
{ id: "exp-energie", label: "La Zone d'énergie", disabled: true },
{ id: "exp-ecole", label: "L'École", disabled: true },
];
/**
* Cards for screen 2: "Quelle est votre situation ?"
*/
export const SITUATION_CARDS: readonly SiteCardConfig[] = [
{ id: "sit-habitants", label: "Habitants d'Altera", disabled: true },
{ id: "sit-apprentis", label: "Apprentis-Citoyens", disabled: true },
{
id: "sit-refugies",
label: "Réfugiés Climatiques arrivants",
disabled: false,
},
{ id: "sit-seniors", label: "Seniors Hyper-Connectés", disabled: true },
];
+31
View File
@@ -0,0 +1,31 @@
import { create } from "zustand";
import type { SiteStep } from "@/types/game";
interface SiteState {
currentStep: SiteStep;
selectedExperience: number | null;
selectedSituation: number | null;
}
interface SiteActions {
setStep: (step: SiteStep) => void;
setSelectedExperience: (index: number) => void;
setSelectedSituation: (index: number) => void;
reset: () => void;
}
type SiteStore = SiteState & SiteActions;
const initialState: SiteState = {
currentStep: "disclaimer",
selectedExperience: null,
selectedSituation: null,
};
export const useSiteStore = create<SiteStore>()((set) => ({
...initialState,
setStep: (step) => set({ currentStep: step }),
setSelectedExperience: (index) => set({ selectedExperience: index }),
setSelectedSituation: (index) => set({ selectedSituation: index }),
reset: () => set(initialState),
}));
+64
View File
@@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { SiteDisclaimerScreen } from "@/components/site/SiteDisclaimerScreen";
import { SiteWelcomeScreen } from "@/components/site/SiteWelcomeScreen";
import { SiteSituationScreen } from "@/components/site/SiteSituationScreen";
import { SiteNamingScreen } from "@/components/site/SiteNamingScreen";
import { SiteTransitionOverlay } from "@/components/site/SiteTransitionOverlay";
import { SiteMobileBlocker } from "@/components/site/SiteMobileBlocker";
import { SiteLayout } from "@/components/site/SiteLayout";
/**
* Check if user is on mobile device
*/
function useIsMobile(): boolean {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = (): void => {
const userAgent = navigator.userAgent.toLowerCase();
const mobileKeywords = [
"android",
"webos",
"iphone",
"ipad",
"ipod",
"blackberry",
"windows phone",
];
const isMobileDevice = mobileKeywords.some((keyword) =>
userAgent.includes(keyword),
);
const isSmallScreen = window.innerWidth < 768;
setIsMobile(isMobileDevice || isSmallScreen);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
return isMobile;
}
export function SitePage(): React.JSX.Element {
const currentStep = useSiteStore((state) => state.currentStep);
const isMobile = useIsMobile();
if (isMobile) {
return <SiteMobileBlocker />;
}
if (currentStep === "disclaimer") {
return <SiteDisclaimerScreen />;
}
return (
<SiteLayout>
{currentStep === "welcome" && <SiteWelcomeScreen />}
{currentStep === "situation" && <SiteSituationScreen />}
{currentStep === "naming" && <SiteNamingScreen />}
{currentStep === "transition" && <SiteTransitionOverlay />}
</SiteLayout>
);
}
+8
View File
@@ -5,6 +5,7 @@ import {
createRouter, createRouter,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { HomePage } from "@/pages/page"; import { HomePage } from "@/pages/page";
import { SitePage } from "@/pages/site/page";
import { EditorPage } from "@/pages/editor/page"; import { EditorPage } from "@/pages/editor/page";
import { GalleryPage } from "@/pages/gallery/page"; import { GalleryPage } from "@/pages/gallery/page";
import { WaypointEditorPage } from "@/pages/waypoint/page"; import { WaypointEditorPage } from "@/pages/waypoint/page";
@@ -42,6 +43,12 @@ const indexRoute = createRoute({
component: HomePage, component: HomePage,
}); });
const siteRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/site",
component: SitePage,
});
const editorRoute = createRoute({ const editorRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: "/editor", path: "/editor",
@@ -102,6 +109,7 @@ const docsChildRoutes = [
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
indexRoute, indexRoute,
siteRoute,
editorRoute, editorRoute,
galleryRoute, galleryRoute,
waypointRoute, waypointRoute,
+1
View File
@@ -4,6 +4,7 @@ import type { RepairMissionId } from "@/types/gameplay/repairMission";
* Steps for the /site onboarding page * Steps for the /site onboarding page
*/ */
export type SiteStep = export type SiteStep =
| "disclaimer" // Écran 0: Avertissement (ordi recommandé, bonne connexion)
| "welcome" // Écran 1: Bienvenue à Altera | "welcome" // Écran 1: Bienvenue à Altera
| "situation" // Écran 2: Quelle est votre situation | "situation" // Écran 2: Quelle est votre situation
| "naming" // Écran 3: Quel est votre prénom (Danyl) | "naming" // Écran 3: Quel est votre prénom (Danyl)
+35
View File
@@ -0,0 +1,35 @@
const COOKIE_NAME = "siteVisited";
const EXPIRY_HOURS = 24;
/**
* Check if the site has been visited today (within 24 hours)
*/
export function hasSiteBeenVisitedToday(): boolean {
const cookies = document.cookie.split(";");
for (const cookie of cookies) {
const [name, value] = cookie.trim().split("=");
if (name === COOKIE_NAME && value === "true") {
return true;
}
}
return false;
}
/**
* Set the site visited cookie with 24-hour expiration
*/
export function setSiteVisited(): void {
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + EXPIRY_HOURS * 60 * 60 * 1000);
document.cookie = `${COOKIE_NAME}=true; expires=${expiryDate.toUTCString()}; path=/; SameSite=Strict`;
}
/**
* Clear the site visited cookie (useful for debugging)
*/
export function clearSiteVisited(): void {
document.cookie = `${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}