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;
+64
View File
@@ -0,0 +1,64 @@
import { Elysia, t } from "elysia";
import { eq } from "drizzle-orm";
import db from "../db";
import { apps } from "../db/schema/apps";
import { optionalAuthMiddleware } from "../middlewares/optionalAuth";
import { serverSessionService } from "../services/serverSession";
import { serverService } from "../services/server";
export const appController = new Elysia({ prefix: "/apps" })
.use(optionalAuthMiddleware)
// POST /apps/demo/:name - создать демо-сессию по имени приложения
.post(
"/demo/:name",
async ({ params, currentUser, guestId, status }) => {
const { name } = params;
// Найти приложение по имени
const app = await db.query.apps.findFirst({
where: eq(apps.name, name),
});
if (!app) {
return status(404, "Приложение не найдено");
}
// Проверяем наличие guestId для неавторизованных пользователей
if (!currentUser && !guestId) {
return status(400, "Для неавторизованных пользователей требуется Guest ID");
}
// Проверяем, что есть доступные demo-серверы
const demoServers = await serverService.findAvailableStreamServers("demo");
if (demoServers.length === 0) {
return status(
503,
"Нет доступных demo серверов. Пожалуйста, попробуйте позже."
);
}
// Создаём демо-сессию
try {
const newSession = await serverSessionService.create({
appId: app.id,
userId: currentUser?.id,
guestId: currentUser ? undefined : guestId || undefined,
mode: "stream",
tier: "demo",
});
return { session: newSession };
} catch (error) {
if (error instanceof Error) {
return status(503, error.message);
}
return status(500, "Не удалось создать сессию");
}
},
{
params: t.Object({
name: t.String(),
}),
}
);
+2
View File
@@ -6,6 +6,7 @@ import { companyController } from "./controllers/company";
import { branchController } from "./controllers/branch";
import { serverController } from "./controllers/server";
import { chatController } from "./controllers/chat";
import { appController } from "./controllers/app";
import { serverSessionService } from "./services/serverSession";
import { saveChatMessage } from "./services/chat";
import { Server } from "socket.io";
@@ -27,6 +28,7 @@ app.use(companyController);
app.use(branchController);
app.use(serverController);
app.use(chatController);
app.use(appController);
app.listen(process.env.PORT || 3000);