Files
ai-rpg/frontend/src/pages/DashboardPage.tsx

154 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-06-19 11:28:04 +03:00
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { Plus, BookOpen, Play, Pencil, Trash2 } from "lucide-react";
import { worldsApi, sessionsApi } from "@/api";
import type { World } from "@/types";
import { Button } from "@/components/ui/Button";
import { Card, CardBody } from "@/components/ui/Card";
export function DashboardPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const [worlds, setWorlds] = useState<World[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const load = async () => {
try {
setLoading(true);
const ws = await worldsApi.list();
setWorlds(ws);
} catch (err: any) {
setError(err.response?.data?.detail || t("errors.unknown"));
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const handleDelete = async (id: string) => {
if (!confirm(t("common.delete") + "?")) return;
try {
await worldsApi.delete(id);
await load();
} catch (err: any) {
setError(err.response?.data?.detail || t("errors.unknown"));
}
};
const handlePlay = async (world: World) => {
try {
const session = await sessionsApi.create(world.id);
navigate(`/sessions/${session.id}`);
} catch (err: any) {
setError(err.response?.data?.detail || t("errors.unknown"));
}
};
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-serif text-ink-100">{t("worlds.title")}</h1>
<Link to="/worlds/new">
<Button>
<Plus size={16} className="mr-1" />
{t("worlds.new")}
</Button>
</Link>
</div>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
{loading ? (
<p className="text-ink-400">{t("common.loading")}</p>
) : worlds.length === 0 ? (
<Card>
<CardBody className="text-center py-12">
<BookOpen className="mx-auto text-ink-600 mb-3" size={32} />
<p className="text-ink-400 mb-4">{t("worlds.empty")}</p>
<Link to="/worlds/new">
<Button>
<Plus size={16} className="mr-1" />
{t("worlds.new")}
</Button>
</Link>
</CardBody>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{worlds.map((w) => (
<Card key={w.id}>
<CardBody>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-ink-100 mb-1">{w.name}</h3>
<div className="flex items-center gap-2 mb-2">
<StatusBadge status={w.status} t={t} />
<span className="text-xs text-ink-500">{w.language.toUpperCase()}</span>
{w.current_time && (
<span className="text-xs text-ink-500">· {w.current_time}</span>
)}
</div>
<p className="text-xs text-ink-400 line-clamp-2">
{w.definition?.setting_description?.slice(0, 160) || "—"}
</p>
</div>
</div>
<div className="flex gap-2 mt-4">
{w.status === "draft" ? (
<Link to={`/worlds/${w.id}/edit`}>
<Button size="sm" variant="secondary">
<Pencil size={12} className="mr-1" />
{t("worlds.edit")}
</Button>
</Link>
) : (
<Button size="sm" onClick={() => handlePlay(w)}>
<Play size={12} className="mr-1" />
{t("worlds.start")}
</Button>
)}
<Link to={`/worlds/${w.id}/edit`}>
<Button size="sm" variant="ghost">
<Pencil size={12} className="mr-1" />
{t("worlds.edit")}
</Button>
</Link>
<Button size="sm" variant="ghost" onClick={() => handleDelete(w.id)}>
<Trash2 size={12} className="mr-1" />
{t("worlds.delete")}
</Button>
</div>
</CardBody>
</Card>
))}
</div>
)}
</div>
);
}
function StatusBadge({ status, t }: { status: string; t: any }) {
const colors: Record<string, string> = {
draft: "bg-yellow-500/10 text-yellow-400 border-yellow-500/30",
ready: "bg-blue-500/10 text-blue-400 border-blue-500/30",
active: "bg-green-500/10 text-green-400 border-green-500/30",
archived: "bg-ink-500/10 text-ink-400 border-ink-500/30",
};
const labels: Record<string, string> = {
draft: t("worlds.status_draft"),
ready: t("worlds.status_ready"),
active: t("worlds.status_active"),
archived: t("worlds.status_archived"),
};
return (
<span className={`text-xs px-2 py-0.5 rounded border ${colors[status] || colors.draft}`}>
{labels[status] || status}
</span>
);
}