c1cc70eeb9
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*), localStorage keys, Docker/CI image paths, database names, and documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
201 lines
7.4 KiB
TypeScript
201 lines
7.4 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { useGameStore } from '@/store';
|
|
import { ConfirmModal } from '@/components/common/ConfirmModal';
|
|
import { getTokenPayload, isRegistered, isAdmin } from '@/lib/api';
|
|
|
|
export function SettingsPage() {
|
|
const settings = useGameStore((s) => s.meta.settings);
|
|
const companyName = useGameStore((s) => s.meta.companyName);
|
|
const updateState = useGameStore((s) => s.updateState);
|
|
const addNotification = useGameStore((s) => s.addNotification);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
|
const [importData, setImportData] = useState<{ data: unknown; name: string } | null>(null);
|
|
|
|
const toggleSound = () => {
|
|
updateState({ meta: { ...useGameStore.getState().meta, settings: { ...settings, soundEnabled: !settings.soundEnabled } } });
|
|
};
|
|
|
|
const setMusicVolume = (v: number) => {
|
|
updateState({ meta: { ...useGameStore.getState().meta, settings: { ...settings, musicVolume: v } } });
|
|
};
|
|
|
|
const handleReset = () => {
|
|
localStorage.removeItem('token-empire-save');
|
|
window.location.reload();
|
|
};
|
|
|
|
const handleExport = () => {
|
|
const state = useGameStore.getState();
|
|
const { activePage, notifications, ...gameState } = state;
|
|
const blob = new Blob([JSON.stringify(gameState)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `token-empire-${companyName.replace(/\s+/g, '-').toLowerCase()}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.target?.result as string);
|
|
if (!data.meta?.companyName) {
|
|
addNotification({ title: 'Import Failed', message: 'Invalid save file: missing company data.', type: 'danger', tick: useGameStore.getState().meta.tickCount });
|
|
return;
|
|
}
|
|
setImportData({ data, name: data.meta.companyName });
|
|
} catch {
|
|
addNotification({ title: 'Import Failed', message: 'Could not read save file. Make sure it is a valid Token Empire export.', type: 'danger', tick: useGameStore.getState().meta.tickCount });
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
};
|
|
|
|
const confirmImport = () => {
|
|
if (!importData) return;
|
|
localStorage.setItem('token-empire-save', JSON.stringify({ state: importData.data }));
|
|
window.location.reload();
|
|
};
|
|
|
|
const payload = getTokenPayload();
|
|
const registered = isRegistered();
|
|
const admin = isAdmin();
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-2xl">
|
|
<h2 className="text-2xl font-bold">Settings</h2>
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
|
<h3 className="font-semibold">Account</h3>
|
|
{registered ? (
|
|
<div className="space-y-2">
|
|
{payload?.email && (
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm">Email</div>
|
|
<div className="text-xs text-surface-400">{payload.email}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{payload?.username && (
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm">Username</div>
|
|
<div className="text-xs text-surface-400">{payload.username}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{admin && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-accent/20 text-accent font-medium">Admin</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-surface-400">Playing as guest.</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
|
<h3 className="font-semibold">Game</h3>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm">Sound Effects</div>
|
|
<div className="text-xs text-surface-400">Play UI sounds and notifications</div>
|
|
</div>
|
|
<ToggleSwitch checked={settings.soundEnabled} onChange={toggleSound} />
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm">Music Volume</div>
|
|
<div className="text-xs text-surface-400">Background music level</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={settings.musicVolume * 100}
|
|
onChange={(e) => setMusicVolume(Number(e.target.value) / 100)}
|
|
className="w-32 accent-accent"
|
|
/>
|
|
<span className="text-sm font-mono text-surface-400 w-8 text-right">{Math.round(settings.musicVolume * 100)}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
|
<h3 className="font-semibold">Save Data</h3>
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleExport}
|
|
className="px-4 py-2 rounded bg-surface-800 hover:bg-surface-700 border border-surface-600 text-sm"
|
|
>
|
|
Export Save
|
|
</button>
|
|
<button
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="px-4 py-2 rounded bg-surface-800 hover:bg-surface-700 border border-surface-600 text-sm"
|
|
>
|
|
Import Save
|
|
</button>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".json"
|
|
onChange={handleImport}
|
|
className="hidden"
|
|
/>
|
|
<button
|
|
onClick={() => setShowResetConfirm(true)}
|
|
className="px-4 py-2 rounded bg-danger/20 hover:bg-danger/30 border border-danger/50 text-danger text-sm"
|
|
>
|
|
Reset Progress
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{showResetConfirm && (
|
|
<ConfirmModal
|
|
title="Reset All Progress"
|
|
message="This will permanently delete your save data and start a new game. This cannot be undone."
|
|
confirmLabel="Reset Everything"
|
|
danger
|
|
onConfirm={handleReset}
|
|
onCancel={() => setShowResetConfirm(false)}
|
|
/>
|
|
)}
|
|
|
|
{importData && (
|
|
<ConfirmModal
|
|
title="Import Save"
|
|
message={`Import save for "${importData.name}"? This will replace your current game.`}
|
|
confirmLabel="Import"
|
|
onConfirm={confirmImport}
|
|
onCancel={() => setImportData(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: () => void }) {
|
|
return (
|
|
<button
|
|
onClick={onChange}
|
|
className={`w-10 h-6 rounded-full transition-colors ${checked ? 'bg-accent' : 'bg-surface-600'}`}
|
|
>
|
|
<div className={`w-4 h-4 bg-white rounded-full transition-transform mx-1 mt-1 ${checked ? 'translate-x-4' : ''}`} />
|
|
</button>
|
|
);
|
|
}
|