зеркало из
https://github.com/kodackx/disinformation-quest.git
synced 2025-10-29 20:46:05 +02:00
added timeline
Этот коммит содержится в:
родитель
7693783b88
Коммит
60b95516ea
@ -13,7 +13,14 @@ interface ExpertMemoProps {
|
||||
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 highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500';
|
||||
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}`);
|
||||
};
|
||||
|
||||
// Total number of stages
|
||||
const totalStages = 9;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
@ -651,7 +654,6 @@ export const useGameStages = (audioRef: React.RefObject<HTMLAudioElement>): Game
|
||||
description: <ExpertMemo
|
||||
from={t('stages.9.expertMemo.from')}
|
||||
subject={t('stages.9.expertMemo.subject')}
|
||||
isAlert={true}
|
||||
stage="9"
|
||||
audioRef={audioRef}>
|
||||
<p>{t('stages.9.expertMemo.content.greeting')}</p>
|
||||
|
||||
@ -39,6 +39,7 @@ import { DevPanel } from "@/components/game/DevPanel";
|
||||
import { motion } from "framer-motion";
|
||||
import { MONTHS_CONFIG, getMonthConfig } from "@/utils/months";
|
||||
import { toast } from "sonner";
|
||||
import { ProgressionIndicator } from '@/components/game/ProgressionIndicator';
|
||||
|
||||
// Get valid month keys (skipping index 0)
|
||||
const monthKeys = MONTHS_CONFIG.slice(1).map(config => config?.key).filter(Boolean) as string[];
|
||||
@ -58,6 +59,7 @@ const STAGE_CHOICES = [
|
||||
const Index = () => {
|
||||
const { t } = useTranslation();
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
|
||||
const stages = useGameStages(audioRef);
|
||||
const operationNameKey = OPERATION_NAMES[Math.floor(Math.random() * OPERATION_NAMES.length)];
|
||||
const operationName = t(`operations.${operationNameKey}`);
|
||||
@ -78,7 +80,6 @@ const Index = () => {
|
||||
const [showIntroDialog, setShowIntroDialog] = useState(true);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [selectedChoice, setSelectedChoice] = useState<GameStage["choices"][0] | null>(null);
|
||||
const [previousChoices, setPreviousChoices] = useState<ChoiceID[]>([]);
|
||||
const [gameComplete, setGameComplete] = useState(false);
|
||||
const [playerChoices, setPlayerChoices] = useState<string[]>([]);
|
||||
const [gameKey, setGameKey] = useState(0);
|
||||
@ -434,59 +435,61 @@ const Index = () => {
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<GameBackground shouldStartAudio={shouldStartAudio} />
|
||||
<div className="relative min-h-screen bg-transparent p-4 flex items-center justify-center">
|
||||
<Card className="relative border-gray-700 bg-black/30">
|
||||
<div className="bg-gray-800/30 p-6 rounded-t-md border border-gray-700">
|
||||
<h3 className="text-yellow-500 font-semibold mb-4">{t('analysis.metricsUpdate')}</h3>
|
||||
<MetricsDisplay
|
||||
choices={previousChoices}
|
||||
showTitle={false}
|
||||
className="pl-0"
|
||||
/>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardDescription className="text-emerald-400/90 italic">
|
||||
{t('analysis.intelligenceGathered.description')}
|
||||
</CardDescription>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle className="text-xl md:text-2xl text-yellow-500">{currentResult.title}</CardTitle>
|
||||
<div className="relative min-h-screen bg-transparent p-4 flex flex-col">
|
||||
<div className="flex-grow flex items-center justify-center">
|
||||
<Card className="relative border-gray-700 bg-black/30">
|
||||
<div className="bg-gray-800/30 p-6 rounded-t-md border border-gray-700">
|
||||
<h3 className="text-yellow-500 font-semibold mb-4">{t('analysis.metricsUpdate')}</h3>
|
||||
<MetricsDisplay
|
||||
choices={previousChoices}
|
||||
showTitle={false}
|
||||
className="pl-0"
|
||||
/>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardDescription className="text-emerald-400/90 italic">
|
||||
{t('analysis.intelligenceGathered.description')}
|
||||
</CardDescription>
|
||||
<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>
|
||||
<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="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">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
|
||||
>
|
||||
{t('buttons.proceedToNext')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-black px-8 py-4 text-lg transition-all duration-500"
|
||||
>
|
||||
{t('buttons.proceedToNext')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -511,6 +514,14 @@ const Index = () => {
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<GameBackground shouldStartAudio={shouldStartAudio} />
|
||||
<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="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">
|
||||
|
||||
Загрузка…
x
Ссылка в новой задаче
Block a user