зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-28 20:34:15 +02:00
version 0.4.1
Этот коммит содержится в:
родитель
444400bddf
Коммит
55599a2da2
@ -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 = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
@ -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 (
|
||||
<Card className="bg-transparent border-none shadow-none">
|
||||
<CardContent className="flex items-center justify-center px-4">
|
||||
@ -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 (
|
||||
<div className="relative px-4">
|
||||
<div className="overflow-hidden whitespace-normal md:whitespace-nowrap border-r-4 border-yellow-500 pr-1 text-4xl md:text-6xl font-bold text-yellow-500 animate-typewriter animate-cursor-blink max-w-[90vw] break-words">
|
||||
@ -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 (
|
||||
<>
|
||||
<div className="absolute inset-0 flex">
|
||||
@ -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 (
|
||||
<>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
@ -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 <FadeTransition monthIndex={monthIndex} />;
|
||||
return <FadeTransition stage={stage} />;
|
||||
case TransitionStyle.TYPEWRITER:
|
||||
return <TypewriterTransition monthIndex={monthIndex} />;
|
||||
return <TypewriterTransition stage={stage} />;
|
||||
case TransitionStyle.SPLIT_SCREEN:
|
||||
return <SplitScreenTransition monthIndex={monthIndex} />;
|
||||
return <SplitScreenTransition stage={stage} />;
|
||||
case TransitionStyle.MATRIX:
|
||||
return <MatrixTransition monthIndex={monthIndex} />;
|
||||
return <MatrixTransition stage={stage} />;
|
||||
case TransitionStyle.NUMBER_CYCLE:
|
||||
return <NumberCycleTransition monthIndex={monthIndex} />;
|
||||
return <NumberCycleTransition stage={stage} />;
|
||||
default:
|
||||
return <FadeTransition monthIndex={monthIndex} />;
|
||||
return <FadeTransition stage={stage} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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'}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-yellow-500 font-semibold flex items-center gap-3">
|
||||
<h3 className={cn(
|
||||
"font-semibold flex items-center gap-3",
|
||||
entry.dateKey.toLowerCase().includes('alert') || entry.dateKey.toLowerCase().includes('expose')
|
||||
? "text-red-500"
|
||||
: "text-yellow-500"
|
||||
)}>
|
||||
<span className="text-xs text-gray-400 font-mono tracking-wider">{t(entry.dateKey)}</span>
|
||||
<Separator className="w-4 bg-gray-700" orientation="horizontal" />
|
||||
<TypewriterText text={t(entry.titleKey)} />
|
||||
|
||||
@ -51,7 +51,8 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({ from, subject, children,
|
||||
<p key={index}>{paragraph}</p>
|
||||
));
|
||||
}
|
||||
return content;
|
||||
// If it's already a React node (like a div), return it as is
|
||||
return <div className="prose prose-invert">{content}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -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<HTMLAudioElement | null>(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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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<HTMLDivElement>(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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog open={open}>
|
||||
<DialogContent
|
||||
className="bg-black text-white border-gray-700 max-w-2xl max-h-[85vh] overflow-y-auto"
|
||||
ref={contentRef}
|
||||
className="[&>button]:hidden bg-black text-white border-gray-700 max-w-2xl max-h-[85vh] overflow-y-auto space-y-6 p-6 pb-[30px] relative text-center fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]"
|
||||
>
|
||||
<div className="relative flex-1 min-h-0">
|
||||
<div className="space-y-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-yellow-500 text-2xl mb-6">
|
||||
{t('intro.title')}
|
||||
</DialogTitle>
|
||||
|
||||
<div className="space-y-6 text-gray-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-4xl">🎯</div>
|
||||
<p className="text-lg font-medium">
|
||||
{t('intro.mission')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-base">
|
||||
{t('intro.explanation')}
|
||||
</p>
|
||||
|
||||
<p className="text-base">
|
||||
{t('intro.howToPlay.description')}
|
||||
</p>
|
||||
|
||||
<p className="text-yellow-500 text-sm">
|
||||
{t('intro.reminder')}
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col items-center gap-6 mt-8">
|
||||
<div className="flex items-center gap-2 self-start">
|
||||
<LanguageSwitcher />
|
||||
<span className="text-xs text-gray-400">
|
||||
{t('languageSwitcher.hint')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleBeginSimulation}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-black font-semibold w-full py-6 text-lg"
|
||||
>
|
||||
{t('buttons.beginSimulation')}
|
||||
</Button>
|
||||
<DialogHeader className="space-y-6">
|
||||
<DialogTitle className="text-yellow-500 text-2xl">
|
||||
{t('intro.title')}
|
||||
</DialogTitle>
|
||||
|
||||
<div className="space-y-6 text-gray-200">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="text-4xl">🎯</div>
|
||||
<p className="text-lg font-medium">
|
||||
{t('intro.mission')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-base leading-relaxed drop-shadow-lg">
|
||||
{t('intro.explanation')}
|
||||
</p>
|
||||
|
||||
<p className="text-base leading-relaxed drop-shadow-lg">
|
||||
{t('intro.howToPlay.description')}
|
||||
</p>
|
||||
|
||||
<p className="text-yellow-500 text-sm drop-shadow">
|
||||
{t('intro.reminder')}
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col items-center gap-6 w-full">
|
||||
<div className="flex items-center gap-2 self-start">
|
||||
<LanguageSwitcher />
|
||||
<span className="text-xs text-gray-400">
|
||||
{t('languageSwitcher.hint')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleBeginSimulation}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-black font-semibold w-full py-6 text-lg"
|
||||
>
|
||||
{t('buttons.beginSimulation')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0 h-[120px] pointer-events-none transition-opacity duration-300",
|
||||
"bg-gradient-to-t from-black from-10% via-black/90 via-50% to-transparent to-100%",
|
||||
showGradient ? "opacity-95" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
};
|
||||
@ -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<HTMLAudioElement>): 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<HTMLAudioElement>): Game
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
monthIndex: MONTHS.JANUARY,
|
||||
title: getMonthTitle(MONTHS.JANUARY),
|
||||
monthIndex: 1, // January
|
||||
title: getMonthTitle(1),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.1.expertMemo.from')}
|
||||
subject={t('stages.1.expertMemo.subject')}
|
||||
@ -114,8 +100,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
monthIndex: MONTHS.MARCH,
|
||||
title: getMonthTitle(MONTHS.MARCH),
|
||||
monthIndex: 2, // March
|
||||
title: getMonthTitle(2),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.2.expertMemo.from')}
|
||||
subject={t('stages.2.expertMemo.subject')}
|
||||
@ -191,8 +177,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
monthIndex: MONTHS.MAY,
|
||||
title: getMonthTitle(MONTHS.MAY),
|
||||
monthIndex: 3, // May
|
||||
title: getMonthTitle(3),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.3.expertMemo.from')}
|
||||
subject={t('stages.3.expertMemo.subject')}
|
||||
@ -269,8 +255,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
monthIndex: MONTHS.ALERT,
|
||||
title: getMonthTitle(MONTHS.ALERT),
|
||||
monthIndex: 4, // Alert
|
||||
title: getMonthTitle(4),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.4.expertMemo.from')}
|
||||
subject={t('stages.4.expertMemo.subject')}
|
||||
@ -348,8 +334,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
monthIndex: MONTHS.JULY,
|
||||
title: getMonthTitle(MONTHS.JULY),
|
||||
monthIndex: 5, // July
|
||||
title: getMonthTitle(5),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.5.expertMemo.from')}
|
||||
subject={t('stages.5.expertMemo.subject')}
|
||||
@ -426,8 +412,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
monthIndex: MONTHS.SEPTEMBER,
|
||||
title: getMonthTitle(MONTHS.SEPTEMBER),
|
||||
monthIndex: 6, // September
|
||||
title: getMonthTitle(6),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.6.expertMemo.from')}
|
||||
subject={t('stages.6.expertMemo.subject')}
|
||||
@ -504,8 +490,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
monthIndex: MONTHS.NOVEMBER,
|
||||
title: getMonthTitle(MONTHS.NOVEMBER),
|
||||
monthIndex: 7, // November
|
||||
title: getMonthTitle(7),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.7.expertMemo.from')}
|
||||
subject={t('stages.7.expertMemo.subject')}
|
||||
@ -582,8 +568,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
monthIndex: MONTHS.DECEMBER,
|
||||
title: getMonthTitle(MONTHS.DECEMBER),
|
||||
monthIndex: 8, // December
|
||||
title: getMonthTitle(8),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.8.expertMemo.from')}
|
||||
subject={t('stages.8.expertMemo.subject')}
|
||||
@ -660,8 +646,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
monthIndex: MONTHS.EXPOSÉ,
|
||||
title: getMonthTitle(MONTHS.EXPOSÉ),
|
||||
monthIndex: 9, // Exposé
|
||||
title: getMonthTitle(9),
|
||||
description: <ExpertMemo
|
||||
from={t('stages.9.expertMemo.from')}
|
||||
subject={t('stages.9.expertMemo.subject')}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export * from './loadingMessages';
|
||||
export * from './operationNames';
|
||||
export * from './expertAudio';
|
||||
export * from './gameStages';
|
||||
export * from './choiceImplications';
|
||||
export * from './metrics';
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
import { ChoiceID } from './constants/metrics';
|
||||
import { ExpertAudio } from '@/utils/months';
|
||||
|
||||
export interface LoadingMessage {
|
||||
action: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface ExpertAudio {
|
||||
briefing: string;
|
||||
voice: string;
|
||||
}
|
||||
|
||||
export interface StrategyAnimation {
|
||||
type: "network" | "meme" | "news" | "community" | "expert" | "research" |
|
||||
"podcast" | "event" | "platform" | "freedom" | "influencer" | "silence" |
|
||||
|
||||
@ -1,25 +1,52 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
import { useEffect } from "react"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
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 (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
duration={4000}
|
||||
position="top-right"
|
||||
closeButton
|
||||
richColors
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-2 group-[.toaster]:border-yellow-500/30 group-[.toaster]:shadow-[0_0_10px_rgba(234,179,8,0.1)] group-[.toaster]:shadow-lg relative",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
"group-[.toast]:bg-yellow-500/10 group-[.toast]:text-yellow-500 group-[.toast]:hover:bg-yellow-500/20",
|
||||
closeButton:
|
||||
"group-[.toast]:bg-yellow-500/10 group-[.toast]:text-yellow-500 group-[.toast]:hover:bg-yellow-500/20",
|
||||
},
|
||||
descriptionClassName: "text-sm text-yellow-500/70",
|
||||
style: {
|
||||
'--duration': '4000ms',
|
||||
} as React.CSSProperties,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -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<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
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<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
@ -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 (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import { useToast, toast } from "@/hooks/use-toast";
|
||||
|
||||
export { useToast, toast };
|
||||
@ -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<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
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<ToasterToast, "id">
|
||||
|
||||
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<State>(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 }
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<GameStage["choices"][0]["result"] | null>(null);
|
||||
const [dossierEntries, setDossierEntries] = useState<DossierEntry[]>([]);
|
||||
const { toast } = useToast();
|
||||
const [showingMonthTransition, setShowingMonthTransition] = useState(false);
|
||||
const [nextStage, setNextStage] = useState<number | null>(null);
|
||||
const [transitionStyle, setTransitionStyle] = useState<TransitionStyle>(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 = () => {
|
||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
||||
<div className="max-w-4xl mx-auto w-full relative">
|
||||
<MonthTransition
|
||||
monthIndex={stages[0]?.monthIndex ?? 1}
|
||||
stage={1}
|
||||
onComplete={handleInitialTransitionComplete}
|
||||
style={TransitionStyle.NUMBER_CYCLE}
|
||||
/>
|
||||
@ -521,7 +499,7 @@ const Index = () => {
|
||||
<GameBackground shouldStartAudio={shouldStartAudio} />
|
||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
||||
<MonthTransition
|
||||
monthIndex={stages[nextStage]?.monthIndex ?? nextStage + 1}
|
||||
stage={nextStage + 1}
|
||||
onComplete={handleTransitionComplete}
|
||||
style={transitionStyle}
|
||||
/>
|
||||
@ -542,7 +520,13 @@ const Index = () => {
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<MuteButton />
|
||||
<span className="text-yellow-500 font-mono text-lg">{t(`months.${monthKeys[currentStageData.monthIndex]}`)}</span>
|
||||
<span className="text-yellow-500 font-mono text-lg">
|
||||
{(() => {
|
||||
console.log('Index - currentStageData:', currentStageData);
|
||||
const monthConfig = getMonthConfig(currentStage + 1);
|
||||
return t(`months.${monthConfig?.key}`);
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
{currentStage > 0 && <DossierPanel entries={dossierEntries} choices={previousChoices} />}
|
||||
</div>
|
||||
@ -553,16 +537,19 @@ const Index = () => {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{currentStageData.choices.map((choice, index) => (
|
||||
<ChoiceCard
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
previousChoices={previousChoices}
|
||||
onClick={() => handleStrategyClick(choice)}
|
||||
disabled={showingResult || isLoading}
|
||||
optionNumber={index + 1}
|
||||
/>
|
||||
))}
|
||||
{(() => {
|
||||
console.log('Index - Rendering stage:', currentStage);
|
||||
return currentStageData.choices.map((choice, index) => (
|
||||
<ChoiceCard
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
previousChoices={previousChoices}
|
||||
onClick={() => handleStrategyClick(choice)}
|
||||
disabled={showingResult || isLoading}
|
||||
optionNumber={index + 1}
|
||||
/>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className="mt-4 border-t border-gray-700/50">
|
||||
|
||||
100
src/utils/months.ts
Обычный файл
100
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];
|
||||
}
|
||||
@ -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',
|
||||
|
||||
Загрузка…
x
Ссылка в новой задаче
Block a user