Add DemoPage route and integrate appController in server

- Introduced a new DemoPage component and added a corresponding route for it in the client application.
- Integrated appController into the server to manage application-related functionalities.
This commit is contained in:
2025-12-15 16:28:48 +05:00
parent 36a7f79c86
commit 2a58a47077
4 changed files with 237 additions and 0 deletions
+5
View File
@@ -14,6 +14,7 @@ import PopupContainer from "./components/PopupContainer";
import ToastsContainer from "./components/toasts/ToastsContainer";
import TestPage from "./pages/TestPage";
import SessionPage from "./pages/SessionPage";
import DemoPage from "./pages/DemoPage";
import { Toaster } from "react-hot-toast";
const router = createBrowserRouter([
@@ -45,6 +46,10 @@ const router = createBrowserRouter([
path: "/test",
element: <TestPage />,
},
{
path: "/demo/:appName",
element: <DemoPage />,
},
{
path: "/sessions/:id",
element: <SessionPage />,
+166
View File
@@ -0,0 +1,166 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { api } from "../lib/api";
import toast from "react-hot-toast";
import { extractErrorMessage } from "../lib/errorUtils";
import LoaderIcon from "../components/icons/LoaderIcon";
import WarningIcon from "../components/icons/WarningIcon";
import Button from "../components/ui/Button";
interface Session {
id: string;
appId: string;
userId: string | null;
mode: "stream" | "local";
status: "starting" | "started" | "ending" | "ended";
tier: "demo" | "prod";
serverId: string | null;
}
type PageState = "loading" | "error" | "not_found";
function DemoPage() {
const { appName } = useParams<{ appName: string }>();
const navigate = useNavigate();
const [state, setState] = useState<PageState>("loading");
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!appName) {
setState("not_found");
return;
}
const startDemo = async () => {
try {
// Один запрос: создаём сессию по имени приложения
const response = await api
.post(`apps/demo/${encodeURIComponent(appName)}`)
.json<{ session: Session }>();
// Перенаправляем на страницу сессии
navigate(`/sessions/${response.session.id}`, { replace: true });
} catch (err) {
console.error("Failed to start demo:", err);
// Проверяем, не найдено ли приложение
if (err && typeof err === "object" && "response" in err) {
const response = (err as { response: Response }).response;
if (response.status === 404) {
setState("not_found");
return;
}
}
const errorMessage = await extractErrorMessage(err);
setError(errorMessage);
setState("error");
toast.error(errorMessage, {
duration: 5000,
position: "top-center",
});
}
};
startDemo();
}, [appName, navigate]);
// Состояние загрузки
if (state === "loading") {
return (
<div className="flex flex-col gap-6 justify-center items-center min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
<div className="flex flex-col gap-4 items-center">
<div className="size-16 text-purple-400 animate-spin">
<LoaderIcon />
</div>
<div className="text-center">
<h1 className="text-2xl font-semibold text-white">
Запуск демо
</h1>
<p className="mt-2 text-purple-200/70">
Подготовка приложения...
</p>
</div>
</div>
{/* Декоративные элементы */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-purple-500/20 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl" />
</div>
</div>
);
}
// Приложение не найдено
if (state === "not_found") {
return (
<div className="flex flex-col gap-6 justify-center items-center min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
<div className="p-8 max-w-md bg-white/10 rounded-2xl shadow-2xl backdrop-blur-sm border border-white/10">
<div className="flex gap-4 items-start">
<div className="flex-shrink-0 p-3 bg-red-500/20 rounded-xl">
<div className="text-red-400 size-6">
<WarningIcon />
</div>
</div>
<div className="flex-1">
<h1 className="mb-2 text-xl font-semibold text-white">
Приложение не найдено
</h1>
<p className="mb-6 text-purple-200/70">
Приложение «{appName}» не существует или недоступно для демо-режима.
</p>
<Button variant="primary" onClick={() => navigate("/")}>
На главную
</Button>
</div>
</div>
</div>
</div>
);
}
// Ошибка создания сессии
if (state === "error") {
return (
<div className="flex flex-col gap-6 justify-center items-center min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
<div className="p-8 max-w-md bg-white/10 rounded-2xl shadow-2xl backdrop-blur-sm border border-white/10">
<div className="flex gap-4 items-start">
<div className="flex-shrink-0 p-3 bg-red-500/20 rounded-xl">
<div className="text-red-400 size-6">
<WarningIcon />
</div>
</div>
<div className="flex-1">
<h1 className="mb-2 text-xl font-semibold text-white">
Не удалось запустить демо
</h1>
<p className="mb-4 text-purple-200/70">
{error || "Произошла неизвестная ошибка"}
</p>
<div className="flex gap-3">
<Button
variant="primary"
onClick={() => window.location.reload()}
>
Попробовать снова
</Button>
<Button
variant="secondary"
onClick={() => navigate("/")}
>
На главную
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
return null;
}
export default DemoPage;