Files
AIHostingTycoon/apps/web/src/pages/SettingsPage.tsx
T
josh c1cc70eeb9
Balance Check / balance-simulation (pull_request) Successful in 38s
Balance Check / multi-run-balance (pull_request) Successful in 13m44s
Rename AI Tycoon to Token Empire across entire codebase
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>
2026-04-27 21:04:07 -04:00

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>
);
}