mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Migrate to React Router
This commit is contained in:
+32
-35
@@ -1,50 +1,47 @@
|
|||||||
import { MINIMUM_WIDTH } from "./pages/app/core/config";
|
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { isElectron } from "./pages/app/electron/electron";
|
|
||||||
import { ElectronTitleBar } from "./pages/app/ui/components/Electron";
|
import { ElectronTitleBar } from "./pages/app/ui/components/Electron";
|
||||||
import useWindowSize from "./pages/app/ui/hooks/useWindowSize";
|
|
||||||
import ChatScreen from "./pages/app/ui/screen/ChatScreen";
|
|
||||||
import DownloadAppScreen from "./pages/app/ui/screen/DownloadAppScreen";
|
|
||||||
import LoginScreen from "./pages/app/ui/screen/LoginScreen";
|
|
||||||
import RegisterScreen from "./pages/app/ui/screen/RegisterScreen";
|
|
||||||
import { useAppState } from "./pages/app/ui/state";
|
import { useAppState } from "./pages/app/ui/state";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import LoginPage from "./pages/LoginPage";
|
||||||
|
import RegisterPage from "./pages/RegisterPage";
|
||||||
|
import ChatPage from "./pages/ChatPage";
|
||||||
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
|
import ProtectedRoute from "./pages/ProtectedRoute";
|
||||||
|
import { isElectron } from "./pages/app/electron/electron";
|
||||||
|
import { MINIMUM_WIDTH } from "./pages/app/core/config";
|
||||||
|
import useWindowSize from "./pages/app/ui/hooks/useWindowSize";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { currentPage, restoreUserFromStorage } = useAppState();
|
const { restoreUserFromStorage } = useAppState();
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
|
const [authReady, setAuthReady] = useState(false);
|
||||||
|
|
||||||
// Restore user from localStorage on app initialization
|
// Restore user from localStorage on app initialization
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
restoreUserFromStorage();
|
restoreUserFromStorage().finally(() => {
|
||||||
|
setAuthReady(true);
|
||||||
|
});
|
||||||
}, [restoreUserFromStorage]);
|
}, [restoreUserFromStorage]);
|
||||||
|
|
||||||
if (!isElectron && width < MINIMUM_WIDTH) {
|
return authReady && (
|
||||||
return <DownloadAppScreen />
|
<BrowserRouter>
|
||||||
}
|
{!isElectron && width < MINIMUM_WIDTH && <Navigate to="/download-app" replace />}
|
||||||
|
|
||||||
let page = <LoginScreen />;
|
|
||||||
|
|
||||||
switch (currentPage) {
|
|
||||||
case "login": {
|
|
||||||
page = <LoginScreen />
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "register": {
|
|
||||||
page = <RegisterScreen />
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "chat": {
|
|
||||||
page = <ChatScreen />
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ElectronTitleBar />
|
<ElectronTitleBar />
|
||||||
<div id="main-wrapper">
|
<div id="main-wrapper">
|
||||||
{page}
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="/">
|
||||||
|
<Route path="chat" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ChatPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
</Route>
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './resources/css/style.scss';
|
import './pages/app/resources/css/style.scss';
|
||||||
import "mdui/mdui.css";
|
import "mdui/mdui.css";
|
||||||
|
|
||||||
import "./pages/app/utils/material";
|
import "./pages/app/utils/material";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { LeftPanel } from "./app/ui/components/chat/LeftPanel";
|
||||||
|
import { RightPanel } from "./app/ui/components/chat/RightPanel";
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
return (
|
||||||
|
<div id="chat-interface">
|
||||||
|
<div className="all-container">
|
||||||
|
<LeftPanel />
|
||||||
|
<RightPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export default function DownloadAppPage() {
|
||||||
|
return (
|
||||||
|
<div className="download-app-screen">
|
||||||
|
<div className="inner">
|
||||||
|
<h1>Чтобы пользоваться мессенджером, скачайте приложение</h1>
|
||||||
|
<p>
|
||||||
|
Этот сайт <b>не предназначен</b> для работы на маленьких экранах, поэтому
|
||||||
|
вам нужно скачать приложение мессенджера.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://github.com/denis0001-dev/FromChat-android/releases/latest">
|
||||||
|
<mdui-button>Скачать на GitHub</mdui-button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Если возникнут сложности или есть вопросы, нажмите кнопку!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://t.me/denis0001-dev">
|
||||||
|
<mdui-button>Написать в поддержку</mdui-button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useImmer } from "use-immer";
|
||||||
|
import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts";
|
||||||
|
import { AuthContainer, AuthHeader } from "./app/ui/components/Auth";
|
||||||
|
import type { ErrorResponse, LoginRequest, LoginResponse } from "./app/core/types";
|
||||||
|
import { ensureKeysOnLogin } from "./app/auth/crypto";
|
||||||
|
import { API_BASE_URL } from "./app/core/config";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import type { TextField } from "mdui/components/text-field";
|
||||||
|
import { useAppState } from "./app/ui/state";
|
||||||
|
import { MaterialTextField } from "./app/ui/components/core/TextField";
|
||||||
|
import { initialize, isSupported, startElectronReceiver, subscribe } from "./app/utils/push-notifications";
|
||||||
|
import { isElectron } from "./app/electron/electron";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
|
const setUser = useAppState(state => state.setUser);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function showAlert(type: AlertType, message: string) {
|
||||||
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernameElement = useRef<TextField>(null);
|
||||||
|
const passwordElement = useRef<TextField>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContainer>
|
||||||
|
<AuthHeader icon="login" title="Добро пожаловать!" subtitle="Войдите в свой аккаунт" />
|
||||||
|
<div className="auth-body">
|
||||||
|
<AlertsContainer alerts={alerts} />
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = usernameElement.current!.value.trim();
|
||||||
|
const password = passwordElement.current!.value.trim();
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
showAlert("danger", "Пожалуйста, заполните все поля");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request: LoginRequest = {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data: LoginResponse = await response.json();
|
||||||
|
// Store the JWT token first
|
||||||
|
setUser(data.token, data.user);
|
||||||
|
|
||||||
|
// Setup keys with the token we just received
|
||||||
|
try {
|
||||||
|
await ensureKeysOnLogin(password, data.token);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Key setup failed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate("/chat");
|
||||||
|
|
||||||
|
// Initialize notifications
|
||||||
|
try {
|
||||||
|
if (isSupported()) {
|
||||||
|
const initialized = await initialize();
|
||||||
|
if (initialized) {
|
||||||
|
await subscribe(data.token);
|
||||||
|
|
||||||
|
// For Electron, start the notification receiver
|
||||||
|
if (isElectron) {
|
||||||
|
await startElectronReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Notifications enabled");
|
||||||
|
} else {
|
||||||
|
console.log("Notification permission denied");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Notifications not supported");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Notification setup failed:", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data: ErrorResponse = await response.json();
|
||||||
|
showAlert("danger", data.message || "Неверное имя пользователя или пароль");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showAlert("danger", "Ошибка соединения с сервером");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<MaterialTextField
|
||||||
|
label="Имя пользователя"
|
||||||
|
id="login-username"
|
||||||
|
name="username"
|
||||||
|
variant="outlined"
|
||||||
|
icon="person--filled"
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
ref={usernameElement} />
|
||||||
|
|
||||||
|
<MaterialTextField
|
||||||
|
label="Пароль"
|
||||||
|
id="login-password"
|
||||||
|
name="password"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
toggle-password
|
||||||
|
icon="password--filled"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
ref={passwordElement} />
|
||||||
|
|
||||||
|
<mdui-button type="submit">Войти</mdui-button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p>
|
||||||
|
Ещё нет аккаунта?
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="link"
|
||||||
|
onClick={() => navigate("/register")}>
|
||||||
|
Зарегистрируйтесь
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function NotFoundPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="not-found-page">
|
||||||
|
<div className="not-found-container">
|
||||||
|
<div className="not-found-content">
|
||||||
|
<div className="error-code">404</div>
|
||||||
|
<h1>Страница не найдена</h1>
|
||||||
|
<p>
|
||||||
|
К сожалению, запрашиваемая страница не существует или была перемещена.
|
||||||
|
</p>
|
||||||
|
<div className="not-found-actions">
|
||||||
|
<mdui-button
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
На главную
|
||||||
|
</mdui-button>
|
||||||
|
<mdui-button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
Назад
|
||||||
|
</mdui-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="not-found-illustration">
|
||||||
|
<mdui-icon name="search_off"></mdui-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAppState } from "./app/ui/state";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
|
const { user } = useAppState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user.authToken) {
|
||||||
|
navigate("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [user.authToken, user.currentUser, navigate]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { useImmer } from "use-immer";
|
||||||
|
import { AuthContainer, AuthHeader } from "./app/ui/components/Auth";
|
||||||
|
import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { TextField } from "mdui/components/text-field";
|
||||||
|
import type { ErrorResponse, RegisterRequest, LoginResponse } from "./app/core/types";
|
||||||
|
import { API_BASE_URL } from "./app/core/config";
|
||||||
|
import { useAppState } from "./app/ui/state";
|
||||||
|
import { MaterialTextField } from "./app/ui/components/core/TextField";
|
||||||
|
import { ensureKeysOnLogin } from "./app/auth/crypto";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
|
const setUser = useAppState(state => state.setUser);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function showAlert(type: AlertType, message: string) {
|
||||||
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernameElement = useRef<TextField>(null);
|
||||||
|
const passwordElement = useRef<TextField>(null);
|
||||||
|
const confirmPasswordElement = useRef<TextField>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContainer>
|
||||||
|
<AuthHeader icon="person_add" title="Регистрация" subtitle="Создайте новый аккаунт" />
|
||||||
|
<div className="auth-body">
|
||||||
|
<AlertsContainer alerts={alerts} />
|
||||||
|
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = usernameElement.current!.value.trim();
|
||||||
|
const password = passwordElement.current!.value.trim();
|
||||||
|
const confirmPassword = confirmPasswordElement.current!.value.trim();
|
||||||
|
|
||||||
|
if (!username || !password || !confirmPassword) {
|
||||||
|
showAlert("danger", "Пожалуйста, заполните все поля");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
showAlert("danger", "Пароли не совпадают");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.length < 3 || username.length > 20) {
|
||||||
|
showAlert("danger", "Имя пользователя должно быть от 3 до 20 символов");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 5 || password.length > 50) {
|
||||||
|
showAlert("danger", "Пароль должен быть от 5 до 50 символов");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request: RegisterRequest = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
confirm_password: confirmPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data: LoginResponse = await response.json();
|
||||||
|
// Store the JWT token first
|
||||||
|
setUser(data.token, data.user);
|
||||||
|
|
||||||
|
// Setup keys with the token we just received
|
||||||
|
try {
|
||||||
|
await ensureKeysOnLogin(password, data.token);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Key setup failed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate("/chat");
|
||||||
|
} else {
|
||||||
|
const data: ErrorResponse = await response.json();
|
||||||
|
showAlert("danger", data.message || "Ошибка при регистрации");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showAlert("danger", "Ошибка соединения с сервером");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<MaterialTextField
|
||||||
|
label="Имя пользователя"
|
||||||
|
id="register-username"
|
||||||
|
name="username"
|
||||||
|
variant="outlined"
|
||||||
|
icon="person--filled"
|
||||||
|
autocomplete="username"
|
||||||
|
maxlength={20}
|
||||||
|
counter
|
||||||
|
required
|
||||||
|
ref={usernameElement} />
|
||||||
|
<MaterialTextField
|
||||||
|
label="Пароль"
|
||||||
|
id="register-password"
|
||||||
|
name="password"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
toggle-password
|
||||||
|
icon="password--filled"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
ref={passwordElement} />
|
||||||
|
<MaterialTextField
|
||||||
|
label="Подтвердите пароль"
|
||||||
|
id="register-confirm-password"
|
||||||
|
name="confirm_password"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
toggle-password
|
||||||
|
icon="password--filled"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
ref={confirmPasswordElement} />
|
||||||
|
|
||||||
|
<mdui-button type="submit">Зарегистрироваться</mdui-button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p>
|
||||||
|
Уже есть аккаунт?
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
id="login-link"
|
||||||
|
className="link"
|
||||||
|
onClick={() => navigate("/login")}>
|
||||||
|
Войдите
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
.not-found-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3rem;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 3rem;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 6rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #667eea;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d3748;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #718096;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-illustration {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #667eea;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.not-found-container {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
@use "electron";
|
@use "electron";
|
||||||
@use "dialogs/reply";
|
@use "dialogs/reply";
|
||||||
@use "download-app";
|
@use "download-app";
|
||||||
|
@use "404" as not-found;
|
||||||
|
|
||||||
@use "lib/fonts/montserrat";
|
@use "lib/fonts/montserrat";
|
||||||
@use "lib/fonts/material-symbols";
|
@use "lib/fonts/material-symbols";
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import { API_BASE_URL } from "../../core/config";
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import type { TextField } from "mdui/components/text-field";
|
import type { TextField } from "mdui/components/text-field";
|
||||||
import { useAppState } from "../state";
|
import { useAppState } from "../state";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { MaterialTextField } from "../components/core/TextField";
|
import { MaterialTextField } from "../components/core/TextField";
|
||||||
import { initialize, isSupported, startElectronReceiver, subscribe } from "../../utils/push-notifications";
|
import { initialize, isSupported, startElectronReceiver, subscribe } from "../../utils/push-notifications";
|
||||||
import { isElectron } from "../../electron/electron";
|
import { isElectron } from "../../electron/electron";
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
const setCurrentPage = useAppState(state => state.setCurrentPage);
|
|
||||||
const setUser = useAppState(state => state.setUser);
|
const setUser = useAppState(state => state.setUser);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function showAlert(type: AlertType, message: string) {
|
function showAlert(type: AlertType, message: string) {
|
||||||
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
||||||
@@ -67,7 +68,7 @@ export default function LoginScreen() {
|
|||||||
console.error("Key setup failed:", e);
|
console.error("Key setup failed:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPage("chat");
|
navigate("/chat");
|
||||||
|
|
||||||
// Initialize notifications
|
// Initialize notifications
|
||||||
try {
|
try {
|
||||||
@@ -130,7 +131,7 @@ export default function LoginScreen() {
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="link"
|
className="link"
|
||||||
onClick={() => setCurrentPage("register")}>
|
onClick={() => navigate("/register")}>
|
||||||
Зарегистрируйтесь
|
Зарегистрируйтесь
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import { TextField } from "mdui/components/text-field";
|
|||||||
import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types";
|
import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types";
|
||||||
import { API_BASE_URL } from "../../core/config";
|
import { API_BASE_URL } from "../../core/config";
|
||||||
import { useAppState } from "../state";
|
import { useAppState } from "../state";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { MaterialTextField } from "../components/core/TextField";
|
import { MaterialTextField } from "../components/core/TextField";
|
||||||
import { ensureKeysOnLogin } from "../../auth/crypto";
|
import { ensureKeysOnLogin } from "../../auth/crypto";
|
||||||
|
|
||||||
export default function RegisterScreen() {
|
export default function RegisterScreen() {
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
const setCurrentPage = useAppState(state => state.setCurrentPage);
|
|
||||||
const setUser = useAppState(state => state.setUser);
|
const setUser = useAppState(state => state.setUser);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function showAlert(type: AlertType, message: string) {
|
function showAlert(type: AlertType, message: string) {
|
||||||
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
||||||
@@ -83,7 +84,7 @@ export default function RegisterScreen() {
|
|||||||
console.error("Key setup failed:", e);
|
console.error("Key setup failed:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPage("chat");
|
navigate("/chat");
|
||||||
} else {
|
} else {
|
||||||
const data: ErrorResponse = await response.json();
|
const data: ErrorResponse = await response.json();
|
||||||
showAlert("danger", data.message || "Ошибка при регистрации");
|
showAlert("danger", data.message || "Ошибка при регистрации");
|
||||||
@@ -136,7 +137,7 @@ export default function RegisterScreen() {
|
|||||||
href="#"
|
href="#"
|
||||||
id="login-link"
|
id="login-link"
|
||||||
className="link"
|
className="link"
|
||||||
onClick={() => setCurrentPage("login")}>
|
onClick={() => navigate("/login")}>
|
||||||
Войдите
|
Войдите
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { API_BASE_URL } from "../core/config";
|
|||||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications";
|
import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications";
|
||||||
import { isElectron } from "../electron/electron";
|
import { isElectron } from "../electron/electron";
|
||||||
|
|
||||||
type Page = "login" | "register" | "chat"
|
|
||||||
export type ChatTabs = "chats" | "channels" | "contacts" | "dms"
|
export type ChatTabs = "chats" | "channels" | "contacts" | "dms"
|
||||||
|
|
||||||
interface ActiveDM {
|
interface ActiveDM {
|
||||||
@@ -37,9 +36,6 @@ export interface UserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPage: Page;
|
|
||||||
setCurrentPage: (page: Page) => void;
|
|
||||||
|
|
||||||
// Chat state
|
// Chat state
|
||||||
chat: ChatState;
|
chat: ChatState;
|
||||||
addMessage: (message: Message) => void;
|
addMessage: (message: Message) => void;
|
||||||
@@ -60,13 +56,10 @@ interface AppState {
|
|||||||
user: UserState;
|
user: UserState;
|
||||||
setUser: (token: string, user: User) => void;
|
setUser: (token: string, user: User) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
restoreUserFromStorage: () => void;
|
restoreUserFromStorage: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppState = create<AppState>((set, get) => ({
|
export const useAppState = create<AppState>((set, get) => ({
|
||||||
currentPage: "login", // default page
|
|
||||||
setCurrentPage: (page: Page) => set({ currentPage: page }),
|
|
||||||
|
|
||||||
// Chat state
|
// Chat state
|
||||||
chat: {
|
chat: {
|
||||||
messages: [],
|
messages: [],
|
||||||
@@ -191,8 +184,7 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
user: {
|
user: {
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
authToken: null
|
authToken: null
|
||||||
},
|
}
|
||||||
currentPage: "login"
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
restoreUserFromStorage: async () => {
|
restoreUserFromStorage: async () => {
|
||||||
@@ -212,8 +204,7 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
user: {
|
user: {
|
||||||
currentUser: user,
|
currentUser: user,
|
||||||
authToken: token
|
authToken: token
|
||||||
},
|
}
|
||||||
currentPage: "chat"
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"mdui": "^2.1.4",
|
"mdui": "^2.1.4",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.9.3",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"use-immer": "^0.11.0",
|
"use-immer": "^0.11.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
|
|||||||
Reference in New Issue
Block a user