Feat/polish-intro #11

Merged
math-pixel merged 20 commits from feat/polisth-intro into develop 2026-05-31 09:01:18 +00:00
10 changed files with 116 additions and 72 deletions
Showing only changes of commit 0f6860f1ae - Show all commits
+2 -6
View File
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { SITE_CONFIG } from "@/data/site/siteConfig";
import { SITE_BACKGROUND_STYLE } from "@/data/site/siteConfig";
import { Subtitles } from "@/components/ui/Subtitles";
interface SiteLayoutProps {
@@ -16,11 +16,7 @@ export function SiteLayout({ children }: SiteLayoutProps): React.JSX.Element {
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#87CEEB",
backgroundImage: `url(${SITE_CONFIG.backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
...SITE_BACKGROUND_STYLE,
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#fff",
overflow: "hidden",
+8 -6
View File
@@ -7,13 +7,15 @@ 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 selectedSituationIndex = useSiteStore(
(state) => state.selectedSituationIndex,
);
const setSelectedSituationIndex = useSiteStore(
(state) => state.setSelectedSituationIndex,
);
const setStep = useSiteStore((state) => state.setStep);
const canProceed = selectedSituation !== null;
const canProceed = selectedSituationIndex !== null;
const handleConfirm = (): void => {
if (canProceed) {
@@ -63,11 +65,11 @@ export function SiteSituationScreen(): React.JSX.Element {
<SiteCard
key={card.id}
config={card}
selected={selectedSituation === index}
selected={selectedSituationIndex === index}
variant="situation"
onSelect={() => {
if (!card.disabled) {
setSelectedSituation(index);
setSelectedSituationIndex(index);
}
}}
/>
+8 -6
View File
@@ -7,13 +7,15 @@ 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 selectedExperienceIndex = useSiteStore(
(state) => state.selectedExperienceIndex,
);
const setSelectedExperienceIndex = useSiteStore(
(state) => state.setSelectedExperienceIndex,
);
const setStep = useSiteStore((state) => state.setStep);
const canProceed = selectedExperience !== null;
const canProceed = selectedExperienceIndex !== null;
const handleNext = (): void => {
if (canProceed) {
@@ -104,10 +106,10 @@ export function SiteWelcomeScreen(): React.JSX.Element {
<SiteCard
key={card.id}
config={card}
selected={selectedExperience === index}
selected={selectedExperienceIndex === index}
onSelect={() => {
if (!card.disabled) {
setSelectedExperience(index);
setSelectedExperienceIndex(index);
}
}}
/>
-8
View File
@@ -1,11 +1,3 @@
export const AUDIO_PATHS = {
intro: "/sounds/effect/fa.mp3",
bienvenue: "/sounds/effect/fa.mp3",
alertCentral: "/sounds/effect/fa.mp3",
searching: "/sounds/effect/fa.mp3",
helped: "/sounds/effect/fa.mp3",
} as const;
export type AudioCategory = "music" | "sfx" | "dialogue";
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
+9
View File
@@ -0,0 +1,9 @@
/**
* Dialogue manifest IDs used by the /site flow and the intro sequence.
* Defined once here so components don't hold magic strings.
*/
export const SITE_DIALOGUE_IDS = {
naming: "narrateur_intro_prenom",
transition: "narrateur_intro_apresprenom",
introOrder: "narrateur_ordreebike",
} as const;
+19 -4
View File
@@ -1,8 +1,23 @@
import type { CSSProperties } from "react";
const BACKGROUND_IMAGE = "/assets/bg-site.png";
export const SITE_CONFIG = {
backgroundImage: "/assets/bg-site.png",
forcedName: "Danyl",
backgroundImage: BACKGROUND_IMAGE,
presetPlayerName: "Danyl",
} as const;
/**
* Shared background style used by SiteLayout and SiteMobileBlocker.
*/
export const SITE_BACKGROUND_STYLE: CSSProperties = {
backgroundColor: "#87CEEB",
backgroundImage: `url(${BACKGROUND_IMAGE})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
};
export interface SiteCardConfig {
id: string;
label: string;
@@ -24,10 +39,10 @@ export const EXPERIENCE_CARDS: readonly SiteCardConfig[] = [
* Cards for screen 2: "Quelle est votre situation ?"
*/
export const SITUATION_CARDS: readonly SiteCardConfig[] = [
{ id: "sit-refugie-climat", label: "Sans domicile fixe", disabled: true },
{ id: "sit-sans-domicile", label: "Sans domicile fixe", disabled: true },
{ id: "sit-refugie-guerre", label: "Réfugié.e de guerre", disabled: true },
{
id: "sit-sans-domicile",
id: "sit-refugie-climat",
label: "Réfugié.e climatique",
disabled: false,
},
+31
View File
@@ -0,0 +1,31 @@
import { useSyncExternalStore } from "react";
const MOBILE_MEDIA_QUERY =
"(max-width: 767px), (pointer: coarse) and (hover: none)";
function subscribeToMobileQuery(callback: () => void): () => void {
const query = window.matchMedia(MOBILE_MEDIA_QUERY);
query.addEventListener("change", callback);
return () => query.removeEventListener("change", callback);
}
function getMobileSnapshot(): boolean {
return window.matchMedia(MOBILE_MEDIA_QUERY).matches;
}
function getServerMobileSnapshot(): boolean {
return false;
}
/**
* True when the device is a phone or a touch-only tablet.
* Uses matchMedia so layout decisions follow CSS conventions
* and avoid resize-handler churn.
*/
export function useIsMobile(): boolean {
return useSyncExternalStore(
subscribeToMobileQuery,
getMobileSnapshot,
getServerMobileSnapshot,
);
}
+29
View File
@@ -0,0 +1,29 @@
import { useSyncExternalStore } from "react";
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
function subscribeToReducedMotion(callback: () => void): () => void {
const query = window.matchMedia(REDUCED_MOTION_QUERY);
query.addEventListener("change", callback);
return () => query.removeEventListener("change", callback);
}
function getReducedMotionSnapshot(): boolean {
return window.matchMedia(REDUCED_MOTION_QUERY).matches;
}
function getServerReducedMotionSnapshot(): boolean {
return false;
}
/**
* True when the user has requested reduced motion at the OS level.
* UI fades and transitions should collapse to 0ms when this is true.
*/
export function usePrefersReducedMotion(): boolean {
return useSyncExternalStore(
subscribeToReducedMotion,
getReducedMotionSnapshot,
getServerReducedMotionSnapshot,
);
}
+9 -8
View File
@@ -3,14 +3,14 @@ import type { SiteStep } from "@/types/game";
interface SiteState {
currentStep: SiteStep;
selectedExperience: number | null;
selectedSituation: number | null;
selectedExperienceIndex: number | null;
selectedSituationIndex: number | null;
}
interface SiteActions {
setStep: (step: SiteStep) => void;
setSelectedExperience: (index: number) => void;
setSelectedSituation: (index: number) => void;
setSelectedExperienceIndex: (index: number) => void;
setSelectedSituationIndex: (index: number) => void;
reset: () => void;
}
@@ -18,14 +18,15 @@ type SiteStore = SiteState & SiteActions;
const initialState: SiteState = {
currentStep: "disclaimer",
selectedExperience: null,
selectedSituation: null,
selectedExperienceIndex: null,
selectedSituationIndex: null,
};
export const useSiteStore = create<SiteStore>()((set) => ({
...initialState,
setStep: (step) => set({ currentStep: step }),
setSelectedExperience: (index) => set({ selectedExperience: index }),
setSelectedSituation: (index) => set({ selectedSituation: index }),
setSelectedExperienceIndex: (index) =>
set({ selectedExperienceIndex: index }),
setSelectedSituationIndex: (index) => set({ selectedSituationIndex: index }),
reset: () => set(initialState),
}));
+1 -34
View File
@@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import { useSiteStore } from "@/managers/stores/useSiteStore";
import { SiteDisclaimerScreen } from "@/components/site/SiteDisclaimerScreen";
import { SiteWelcomeScreen } from "@/components/site/SiteWelcomeScreen";
@@ -7,39 +6,7 @@ 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;
}
import { useIsMobile } from "@/hooks/ui/useIsMobile";
export function SitePage(): React.JSX.Element {
const currentStep = useSiteStore((state) => state.currentStep);