diff --git a/src/App.tsx b/src/App.tsx index db41be3..42fd082 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -10,7 +9,6 @@ const queryClient = new QueryClient(); const App = () => ( - diff --git a/src/components/MonthTransition.tsx b/src/components/MonthTransition.tsx index a1fcdd5..d4c8f93 100644 --- a/src/components/MonthTransition.tsx +++ b/src/components/MonthTransition.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; import { Card, CardContent } from "@/components/ui/card"; import { useTranslation } from "react-i18next"; +import { getMonthConfig } from '@/utils/months'; export enum TransitionStyle { FADE = "fade", @@ -11,21 +12,20 @@ export enum TransitionStyle { } interface MonthTransitionProps { - monthIndex: number; + stage: number; onComplete: () => void; - style: TransitionStyle; + style?: TransitionStyle; } -// Helper function to translate month name -const useTranslatedMonth = (monthIndex: number) => { +const useTranslatedMonth = (stage: number) => { const { t } = useTranslation(); - const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'exposé']; - return t(`months.${monthKeys[monthIndex]}`); + const monthConfig = getMonthConfig(stage); + return monthConfig ? t(monthConfig.translationKey) : ''; }; // Create separate components for each style -const FadeTransition = ({ monthIndex }: { monthIndex: number }) => { - const translatedMonth = useTranslatedMonth(monthIndex); +const FadeTransition = ({ stage }: { stage: number }) => { + const translatedMonth = useTranslatedMonth(stage); return ( @@ -37,8 +37,8 @@ const FadeTransition = ({ monthIndex }: { monthIndex: number }) => { ); }; -const TypewriterTransition = ({ monthIndex }: { monthIndex: number }) => { - const translatedMonth = useTranslatedMonth(monthIndex); +const TypewriterTransition = ({ stage }: { stage: number }) => { + const translatedMonth = useTranslatedMonth(stage); return (
@@ -48,8 +48,8 @@ const TypewriterTransition = ({ monthIndex }: { monthIndex: number }) => { ); }; -const SplitScreenTransition = ({ monthIndex }: { monthIndex: number }) => { - const translatedMonth = useTranslatedMonth(monthIndex); +const SplitScreenTransition = ({ stage }: { stage: number }) => { + const translatedMonth = useTranslatedMonth(stage); return ( <>
@@ -63,8 +63,8 @@ const SplitScreenTransition = ({ monthIndex }: { monthIndex: number }) => { ); }; -const MatrixTransition = ({ monthIndex }: { monthIndex: number }) => { - const translatedMonth = useTranslatedMonth(monthIndex); +const MatrixTransition = ({ stage }: { stage: number }) => { + const translatedMonth = useTranslatedMonth(stage); return ( <>
@@ -90,8 +90,8 @@ const MatrixTransition = ({ monthIndex }: { monthIndex: number }) => { ); }; -const NumberCycleTransition = ({ monthIndex }: { monthIndex: number }) => { - const translatedMonth = useTranslatedMonth(monthIndex); +const NumberCycleTransition = ({ stage }: { stage: number }) => { + const translatedMonth = useTranslatedMonth(stage); const [displayText, setDisplayText] = useState( Array(translatedMonth.length).fill('0').join('') ); @@ -152,7 +152,7 @@ const NumberCycleTransition = ({ monthIndex }: { monthIndex: number }) => { ); }; -export const MonthTransition = ({ monthIndex, onComplete, style }: MonthTransitionProps) => { +export const MonthTransition = ({ stage, onComplete, style }: MonthTransitionProps) => { useEffect(() => { const timer = setTimeout(onComplete, 3500); return () => clearTimeout(timer); @@ -161,17 +161,17 @@ export const MonthTransition = ({ monthIndex, onComplete, style }: MonthTransiti const renderTransition = () => { switch (style) { case TransitionStyle.FADE: - return ; + return ; case TransitionStyle.TYPEWRITER: - return ; + return ; case TransitionStyle.SPLIT_SCREEN: - return ; + return ; case TransitionStyle.MATRIX: - return ; + return ; case TransitionStyle.NUMBER_CYCLE: - return ; + return ; default: - return ; + return ; } }; diff --git a/src/components/game/BriefingAudio.tsx b/src/components/game/BriefingAudio.tsx index 3ab6564..cef73d7 100644 --- a/src/components/game/BriefingAudio.tsx +++ b/src/components/game/BriefingAudio.tsx @@ -3,7 +3,8 @@ import { Button } from "@/components/ui/button"; import { PlayIcon, PauseIcon } from "@heroicons/react/24/outline"; import { useTranslation } from "react-i18next"; import { playBriefing } from "@/utils/audio"; -import { toast } from "@/components/ui/use-toast"; +import { toast } from "sonner"; +import { getMonthConfig } from "@/utils/months"; interface BriefingAudioProps { stage: string; @@ -18,17 +19,22 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio const getAudioFileName = (stage: string) => { const currentLanguage = i18n.language; - const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'expose']; + + console.log('BriefingAudio - Stage received:', stage); // Handle special stages if (stage === "INTRO") { return `intro-${currentLanguage}.mp3`; } - // For all other stages (including ALERT), use the month-based naming - const monthIndex = parseInt(stage); - const monthKey = monthKeys[monthIndex]; - return `${monthKey}-${currentLanguage}.mp3`; + const monthConfig = getMonthConfig(stage); + console.log('BriefingAudio - Selected monthConfig:', monthConfig); + + if (!monthConfig?.audio?.briefing) { + throw new Error(`No audio briefing configured for stage ${stage}`); + } + + return monthConfig.audio.briefing; }; const handlePlayPause = async () => { @@ -56,10 +62,8 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio setIsPlaying(true); } catch (error) { console.error('Audio error:', error); - toast({ - title: "Audio Error", - description: `Failed to play briefing: ${error instanceof Error ? error.message : 'Unknown error'}`, - variant: "destructive", + toast.error("Audio Error", { + description: `Failed to play briefing: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }; diff --git a/src/components/game/DossierPanel.tsx b/src/components/game/DossierPanel.tsx index dd40248..de99cda 100644 --- a/src/components/game/DossierPanel.tsx +++ b/src/components/game/DossierPanel.tsx @@ -81,10 +81,20 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: index * 0.1 }} - className="space-y-4 relative bg-gray-800/30 p-4 sm:p-6 rounded-md border border-gray-700" + className={cn( + "space-y-4 relative bg-gray-800/30 p-4 sm:p-6 rounded-md border", + entry.dateKey.toLowerCase().includes('alert') || entry.dateKey.toLowerCase().includes('expose') + ? "border-red-500/50" + : "border-gray-700" + )} >
-

+

{t(entry.dateKey)} diff --git a/src/components/game/ExpertMemo.tsx b/src/components/game/ExpertMemo.tsx index e11d8ae..7492568 100644 --- a/src/components/game/ExpertMemo.tsx +++ b/src/components/game/ExpertMemo.tsx @@ -51,7 +51,8 @@ export const ExpertMemo: React.FC = ({ from, subject, children,

{paragraph}

)); } - return content; + // If it's already a React node (like a div), return it as is + return
{content}
; }; return ( diff --git a/src/components/game/IntroAudio.tsx b/src/components/game/IntroAudio.tsx index bca50c0..93a4066 100644 --- a/src/components/game/IntroAudio.tsx +++ b/src/components/game/IntroAudio.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { Button } from "@/components/ui/button"; import { Play, Pause } from "lucide-react"; -import { useToast } from "@/components/ui/use-toast"; +import { toast } from "sonner"; import { useTranslation } from 'react-i18next'; import { playRecordingSound } from "@/utils/audio"; @@ -12,7 +12,6 @@ interface IntroAudioProps { export const IntroAudio = ({ className }: IntroAudioProps) => { const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef(null); - const { toast } = useToast(); const { t, i18n } = useTranslation(); useEffect(() => { @@ -46,10 +45,8 @@ export const IntroAudio = ({ className }: IntroAudioProps) => { if (playPromise !== undefined) { playPromise.catch(error => { console.error('Playback failed:', error); - toast({ - title: "Playback Error", - description: "Unable to play audio briefing", - variant: "destructive" + toast.error("Playback Error", { + description: "Unable to play audio briefing" }); }); } @@ -57,10 +54,8 @@ export const IntroAudio = ({ className }: IntroAudioProps) => { setIsPlaying(!isPlaying); } catch (error) { console.error('Audio error:', error); - toast({ - title: "Audio Error", - description: "Audio briefing unavailable", - variant: "destructive" + toast.error("Audio Error", { + description: "Audio briefing unavailable" }); } }; diff --git a/src/components/game/IntroDialog.tsx b/src/components/game/IntroDialog.tsx index c11f113..8ca489c 100644 --- a/src/components/game/IntroDialog.tsx +++ b/src/components/game/IntroDialog.tsx @@ -5,9 +5,10 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { useState } from "react"; +import { useState, useRef, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; +import { cn } from "@/lib/utils"; interface IntroDialogProps { onStartAudio?: () => void; @@ -16,6 +17,31 @@ interface IntroDialogProps { export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => { const [open, setOpen] = useState(true); const { t } = useTranslation(); + const [showGradient, setShowGradient] = useState(false); + const contentRef = useRef(null); + + const checkScroll = useCallback(() => { + const element = contentRef.current; + if (element) { + const hasOverflow = element.scrollHeight > element.clientHeight; + const isAtBottom = Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) < 1; + setShowGradient(hasOverflow && !isAtBottom); + } + }, []); + + useEffect(() => { + const element = contentRef.current; + if (element) { + checkScroll(); + element.addEventListener('scroll', checkScroll); + window.addEventListener('resize', checkScroll); + + return () => { + element.removeEventListener('scroll', checkScroll); + window.removeEventListener('resize', checkScroll); + }; + } + }, [checkScroll]); const handleBeginSimulation = () => { setOpen(false); @@ -23,57 +49,62 @@ export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => { }; return ( - + -
-
- - - {t('intro.title')} - - -
-
-
🎯
-

- {t('intro.mission')} -

-
- -

- {t('intro.explanation')} -

- -

- {t('intro.howToPlay.description')} -

- -

- {t('intro.reminder')} -

-
-
- -
-
- - - {t('languageSwitcher.hint')} - -
- - + + + {t('intro.title')} + + +
+
+
🎯
+

+ {t('intro.mission')} +

+ +

+ {t('intro.explanation')} +

+ +

+ {t('intro.howToPlay.description')} +

+ +

+ {t('intro.reminder')} +

+
+ +
+
+ + + {t('languageSwitcher.hint')} + +
+ +
+ +
); }; \ No newline at end of file diff --git a/src/components/game/constants/expertAudio.ts b/src/components/game/constants/expertAudio.ts deleted file mode 100644 index 3a1365d..0000000 --- a/src/components/game/constants/expertAudio.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const EXPERT_AUDIO = { - january: { - briefing: "/audio/dr-chen-january.mp3", - voice: "Dr. Chen" - }, - february: { - briefing: "/audio/dr-webb-february.mp3", - voice: "Dr. Webb" - }, - march: { - briefing: "/audio/prof-morrison-march.mp3", - voice: "Professor Morrison" - }, - april: { - briefing: "/audio/agent-torres-april.mp3", - voice: "Agent Torres" - } -}; \ No newline at end of file diff --git a/src/components/game/constants/gameStages.tsx b/src/components/game/constants/gameStages.tsx index bc1bc94..d8a48f6 100644 --- a/src/components/game/constants/gameStages.tsx +++ b/src/components/game/constants/gameStages.tsx @@ -4,27 +4,13 @@ import { ExpertMemo } from '../ExpertMemo'; import { useTranslation } from 'react-i18next'; import { ChoiceID } from './metrics'; -// Define month indices as constants -export const MONTHS = { - JANUARY: 0, - MARCH: 1, - MAY: 2, - JULY: 3, - SEPTEMBER: 4, - NOVEMBER: 5, - DECEMBER: 6, - ALERT: 7, - EXPOSÉ: 8, -} as const; - // Create a custom hook to handle stages with translations export const useGameStages = (audioRef: React.RefObject): GameStage[] => { const { t } = useTranslation(); // Helper function to get translated month title - const getMonthTitle = (monthIndex: number) => { - const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'exposé']; - return t(`months.${monthKeys[monthIndex]}`); + const getMonthTitle = (stage: number) => { + return t(`months.${stage}`); }; // Helper function to get translated choice option @@ -35,8 +21,8 @@ export const useGameStages = (audioRef: React.RefObject): Game return [ { id: 1, - monthIndex: MONTHS.JANUARY, - title: getMonthTitle(MONTHS.JANUARY), + monthIndex: 1, // January + title: getMonthTitle(1), description: ): Game }, { id: 2, - monthIndex: MONTHS.MARCH, - title: getMonthTitle(MONTHS.MARCH), + monthIndex: 2, // March + title: getMonthTitle(2), description: ): Game }, { id: 3, - monthIndex: MONTHS.MAY, - title: getMonthTitle(MONTHS.MAY), + monthIndex: 3, // May + title: getMonthTitle(3), description: ): Game }, { id: 4, - monthIndex: MONTHS.ALERT, - title: getMonthTitle(MONTHS.ALERT), + monthIndex: 4, // Alert + title: getMonthTitle(4), description: ): Game }, { id: 5, - monthIndex: MONTHS.JULY, - title: getMonthTitle(MONTHS.JULY), + monthIndex: 5, // July + title: getMonthTitle(5), description: ): Game }, { id: 6, - monthIndex: MONTHS.SEPTEMBER, - title: getMonthTitle(MONTHS.SEPTEMBER), + monthIndex: 6, // September + title: getMonthTitle(6), description: ): Game }, { id: 7, - monthIndex: MONTHS.NOVEMBER, - title: getMonthTitle(MONTHS.NOVEMBER), + monthIndex: 7, // November + title: getMonthTitle(7), description: ): Game }, { id: 8, - monthIndex: MONTHS.DECEMBER, - title: getMonthTitle(MONTHS.DECEMBER), + monthIndex: 8, // December + title: getMonthTitle(8), description: ): Game }, { id: 9, - monthIndex: MONTHS.EXPOSÉ, - title: getMonthTitle(MONTHS.EXPOSÉ), + monthIndex: 9, // Exposé + title: getMonthTitle(9), description: const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() + useEffect(() => { + const updateCountdown = () => { + const toasts = document.querySelectorAll('[data-sonner-toast]') + toasts.forEach(toast => { + const createdAt = Number(toast.getAttribute('data-created')) + const duration = 4000 // Match the duration from toastOptions + const now = Date.now() + const remaining = Math.max(0, Math.ceil((createdAt + duration - now) / 1000)) + toast.setAttribute('data-remaining', remaining.toString()) + }) + } + + const interval = setInterval(updateCountdown, 100) + return () => clearInterval(interval) + }, []) + return ( diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx deleted file mode 100644 index a822477..0000000 --- a/src/components/ui/toast.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx deleted file mode 100644 index 6c67edf..0000000 --- a/src/components/ui/toaster.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useToast } from "@/hooks/use-toast" -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" - -export function Toaster() { - const { toasts } = useToast() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ) - })} - -
- ) -} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts deleted file mode 100644 index b0aef21..0000000 --- a/src/components/ui/use-toast.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useToast, toast } from "@/hooks/use-toast"; - -export { useToast, toast }; diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts deleted file mode 100644 index 2c14125..0000000 --- a/src/hooks/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } diff --git a/src/index.css b/src/index.css index 5bd61be..c08a0e7 100644 --- a/src/index.css +++ b/src/index.css @@ -92,4 +92,38 @@ body { @apply bg-background text-foreground; } +} + +/* Toast Customization */ +[data-sonner-toaster] [data-sonner-toast] { + position: relative; +} + +[data-sonner-toaster] [data-sonner-toast]::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: rgb(234 179 8 / 0.3); + animation: toast-progress var(--duration, 4000ms) linear; +} + +[data-sonner-toaster] [data-sonner-toast]::before { + content: 'Auto-dismissing in ' attr(data-remaining) 's'; + position: absolute; + bottom: 4px; + right: 8px; + font-size: 0.7rem; + color: rgb(234 179 8 / 0.5); +} + +@keyframes toast-progress { + from { + transform: scaleX(1); + } + to { + transform: scaleX(0); + } } \ No newline at end of file diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 4ffdda4..55da393 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -10,7 +10,6 @@ import { IntroDialog } from "../components/game/IntroDialog"; import { useGameStages, OPERATION_NAMES, useLoadingMessages, generateFinalReport } from "@/components/game/constants"; import { ChoiceID, calculateMetrics } from "@/components/game/constants/metrics"; import { DossierEntry, GameStage } from "@/components/game/types"; -import { useToast } from "@/components/ui/use-toast"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; @@ -38,18 +37,11 @@ import { MetricsDisplay } from "@/components/game/MetricsDisplay"; import { MuteButton } from '@/components/MuteButton'; import { DevPanel } from "@/components/game/DevPanel"; import { motion } from "framer-motion"; +import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months"; +import { toast } from "sonner"; -const monthKeys = [ - 'january', // 0 - 'march', // 1 - 'may', // 2 - 'alert', // 3 - 'july', // 4 - 'september', // 5 - 'november', // 6 - 'december', // 7 - 'exposé' // 8 -]; +// Get valid month keys (skipping index 0) +const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[]; const STAGE_CHOICES = [ ['DEPLOY_BOTS', 'ESTABLISH_MEMES'], // January @@ -76,7 +68,6 @@ const Index = () => { const [showingResult, setShowingResult] = useState(false); const [currentResult, setCurrentResult] = useState(null); const [dossierEntries, setDossierEntries] = useState([]); - const { toast } = useToast(); const [showingMonthTransition, setShowingMonthTransition] = useState(false); const [nextStage, setNextStage] = useState(null); const [transitionStyle, setTransitionStyle] = useState(TransitionStyle.NUMBER_CYCLE); @@ -144,10 +135,7 @@ const Index = () => { const handleInitialTransitionComplete = () => { setShowingInitialTransition(false); setGameStarted(true); - toast({ - title: t('mission.welcome.title'), - description: t('mission.welcome.description'), - }); + toast(t('mission.welcome.description')); }; const handleChoice = async (choice: GameStage["choices"][0]) => { @@ -208,17 +196,7 @@ const Index = () => { setShowingResult(true); const newEntry: DossierEntry = { - dateKey: stages[currentStage].monthIndex === 0 ? 'months.january' : - stages[currentStage].monthIndex === 1 ? 'months.february' : - stages[currentStage].monthIndex === 2 ? 'months.march' : - stages[currentStage].monthIndex === 3 ? 'months.april' : - stages[currentStage].monthIndex === 4 ? 'months.may' : - stages[currentStage].monthIndex === 5 ? 'months.june' : - stages[currentStage].monthIndex === 6 ? 'months.july' : - stages[currentStage].monthIndex === 7 ? 'months.august' : - stages[currentStage].monthIndex === 8 ? 'months.september' : - stages[currentStage].monthIndex === 9 ? 'months.october' : - stages[currentStage].monthIndex === 10 ? 'months.november' : 'months.december', + dateKey: `months.${getMonthConfig(currentStage + 1)?.key}`, titleKey: `stages.${currentStage + 1}.choices.${choice.id}.result.title`, insightKeys: Array.from({ length: 4 }, (_, i) => `stages.${currentStage + 1}.choices.${choice.id}.result.insights.${i}`), strategicNoteKey: `stages.${currentStage + 1}.choices.${choice.id}.result.nextStepHint` @@ -296,7 +274,7 @@ const Index = () => {
@@ -521,7 +499,7 @@ const Index = () => {
@@ -542,7 +520,13 @@ const Index = () => {
- {t(`months.${monthKeys[currentStageData.monthIndex]}`)} + + {(() => { + console.log('Index - currentStageData:', currentStageData); + const monthConfig = getMonthConfig(currentStage + 1); + return t(`months.${monthConfig?.key}`); + })()} +
{currentStage > 0 && }
@@ -553,16 +537,19 @@ const Index = () => {
- {currentStageData.choices.map((choice, index) => ( - handleStrategyClick(choice)} - disabled={showingResult || isLoading} - optionNumber={index + 1} - /> - ))} + {(() => { + console.log('Index - Rendering stage:', currentStage); + return currentStageData.choices.map((choice, index) => ( + handleStrategyClick(choice)} + disabled={showingResult || isLoading} + optionNumber={index + 1} + /> + )); + })()}
diff --git a/src/utils/months.ts b/src/utils/months.ts new file mode 100644 index 0000000..19e8ec2 --- /dev/null +++ b/src/utils/months.ts @@ -0,0 +1,100 @@ +export interface ExpertAudio { + briefing: string; + voice: string; +} + +export interface AudioConfig { + briefing: string; + voice: string; +} + +export interface MonthConfig { + key: string; + // This key can be used with your localization i18n system. + translationKey: string; + audio?: AudioConfig; +} + +// Using a sparse array where index matches stage number +// Index 0 is empty, stages start at 1 +export const MONTHS_CONFIG: (MonthConfig | undefined)[] = []; + +// January is stage 1 +MONTHS_CONFIG[1] = { + key: "january", + translationKey: "months.january", + audio: { + briefing: "january-en.mp3", + voice: "Dr. Chen" + } +}; + +// March is stage 2 +MONTHS_CONFIG[2] = { + key: "march", + translationKey: "months.march", + audio: { + briefing: "march-en.mp3", + voice: "Professor Morrison" + } +}; + +// May is stage 3 +MONTHS_CONFIG[3] = { + key: "may", + translationKey: "months.may", + audio: { + briefing: "may-en.mp3", + voice: "Dr. Chen" + } +}; + +// Alert is stage 4 +MONTHS_CONFIG[4] = { + key: "alert", + translationKey: "months.alert", + audio: { + briefing: "alert-en.mp3", + voice: "System Alert" + } +}; + +// July is stage 5 +MONTHS_CONFIG[5] = { + key: "july", + translationKey: "months.july", + audio: { + briefing: "july-en.mp3", + voice: "Dr. Webb" + } +}; + +// September is stage 6 +MONTHS_CONFIG[6] = { + key: "september", + translationKey: "months.september" +}; + +// November is stage 7 +MONTHS_CONFIG[7] = { + key: "november", + translationKey: "months.november" +}; + +// December is stage 8 +MONTHS_CONFIG[8] = { + key: "december", + translationKey: "months.december" +}; + +// Exposé is stage 9 +MONTHS_CONFIG[9] = { + key: "exposé", + translationKey: "months.exposé" +}; + +// Utility function to get month config - now much simpler! +export function getMonthConfig(stage: string | number): MonthConfig | undefined { + const stageNum = typeof stage === 'string' ? parseInt(stage) : stage; + return MONTHS_CONFIG[stageNum]; +} \ No newline at end of file diff --git a/src/version.ts b/src/version.ts index 424ba48..b3fc8f2 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,7 +1,17 @@ export const VERSION = { - current: '0.3.0', + current: '0.4.1', releaseDate: '2024-12-16', changelog: { + '0.4.1': [ + 'Month index fixes and consolidation with stage index', + 'Enhanced metrics and KPI visualization', + 'Improved dossier panel', + ], + '0.4.0': [ + 'Major layout changes and improvements', + 'Improved mission briefing UI', + 'Added dev panel for testing', + ], '0.3.0': [ 'Added KPIs', 'Revamped strength and weakness mechanics',