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:
@@ -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 />,
|
||||
|
||||
@@ -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;
|
||||
@@ -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(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user