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 ToastsContainer from "./components/toasts/ToastsContainer";
|
||||||
import TestPage from "./pages/TestPage";
|
import TestPage from "./pages/TestPage";
|
||||||
import SessionPage from "./pages/SessionPage";
|
import SessionPage from "./pages/SessionPage";
|
||||||
|
import DemoPage from "./pages/DemoPage";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@@ -45,6 +46,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/test",
|
path: "/test",
|
||||||
element: <TestPage />,
|
element: <TestPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/demo/:appName",
|
||||||
|
element: <DemoPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/sessions/:id",
|
path: "/sessions/:id",
|
||||||
element: <SessionPage />,
|
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 { branchController } from "./controllers/branch";
|
||||||
import { serverController } from "./controllers/server";
|
import { serverController } from "./controllers/server";
|
||||||
import { chatController } from "./controllers/chat";
|
import { chatController } from "./controllers/chat";
|
||||||
|
import { appController } from "./controllers/app";
|
||||||
import { serverSessionService } from "./services/serverSession";
|
import { serverSessionService } from "./services/serverSession";
|
||||||
import { saveChatMessage } from "./services/chat";
|
import { saveChatMessage } from "./services/chat";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
@@ -27,6 +28,7 @@ app.use(companyController);
|
|||||||
app.use(branchController);
|
app.use(branchController);
|
||||||
app.use(serverController);
|
app.use(serverController);
|
||||||
app.use(chatController);
|
app.use(chatController);
|
||||||
|
app.use(appController);
|
||||||
|
|
||||||
app.listen(process.env.PORT || 3000);
|
app.listen(process.env.PORT || 3000);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user