зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-30 04:56:05 +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 { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
@ -10,7 +9,6 @@ const queryClient = new QueryClient();
|
|||||||
const App = () => (
|
const App = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getMonthConfig } from '@/utils/months';
|
||||||
|
|
||||||
export enum TransitionStyle {
|
export enum TransitionStyle {
|
||||||
FADE = "fade",
|
FADE = "fade",
|
||||||
@ -11,21 +12,20 @@ export enum TransitionStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MonthTransitionProps {
|
interface MonthTransitionProps {
|
||||||
monthIndex: number;
|
stage: number;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
style: TransitionStyle;
|
style?: TransitionStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to translate month name
|
const useTranslatedMonth = (stage: number) => {
|
||||||
const useTranslatedMonth = (monthIndex: number) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'exposé'];
|
const monthConfig = getMonthConfig(stage);
|
||||||
return t(`months.${monthKeys[monthIndex]}`);
|
return monthConfig ? t(monthConfig.translationKey) : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create separate components for each style
|
// Create separate components for each style
|
||||||
const FadeTransition = ({ monthIndex }: { monthIndex: number }) => {
|
const FadeTransition = ({ stage }: { stage: number }) => {
|
||||||
const translatedMonth = useTranslatedMonth(monthIndex);
|
const translatedMonth = useTranslatedMonth(stage);
|
||||||
return (
|
return (
|
||||||
<Card className="bg-transparent border-none shadow-none">
|
<Card className="bg-transparent border-none shadow-none">
|
||||||
<CardContent className="flex items-center justify-center px-4">
|
<CardContent className="flex items-center justify-center px-4">
|
||||||
@ -37,8 +37,8 @@ const FadeTransition = ({ monthIndex }: { monthIndex: number }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TypewriterTransition = ({ monthIndex }: { monthIndex: number }) => {
|
const TypewriterTransition = ({ stage }: { stage: number }) => {
|
||||||
const translatedMonth = useTranslatedMonth(monthIndex);
|
const translatedMonth = useTranslatedMonth(stage);
|
||||||
return (
|
return (
|
||||||
<div className="relative px-4">
|
<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">
|
<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 SplitScreenTransition = ({ stage }: { stage: number }) => {
|
||||||
const translatedMonth = useTranslatedMonth(monthIndex);
|
const translatedMonth = useTranslatedMonth(stage);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 flex">
|
<div className="absolute inset-0 flex">
|
||||||
@ -63,8 +63,8 @@ const SplitScreenTransition = ({ monthIndex }: { monthIndex: number }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MatrixTransition = ({ monthIndex }: { monthIndex: number }) => {
|
const MatrixTransition = ({ stage }: { stage: number }) => {
|
||||||
const translatedMonth = useTranslatedMonth(monthIndex);
|
const translatedMonth = useTranslatedMonth(stage);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
@ -90,8 +90,8 @@ const MatrixTransition = ({ monthIndex }: { monthIndex: number }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NumberCycleTransition = ({ monthIndex }: { monthIndex: number }) => {
|
const NumberCycleTransition = ({ stage }: { stage: number }) => {
|
||||||
const translatedMonth = useTranslatedMonth(monthIndex);
|
const translatedMonth = useTranslatedMonth(stage);
|
||||||
const [displayText, setDisplayText] = useState(
|
const [displayText, setDisplayText] = useState(
|
||||||
Array(translatedMonth.length).fill('0').join('')
|
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(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(onComplete, 3500);
|
const timer = setTimeout(onComplete, 3500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
@ -161,17 +161,17 @@ export const MonthTransition = ({ monthIndex, onComplete, style }: MonthTransiti
|
|||||||
const renderTransition = () => {
|
const renderTransition = () => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case TransitionStyle.FADE:
|
case TransitionStyle.FADE:
|
||||||
return <FadeTransition monthIndex={monthIndex} />;
|
return <FadeTransition stage={stage} />;
|
||||||
case TransitionStyle.TYPEWRITER:
|
case TransitionStyle.TYPEWRITER:
|
||||||
return <TypewriterTransition monthIndex={monthIndex} />;
|
return <TypewriterTransition stage={stage} />;
|
||||||
case TransitionStyle.SPLIT_SCREEN:
|
case TransitionStyle.SPLIT_SCREEN:
|
||||||
return <SplitScreenTransition monthIndex={monthIndex} />;
|
return <SplitScreenTransition stage={stage} />;
|
||||||
case TransitionStyle.MATRIX:
|
case TransitionStyle.MATRIX:
|
||||||
return <MatrixTransition monthIndex={monthIndex} />;
|
return <MatrixTransition stage={stage} />;
|
||||||
case TransitionStyle.NUMBER_CYCLE:
|
case TransitionStyle.NUMBER_CYCLE:
|
||||||
return <NumberCycleTransition monthIndex={monthIndex} />;
|
return <NumberCycleTransition stage={stage} />;
|
||||||
default:
|
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 { PlayIcon, PauseIcon } from "@heroicons/react/24/outline";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { playBriefing } from "@/utils/audio";
|
import { playBriefing } from "@/utils/audio";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "sonner";
|
||||||
|
import { getMonthConfig } from "@/utils/months";
|
||||||
|
|
||||||
interface BriefingAudioProps {
|
interface BriefingAudioProps {
|
||||||
stage: string;
|
stage: string;
|
||||||
@ -18,17 +19,22 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio
|
|||||||
|
|
||||||
const getAudioFileName = (stage: string) => {
|
const getAudioFileName = (stage: string) => {
|
||||||
const currentLanguage = i18n.language;
|
const currentLanguage = i18n.language;
|
||||||
const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'expose'];
|
|
||||||
|
console.log('BriefingAudio - Stage received:', stage);
|
||||||
|
|
||||||
// Handle special stages
|
// Handle special stages
|
||||||
if (stage === "INTRO") {
|
if (stage === "INTRO") {
|
||||||
return `intro-${currentLanguage}.mp3`;
|
return `intro-${currentLanguage}.mp3`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all other stages (including ALERT), use the month-based naming
|
const monthConfig = getMonthConfig(stage);
|
||||||
const monthIndex = parseInt(stage);
|
console.log('BriefingAudio - Selected monthConfig:', monthConfig);
|
||||||
const monthKey = monthKeys[monthIndex];
|
|
||||||
return `${monthKey}-${currentLanguage}.mp3`;
|
if (!monthConfig?.audio?.briefing) {
|
||||||
|
throw new Error(`No audio briefing configured for stage ${stage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return monthConfig.audio.briefing;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlayPause = async () => {
|
const handlePlayPause = async () => {
|
||||||
@ -56,10 +62,8 @@ export const BriefingAudio = ({ stage, audioRef, className = "" }: BriefingAudio
|
|||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Audio error:', error);
|
console.error('Audio error:', error);
|
||||||
toast({
|
toast.error("Audio Error", {
|
||||||
title: "Audio Error",
|
description: `Failed to play briefing: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
description: `Failed to play briefing: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -81,10 +81,20 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
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>
|
<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>
|
<span className="text-xs text-gray-400 font-mono tracking-wider">{t(entry.dateKey)}</span>
|
||||||
<Separator className="w-4 bg-gray-700" orientation="horizontal" />
|
<Separator className="w-4 bg-gray-700" orientation="horizontal" />
|
||||||
<TypewriterText text={t(entry.titleKey)} />
|
<TypewriterText text={t(entry.titleKey)} />
|
||||||
|
|||||||
@ -51,7 +51,8 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({ from, subject, children,
|
|||||||
<p key={index}>{paragraph}</p>
|
<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 (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Play, Pause } from "lucide-react";
|
import { Play, Pause } from "lucide-react";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { toast } from "sonner";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { playRecordingSound } from "@/utils/audio";
|
import { playRecordingSound } from "@/utils/audio";
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ interface IntroAudioProps {
|
|||||||
export const IntroAudio = ({ className }: IntroAudioProps) => {
|
export const IntroAudio = ({ className }: IntroAudioProps) => {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const { toast } = useToast();
|
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -46,10 +45,8 @@ export const IntroAudio = ({ className }: IntroAudioProps) => {
|
|||||||
if (playPromise !== undefined) {
|
if (playPromise !== undefined) {
|
||||||
playPromise.catch(error => {
|
playPromise.catch(error => {
|
||||||
console.error('Playback failed:', error);
|
console.error('Playback failed:', error);
|
||||||
toast({
|
toast.error("Playback Error", {
|
||||||
title: "Playback Error",
|
description: "Unable to play audio briefing"
|
||||||
description: "Unable to play audio briefing",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -57,10 +54,8 @@ export const IntroAudio = ({ className }: IntroAudioProps) => {
|
|||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Audio error:', error);
|
console.error('Audio error:', error);
|
||||||
toast({
|
toast.error("Audio Error", {
|
||||||
title: "Audio Error",
|
description: "Audio briefing unavailable"
|
||||||
description: "Audio briefing unavailable",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useState } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface IntroDialogProps {
|
interface IntroDialogProps {
|
||||||
onStartAudio?: () => void;
|
onStartAudio?: () => void;
|
||||||
@ -16,6 +17,31 @@ interface IntroDialogProps {
|
|||||||
export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => {
|
export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const { t } = useTranslation();
|
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 = () => {
|
const handleBeginSimulation = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@ -23,57 +49,62 @@ export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open}>
|
||||||
<DialogContent
|
<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">
|
<DialogHeader className="space-y-6">
|
||||||
<div className="space-y-6">
|
<DialogTitle className="text-yellow-500 text-2xl">
|
||||||
<DialogHeader>
|
{t('intro.title')}
|
||||||
<DialogTitle className="text-yellow-500 text-2xl mb-6">
|
</DialogTitle>
|
||||||
{t('intro.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<div className="space-y-6 text-gray-200">
|
<div className="space-y-6 text-gray-200">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<div className="text-4xl">🎯</div>
|
<div className="text-4xl">🎯</div>
|
||||||
<p className="text-lg font-medium">
|
<p className="text-lg font-medium">
|
||||||
{t('intro.mission')}
|
{t('intro.mission')}
|
||||||
</p>
|
</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>
|
|
||||||
</div>
|
</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>
|
</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>
|
</div>
|
||||||
|
|
||||||
</DialogContent>
|
</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>
|
</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 { useTranslation } from 'react-i18next';
|
||||||
import { ChoiceID } from './metrics';
|
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
|
// Create a custom hook to handle stages with translations
|
||||||
export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): GameStage[] => {
|
export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): GameStage[] => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Helper function to get translated month title
|
// Helper function to get translated month title
|
||||||
const getMonthTitle = (monthIndex: number) => {
|
const getMonthTitle = (stage: number) => {
|
||||||
const monthKeys = ['january', 'march', 'may', 'july', 'september', 'november', 'december', 'alert', 'exposé'];
|
return t(`months.${stage}`);
|
||||||
return t(`months.${monthKeys[monthIndex]}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get translated choice option
|
// Helper function to get translated choice option
|
||||||
@ -35,8 +21,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
monthIndex: MONTHS.JANUARY,
|
monthIndex: 1, // January
|
||||||
title: getMonthTitle(MONTHS.JANUARY),
|
title: getMonthTitle(1),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.1.expertMemo.from')}
|
from={t('stages.1.expertMemo.from')}
|
||||||
subject={t('stages.1.expertMemo.subject')}
|
subject={t('stages.1.expertMemo.subject')}
|
||||||
@ -114,8 +100,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
monthIndex: MONTHS.MARCH,
|
monthIndex: 2, // March
|
||||||
title: getMonthTitle(MONTHS.MARCH),
|
title: getMonthTitle(2),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.2.expertMemo.from')}
|
from={t('stages.2.expertMemo.from')}
|
||||||
subject={t('stages.2.expertMemo.subject')}
|
subject={t('stages.2.expertMemo.subject')}
|
||||||
@ -191,8 +177,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
monthIndex: MONTHS.MAY,
|
monthIndex: 3, // May
|
||||||
title: getMonthTitle(MONTHS.MAY),
|
title: getMonthTitle(3),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.3.expertMemo.from')}
|
from={t('stages.3.expertMemo.from')}
|
||||||
subject={t('stages.3.expertMemo.subject')}
|
subject={t('stages.3.expertMemo.subject')}
|
||||||
@ -269,8 +255,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
monthIndex: MONTHS.ALERT,
|
monthIndex: 4, // Alert
|
||||||
title: getMonthTitle(MONTHS.ALERT),
|
title: getMonthTitle(4),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.4.expertMemo.from')}
|
from={t('stages.4.expertMemo.from')}
|
||||||
subject={t('stages.4.expertMemo.subject')}
|
subject={t('stages.4.expertMemo.subject')}
|
||||||
@ -348,8 +334,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
monthIndex: MONTHS.JULY,
|
monthIndex: 5, // July
|
||||||
title: getMonthTitle(MONTHS.JULY),
|
title: getMonthTitle(5),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.5.expertMemo.from')}
|
from={t('stages.5.expertMemo.from')}
|
||||||
subject={t('stages.5.expertMemo.subject')}
|
subject={t('stages.5.expertMemo.subject')}
|
||||||
@ -426,8 +412,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
monthIndex: MONTHS.SEPTEMBER,
|
monthIndex: 6, // September
|
||||||
title: getMonthTitle(MONTHS.SEPTEMBER),
|
title: getMonthTitle(6),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.6.expertMemo.from')}
|
from={t('stages.6.expertMemo.from')}
|
||||||
subject={t('stages.6.expertMemo.subject')}
|
subject={t('stages.6.expertMemo.subject')}
|
||||||
@ -504,8 +490,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
monthIndex: MONTHS.NOVEMBER,
|
monthIndex: 7, // November
|
||||||
title: getMonthTitle(MONTHS.NOVEMBER),
|
title: getMonthTitle(7),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.7.expertMemo.from')}
|
from={t('stages.7.expertMemo.from')}
|
||||||
subject={t('stages.7.expertMemo.subject')}
|
subject={t('stages.7.expertMemo.subject')}
|
||||||
@ -582,8 +568,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
monthIndex: MONTHS.DECEMBER,
|
monthIndex: 8, // December
|
||||||
title: getMonthTitle(MONTHS.DECEMBER),
|
title: getMonthTitle(8),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.8.expertMemo.from')}
|
from={t('stages.8.expertMemo.from')}
|
||||||
subject={t('stages.8.expertMemo.subject')}
|
subject={t('stages.8.expertMemo.subject')}
|
||||||
@ -660,8 +646,8 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
monthIndex: MONTHS.EXPOSÉ,
|
monthIndex: 9, // Exposé
|
||||||
title: getMonthTitle(MONTHS.EXPOSÉ),
|
title: getMonthTitle(9),
|
||||||
description: <ExpertMemo
|
description: <ExpertMemo
|
||||||
from={t('stages.9.expertMemo.from')}
|
from={t('stages.9.expertMemo.from')}
|
||||||
subject={t('stages.9.expertMemo.subject')}
|
subject={t('stages.9.expertMemo.subject')}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
export * from './loadingMessages';
|
export * from './loadingMessages';
|
||||||
export * from './operationNames';
|
export * from './operationNames';
|
||||||
export * from './expertAudio';
|
|
||||||
export * from './gameStages';
|
export * from './gameStages';
|
||||||
export * from './choiceImplications';
|
export * from './choiceImplications';
|
||||||
export * from './metrics';
|
export * from './metrics';
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { ChoiceID } from './constants/metrics';
|
import { ChoiceID } from './constants/metrics';
|
||||||
|
import { ExpertAudio } from '@/utils/months';
|
||||||
|
|
||||||
export interface LoadingMessage {
|
export interface LoadingMessage {
|
||||||
action: string;
|
action: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpertAudio {
|
|
||||||
briefing: string;
|
|
||||||
voice: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StrategyAnimation {
|
export interface StrategyAnimation {
|
||||||
type: "network" | "meme" | "news" | "community" | "expert" | "research" |
|
type: "network" | "meme" | "news" | "community" | "expert" | "research" |
|
||||||
"podcast" | "event" | "platform" | "freedom" | "influencer" | "silence" |
|
"podcast" | "event" | "platform" | "freedom" | "influencer" | "silence" |
|
||||||
|
|||||||
@ -1,25 +1,52 @@
|
|||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner } from "sonner"
|
import { Toaster as Sonner } from "sonner"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
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 (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
|
duration={4000}
|
||||||
|
position="top-right"
|
||||||
|
closeButton
|
||||||
|
richColors
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
classNames: {
|
classNames: {
|
||||||
toast:
|
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",
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
actionButton:
|
actionButton:
|
||||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
cancelButton:
|
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}
|
{...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 }
|
|
||||||
@ -93,3 +93,37 @@
|
|||||||
@apply bg-background text-foreground;
|
@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 { useGameStages, OPERATION_NAMES, useLoadingMessages, generateFinalReport } from "@/components/game/constants";
|
||||||
import { ChoiceID, calculateMetrics } from "@/components/game/constants/metrics";
|
import { ChoiceID, calculateMetrics } from "@/components/game/constants/metrics";
|
||||||
import { DossierEntry, GameStage } from "@/components/game/types";
|
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 { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -38,18 +37,11 @@ import { MetricsDisplay } from "@/components/game/MetricsDisplay";
|
|||||||
import { MuteButton } from '@/components/MuteButton';
|
import { MuteButton } from '@/components/MuteButton';
|
||||||
import { DevPanel } from "@/components/game/DevPanel";
|
import { DevPanel } from "@/components/game/DevPanel";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const monthKeys = [
|
// Get valid month keys (skipping index 0)
|
||||||
'january', // 0
|
const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[];
|
||||||
'march', // 1
|
|
||||||
'may', // 2
|
|
||||||
'alert', // 3
|
|
||||||
'july', // 4
|
|
||||||
'september', // 5
|
|
||||||
'november', // 6
|
|
||||||
'december', // 7
|
|
||||||
'exposé' // 8
|
|
||||||
];
|
|
||||||
|
|
||||||
const STAGE_CHOICES = [
|
const STAGE_CHOICES = [
|
||||||
['DEPLOY_BOTS', 'ESTABLISH_MEMES'], // January
|
['DEPLOY_BOTS', 'ESTABLISH_MEMES'], // January
|
||||||
@ -76,7 +68,6 @@ const Index = () => {
|
|||||||
const [showingResult, setShowingResult] = useState(false);
|
const [showingResult, setShowingResult] = useState(false);
|
||||||
const [currentResult, setCurrentResult] = useState<GameStage["choices"][0]["result"] | null>(null);
|
const [currentResult, setCurrentResult] = useState<GameStage["choices"][0]["result"] | null>(null);
|
||||||
const [dossierEntries, setDossierEntries] = useState<DossierEntry[]>([]);
|
const [dossierEntries, setDossierEntries] = useState<DossierEntry[]>([]);
|
||||||
const { toast } = useToast();
|
|
||||||
const [showingMonthTransition, setShowingMonthTransition] = useState(false);
|
const [showingMonthTransition, setShowingMonthTransition] = useState(false);
|
||||||
const [nextStage, setNextStage] = useState<number | null>(null);
|
const [nextStage, setNextStage] = useState<number | null>(null);
|
||||||
const [transitionStyle, setTransitionStyle] = useState<TransitionStyle>(TransitionStyle.NUMBER_CYCLE);
|
const [transitionStyle, setTransitionStyle] = useState<TransitionStyle>(TransitionStyle.NUMBER_CYCLE);
|
||||||
@ -144,10 +135,7 @@ const Index = () => {
|
|||||||
const handleInitialTransitionComplete = () => {
|
const handleInitialTransitionComplete = () => {
|
||||||
setShowingInitialTransition(false);
|
setShowingInitialTransition(false);
|
||||||
setGameStarted(true);
|
setGameStarted(true);
|
||||||
toast({
|
toast(t('mission.welcome.description'));
|
||||||
title: t('mission.welcome.title'),
|
|
||||||
description: t('mission.welcome.description'),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChoice = async (choice: GameStage["choices"][0]) => {
|
const handleChoice = async (choice: GameStage["choices"][0]) => {
|
||||||
@ -208,17 +196,7 @@ const Index = () => {
|
|||||||
setShowingResult(true);
|
setShowingResult(true);
|
||||||
|
|
||||||
const newEntry: DossierEntry = {
|
const newEntry: DossierEntry = {
|
||||||
dateKey: stages[currentStage].monthIndex === 0 ? 'months.january' :
|
dateKey: `months.${getMonthConfig(currentStage + 1)?.key}`,
|
||||||
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',
|
|
||||||
titleKey: `stages.${currentStage + 1}.choices.${choice.id}.result.title`,
|
titleKey: `stages.${currentStage + 1}.choices.${choice.id}.result.title`,
|
||||||
insightKeys: Array.from({ length: 4 }, (_, i) => `stages.${currentStage + 1}.choices.${choice.id}.result.insights.${i}`),
|
insightKeys: Array.from({ length: 4 }, (_, i) => `stages.${currentStage + 1}.choices.${choice.id}.result.insights.${i}`),
|
||||||
strategicNoteKey: `stages.${currentStage + 1}.choices.${choice.id}.result.nextStepHint`
|
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="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
||||||
<div className="max-w-4xl mx-auto w-full relative">
|
<div className="max-w-4xl mx-auto w-full relative">
|
||||||
<MonthTransition
|
<MonthTransition
|
||||||
monthIndex={stages[0]?.monthIndex ?? 1}
|
stage={1}
|
||||||
onComplete={handleInitialTransitionComplete}
|
onComplete={handleInitialTransitionComplete}
|
||||||
style={TransitionStyle.NUMBER_CYCLE}
|
style={TransitionStyle.NUMBER_CYCLE}
|
||||||
/>
|
/>
|
||||||
@ -521,7 +499,7 @@ const Index = () => {
|
|||||||
<GameBackground shouldStartAudio={shouldStartAudio} />
|
<GameBackground shouldStartAudio={shouldStartAudio} />
|
||||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
||||||
<MonthTransition
|
<MonthTransition
|
||||||
monthIndex={stages[nextStage]?.monthIndex ?? nextStage + 1}
|
stage={nextStage + 1}
|
||||||
onComplete={handleTransitionComplete}
|
onComplete={handleTransitionComplete}
|
||||||
style={transitionStyle}
|
style={transitionStyle}
|
||||||
/>
|
/>
|
||||||
@ -542,7 +520,13 @@ const Index = () => {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<MuteButton />
|
<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>
|
</div>
|
||||||
{currentStage > 0 && <DossierPanel entries={dossierEntries} choices={previousChoices} />}
|
{currentStage > 0 && <DossierPanel entries={dossierEntries} choices={previousChoices} />}
|
||||||
</div>
|
</div>
|
||||||
@ -553,16 +537,19 @@ const Index = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{currentStageData.choices.map((choice, index) => (
|
{(() => {
|
||||||
<ChoiceCard
|
console.log('Index - Rendering stage:', currentStage);
|
||||||
key={choice.id}
|
return currentStageData.choices.map((choice, index) => (
|
||||||
choice={choice}
|
<ChoiceCard
|
||||||
previousChoices={previousChoices}
|
key={choice.id}
|
||||||
onClick={() => handleStrategyClick(choice)}
|
choice={choice}
|
||||||
disabled={showingResult || isLoading}
|
previousChoices={previousChoices}
|
||||||
optionNumber={index + 1}
|
onClick={() => handleStrategyClick(choice)}
|
||||||
/>
|
disabled={showingResult || isLoading}
|
||||||
))}
|
optionNumber={index + 1}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<div className="mt-4 border-t border-gray-700/50">
|
<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 = {
|
export const VERSION = {
|
||||||
current: '0.3.0',
|
current: '0.4.1',
|
||||||
releaseDate: '2024-12-16',
|
releaseDate: '2024-12-16',
|
||||||
changelog: {
|
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': [
|
'0.3.0': [
|
||||||
'Added KPIs',
|
'Added KPIs',
|
||||||
'Revamped strength and weakness mechanics',
|
'Revamped strength and weakness mechanics',
|
||||||
|
|||||||
Загрузка…
x
Ссылка в новой задаче
Block a user