v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { useState } from "react";
|
||||
import { Navigate, useNavigate } from "react-router-dom";
|
||||
|
||||
import { ApiError } from "../api/client";
|
||||
import { Spinner } from "../components/Spinner";
|
||||
import { TelegramLoginButton } from "../components/TelegramLoginButton";
|
||||
import {
|
||||
useAuthConfig,
|
||||
useCreateDevUser,
|
||||
useDevUsers,
|
||||
useMe,
|
||||
useStubLogin,
|
||||
useTelegramLogin,
|
||||
} from "../hooks/auth";
|
||||
|
||||
export function LoginPage() {
|
||||
const { data: me, isLoading } = useMe();
|
||||
const { data: config } = useAuthConfig();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const methods = config?.methods ?? [];
|
||||
const hasTelegram = methods.includes("telegram");
|
||||
// Stub-вход показываем только в dev-сборке И если бэкенд его отдаёт (в проде
|
||||
// import.meta.env.DEV === false → блок вырезается из бандла).
|
||||
const showStub = import.meta.env.DEV && methods.includes("stub");
|
||||
|
||||
const telegram = useTelegramLogin();
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
if (me) return <Navigate to="/" replace />;
|
||||
|
||||
const onTelegram = async (user: Record<string, unknown>) => {
|
||||
setError(null);
|
||||
try {
|
||||
await telegram.mutateAsync(user);
|
||||
navigate("/", { replace: true });
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось войти через Telegram");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div className="app-main" style={{ paddingBottom: 24 }}>
|
||||
<h1 className="center" style={{ marginTop: 24 }}>
|
||||
Forbidden Stars
|
||||
</h1>
|
||||
<p className="muted center">Учёт партий</p>
|
||||
|
||||
{hasTelegram && (
|
||||
<div className="card">
|
||||
<h3>Вход через Telegram</h3>
|
||||
{config?.telegram_bot_username ? (
|
||||
<TelegramLoginButton
|
||||
botUsername={config.telegram_bot_username}
|
||||
onAuth={(u) => onTelegram(u as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
) : (
|
||||
<p className="muted small">
|
||||
Telegram не настроен — укажите TELEGRAM_BOT_USERNAME и TELEGRAM_BOT_TOKEN
|
||||
в .env (бот от @BotFather + /setdomain на ваш домен).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showStub && <DevLogin onError={setError} />}
|
||||
|
||||
{error && <p className="error-text center">{error}</p>}
|
||||
|
||||
<p className="center" style={{ marginTop: 40 }}>
|
||||
<button
|
||||
className="btn btn-ghost small"
|
||||
onClick={() => navigate("/admin/login")}
|
||||
style={{ opacity: 0.4 }}
|
||||
>
|
||||
·
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// DEV-вход по нику. Вынесен в отдельный компонент; импортируется только в dev-ветке
|
||||
// LoginPage, поэтому в прод-сборке (import.meta.env.DEV=false) удаляется тришейкингом.
|
||||
function DevLogin({ onError }: { onError: (msg: string | null) => void }) {
|
||||
const { data: devUsers } = useDevUsers(true);
|
||||
const stubLogin = useStubLogin();
|
||||
const createUser = useCreateDevUser();
|
||||
const navigate = useNavigate();
|
||||
const [nickname, setNickname] = useState("");
|
||||
|
||||
const doLogin = async (nick: string) => {
|
||||
onError(null);
|
||||
try {
|
||||
await stubLogin.mutateAsync(nick);
|
||||
navigate("/", { replace: true });
|
||||
} catch (e) {
|
||||
onError(e instanceof ApiError ? e.message : "Не удалось войти");
|
||||
}
|
||||
};
|
||||
|
||||
const onCreate = async () => {
|
||||
const nick = nickname.trim();
|
||||
if (!nick) return;
|
||||
try {
|
||||
await createUser.mutateAsync(nick);
|
||||
await doLogin(nick);
|
||||
} catch (e) {
|
||||
onError(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h3>Тестовый вход (dev)</h3>
|
||||
<div className="field">
|
||||
<label className="label">Новый игрок</label>
|
||||
<div className="row">
|
||||
<input placeholder="Ваш никнейм" value={nickname} onChange={(e) => setNickname(e.target.value)} />
|
||||
<button className="btn btn-primary" onClick={onCreate} disabled={createUser.isPending}>
|
||||
Войти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{devUsers && devUsers.length > 0 && (
|
||||
<>
|
||||
<div className="label">Или выберите существующего:</div>
|
||||
<div className="stack">
|
||||
{devUsers.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
className="btn"
|
||||
disabled={!u.is_active}
|
||||
title={u.is_active ? undefined : "Аккаунт отключён администратором"}
|
||||
style={u.is_active ? undefined : { opacity: 0.45, color: "var(--text-muted)" }}
|
||||
onClick={() => doLogin(u.nickname)}
|
||||
>
|
||||
{u.nickname}
|
||||
{!u.is_active && <span className="small"> · отключён</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user