зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-29 12:46:03 +02:00
added timeline
Этот коммит содержится в:
родитель
7693783b88
Коммит
60b95516ea
@ -13,7 +13,14 @@ interface ExpertMemoProps {
|
|||||||
audioRef?: React.RefObject<HTMLAudioElement>;
|
audioRef?: React.RefObject<HTMLAudioElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExpertMemo: React.FC<ExpertMemoProps> = ({ from, subject, children, isAlert = false, stage, audioRef }) => {
|
export const ExpertMemo: React.FC<ExpertMemoProps> = ({
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
children,
|
||||||
|
isAlert = false,
|
||||||
|
stage,
|
||||||
|
audioRef
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500';
|
const highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500';
|
||||||
const memoClass = isAlert ? 'expert-memo alert' : 'expert-memo';
|
const memoClass = isAlert ? 'expert-memo alert' : 'expert-memo';
|
||||||
|
|||||||
122
src/components/game/ProgressionIndicator.tsx
Обычный файл
122
src/components/game/ProgressionIndicator.tsx
Обычный файл
@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ChoiceID } from './constants/metrics';
|
||||||
|
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
|
||||||
|
|
||||||
|
interface ProgressionIndicatorProps {
|
||||||
|
currentStage: number;
|
||||||
|
totalStages: number;
|
||||||
|
previousChoices?: ChoiceID[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressionIndicator: React.FC<ProgressionIndicatorProps> = ({
|
||||||
|
currentStage,
|
||||||
|
totalStages,
|
||||||
|
previousChoices = [],
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Get month name for the stage
|
||||||
|
const getMonthName = (stageIndex: number) => {
|
||||||
|
const monthConfig = getMonthConfig(stageIndex + 1);
|
||||||
|
if (monthConfig && monthConfig.key) {
|
||||||
|
return t(`months.${monthConfig.key}`);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get choice names for tooltips
|
||||||
|
const getChoiceName = (choiceId: ChoiceID, index: number) => {
|
||||||
|
// Try to get the choice name from the translation
|
||||||
|
try {
|
||||||
|
// Find which choice number it was (1 or 2)
|
||||||
|
const choiceNumber = choiceId.toString().includes('_1') ||
|
||||||
|
choiceId.toString().endsWith('_BOTS') ||
|
||||||
|
choiceId.toString().endsWith('_NEWS') ||
|
||||||
|
choiceId.toString().endsWith('_COLLABORATION') ||
|
||||||
|
choiceId.toString().endsWith('_COURSE') ||
|
||||||
|
choiceId.toString().endsWith('_PANEL') ||
|
||||||
|
choiceId.toString().endsWith('_PAPER') ||
|
||||||
|
choiceId.toString().endsWith('_PLATFORMS') ||
|
||||||
|
choiceId.toString().endsWith('_STRATEGY') ||
|
||||||
|
choiceId.toString().endsWith('_DEFENSE') ? 1 : 2;
|
||||||
|
|
||||||
|
return t(`stages.${index + 1}.choices.${choiceNumber}.text`);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to the ID if translation fails
|
||||||
|
return choiceId.toString().replace(/_/g, ' ');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center justify-center w-full py-3", className)}>
|
||||||
|
<div className="flex items-center space-x-2 w-full max-w-3xl">
|
||||||
|
{Array.from({ length: totalStages }).map((_, index) => {
|
||||||
|
const isActive = index <= currentStage;
|
||||||
|
const isPast = index < currentStage;
|
||||||
|
const hasChoice = index < previousChoices.length;
|
||||||
|
|
||||||
|
// Only render tooltips for past and current stages
|
||||||
|
const DotComponent = isActive ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-3 h-3 rounded-full flex items-center justify-center transition-all duration-300",
|
||||||
|
isActive ? "bg-yellow-500" : "bg-gray-600",
|
||||||
|
"hover:scale-110 cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasChoice && (
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-black" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="bg-black/90 border-gray-700 text-white text-xs">
|
||||||
|
{hasChoice ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-bold mb-1">{getMonthName(index)}</div>
|
||||||
|
<div>{getChoiceName(previousChoices[index], index)}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-bold">{getMonthName(index)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
// For future stages, just render the dot without a tooltip
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-3 h-3 rounded-full flex items-center justify-center",
|
||||||
|
"bg-gray-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{index > 0 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-[1px] flex-grow",
|
||||||
|
isPast ? "bg-yellow-500" : "bg-gray-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{DotComponent}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -18,6 +18,9 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
return t(`choices.option${number}`);
|
return t(`choices.option${number}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Total number of stages
|
||||||
|
const totalStages = 9;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -651,7 +654,6 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
|||||||
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')}
|
||||||
isAlert={true}
|
|
||||||
stage="9"
|
stage="9"
|
||||||
audioRef={audioRef}>
|
audioRef={audioRef}>
|
||||||
<p>{t('stages.9.expertMemo.content.greeting')}</p>
|
<p>{t('stages.9.expertMemo.content.greeting')}</p>
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { DevPanel } from "@/components/game/DevPanel";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
|
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ProgressionIndicator } from '@/components/game/ProgressionIndicator';
|
||||||
|
|
||||||
// Get valid month keys (skipping index 0)
|
// Get valid month keys (skipping index 0)
|
||||||
const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[];
|
const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[];
|
||||||
@ -58,6 +59,7 @@ const STAGE_CHOICES = [
|
|||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
|
||||||
const stages = useGameStages(audioRef);
|
const stages = useGameStages(audioRef);
|
||||||
const operationNameKey = OPERATION_NAMES[Math.floor(Math.random() * OPERATION_NAMES.length)];
|
const operationNameKey = OPERATION_NAMES[Math.floor(Math.random() * OPERATION_NAMES.length)];
|
||||||
const operationName = t(`operations.${operationNameKey}`);
|
const operationName = t(`operations.${operationNameKey}`);
|
||||||
@ -78,7 +80,6 @@ const Index = () => {
|
|||||||
const [showIntroDialog, setShowIntroDialog] = useState(true);
|
const [showIntroDialog, setShowIntroDialog] = useState(true);
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [selectedChoice, setSelectedChoice] = useState<GameStage["choices"][0] | null>(null);
|
const [selectedChoice, setSelectedChoice] = useState<GameStage["choices"][0] | null>(null);
|
||||||
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
|
|
||||||
const [gameComplete, setGameComplete] = useState(false);
|
const [gameComplete, setGameComplete] = useState(false);
|
||||||
const [playerChoices, setPlayerChoices] = useState<string[]>([]);
|
const [playerChoices, setPlayerChoices] = useState<string[]>([]);
|
||||||
const [gameKey, setGameKey] = useState(0);
|
const [gameKey, setGameKey] = useState(0);
|
||||||
@ -434,59 +435,61 @@ const Index = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen overflow-hidden">
|
<div className="relative min-h-screen overflow-hidden">
|
||||||
<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 flex-col">
|
||||||
<Card className="relative border-gray-700 bg-black/30">
|
<div className="flex-grow flex items-center justify-center">
|
||||||
<div className="bg-gray-800/30 p-6 rounded-t-md border border-gray-700">
|
<Card className="relative border-gray-700 bg-black/30">
|
||||||
<h3 className="text-yellow-500 font-semibold mb-4">{t('analysis.metricsUpdate')}</h3>
|
<div className="bg-gray-800/30 p-6 rounded-t-md border border-gray-700">
|
||||||
<MetricsDisplay
|
<h3 className="text-yellow-500 font-semibold mb-4">{t('analysis.metricsUpdate')}</h3>
|
||||||
choices={previousChoices}
|
<MetricsDisplay
|
||||||
showTitle={false}
|
choices={previousChoices}
|
||||||
className="pl-0"
|
showTitle={false}
|
||||||
/>
|
className="pl-0"
|
||||||
</div>
|
/>
|
||||||
<CardHeader>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<CardHeader>
|
||||||
<CardDescription className="text-emerald-400/90 italic">
|
<div className="flex flex-col gap-4">
|
||||||
{t('analysis.intelligenceGathered.description')}
|
<CardDescription className="text-emerald-400/90 italic">
|
||||||
</CardDescription>
|
{t('analysis.intelligenceGathered.description')}
|
||||||
<div className="flex justify-between items-center">
|
</CardDescription>
|
||||||
<CardTitle className="text-xl md:text-2xl text-yellow-500">{currentResult.title}</CardTitle>
|
<div className="flex justify-between items-center">
|
||||||
|
<CardTitle className="text-xl md:text-2xl text-yellow-500">{currentResult.title}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription className="text-gray-300">
|
||||||
|
{currentResult.description}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-yellow-500 font-semibold mb-3">{t('analysis.keyInsights')}</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{currentResult.insights.map((insight, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-2 text-gray-300">
|
||||||
|
<span className="text-yellow-500">•</span>
|
||||||
|
{insight}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-700 pt-4">
|
||||||
|
<p className="text-gray-400 italic">
|
||||||
|
<span className="text-yellow-500 font-semibold">{t('analysis.strategicInsight')} </span>
|
||||||
|
{currentResult.nextStepHint}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="text-gray-300">
|
|
||||||
{currentResult.description}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-yellow-500 font-semibold mb-3">{t('analysis.keyInsights')}</h3>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{currentResult.insights.map((insight, index) => (
|
|
||||||
<li key={index} className="flex items-start gap-2 text-gray-300">
|
|
||||||
<span className="text-yellow-500">•</span>
|
|
||||||
{insight}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-700 pt-4">
|
|
||||||
<p className="text-gray-400 italic">
|
|
||||||
<span className="text-yellow-500 font-semibold">{t('analysis.strategicInsight')} </span>
|
|
||||||
{currentResult.nextStepHint}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center pt-4">
|
<div className="flex justify-center pt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleContinue}
|
onClick={handleContinue}
|
||||||
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
|
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
|
||||||
>
|
>
|
||||||
{t('buttons.proceedToNext')}
|
{t('buttons.proceedToNext')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -511,6 +514,14 @@ const Index = () => {
|
|||||||
<div className="relative min-h-screen overflow-hidden">
|
<div className="relative min-h-screen overflow-hidden">
|
||||||
<GameBackground shouldStartAudio={shouldStartAudio} />
|
<GameBackground shouldStartAudio={shouldStartAudio} />
|
||||||
<div className="relative min-h-screen bg-transparent md:p-4 flex flex-col">
|
<div className="relative min-h-screen bg-transparent md:p-4 flex flex-col">
|
||||||
|
<div className="w-full max-w-4xl mx-auto px-4 mt-4 mb-6">
|
||||||
|
<ProgressionIndicator
|
||||||
|
currentStage={currentStage}
|
||||||
|
totalStages={stages.length}
|
||||||
|
previousChoices={previousChoices}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-grow flex items-center">
|
<div className="flex-grow flex items-center">
|
||||||
<div className="w-full h-full md:max-w-4xl mx-auto md:px-4">
|
<div className="w-full h-full md:max-w-4xl mx-auto md:px-4">
|
||||||
<Card className="bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in h-full md:h-auto md:rounded-lg border-0 md:border">
|
<Card className="bg-black/50 text-white border-gray-700 transition-all duration-1000 animate-fade-in h-full md:h-auto md:rounded-lg border-0 md:border">
|
||||||
|
|||||||
Загрузка…
x
Ссылка в новой задаче
Block a user