diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d403a77..275b2e0 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,50 +1,47 @@
-import { MINIMUM_WIDTH } from "./pages/app/core/config";
-import { isElectron } from "./pages/app/electron/electron";
+import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
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 { 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() {
- const { currentPage, restoreUserFromStorage } = useAppState();
+ const { restoreUserFromStorage } = useAppState();
const { width } = useWindowSize();
+ const [authReady, setAuthReady] = useState(false);
// Restore user from localStorage on app initialization
useEffect(() => {
- restoreUserFromStorage();
+ restoreUserFromStorage().finally(() => {
+ setAuthReady(true);
+ });
}, [restoreUserFromStorage]);
- if (!isElectron && width < MINIMUM_WIDTH) {
- return
- }
-
- let page = ;
-
- switch (currentPage) {
- case "login": {
- page =
- break;
- }
- case "register": {
- page =
- break;
- }
- case "chat": {
- page =
- break;
- }
- }
-
- return (
- <>
+ return authReady && (
+
+ {!isElectron && width < MINIMUM_WIDTH && }
- {page}
+
+ } />
+ } />
+ } />
+
+
+
+
+ } />
+
+ } />
+
- >
+
)
}
\ No newline at end of file
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 572bfd4..2296930 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -5,7 +5,7 @@
* @version 1.0.0
*/
-import './resources/css/style.scss';
+import './pages/app/resources/css/style.scss';
import "mdui/mdui.css";
import "./pages/app/utils/material";
diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx
new file mode 100644
index 0000000..0989a42
--- /dev/null
+++ b/frontend/src/pages/ChatPage.tsx
@@ -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 (
+
+ );
+}
diff --git a/frontend/src/pages/DownloadAppPage.tsx b/frontend/src/pages/DownloadAppPage.tsx
new file mode 100644
index 0000000..3e8ec2e
--- /dev/null
+++ b/frontend/src/pages/DownloadAppPage.tsx
@@ -0,0 +1,25 @@
+export default function DownloadAppPage() {
+ return (
+
+
+
Чтобы пользоваться мессенджером, скачайте приложение
+
+ Этот сайт не предназначен для работы на маленьких экранах, поэтому
+ вам нужно скачать приложение мессенджера.
+
+
+
+ Скачать на GitHub
+
+
+
+ Если возникнут сложности или есть вопросы, нажмите кнопку!
+
+
+
+ Написать в поддержку
+
+
+
+ )
+}
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..0fdb5b4
--- /dev/null
+++ b/frontend/src/pages/LoginPage.tsx
@@ -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([]);
+ 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(null);
+ const passwordElement = useRef(null);
+
+ return (
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..907119f
--- /dev/null
+++ b/frontend/src/pages/NotFoundPage.tsx
@@ -0,0 +1,36 @@
+import { useNavigate } from "react-router-dom";
+
+export default function NotFoundPage() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
404
+
Страница не найдена
+
+ К сожалению, запрашиваемая страница не существует или была перемещена.
+
+
+ navigate("/")}
+ >
+ На главную
+
+ navigate(-1)}
+ >
+ Назад
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/ProtectedRoute.tsx b/frontend/src/pages/ProtectedRoute.tsx
new file mode 100644
index 0000000..40c4652
--- /dev/null
+++ b/frontend/src/pages/ProtectedRoute.tsx
@@ -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}>;
+}
diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx
new file mode 100644
index 0000000..2e437e1
--- /dev/null
+++ b/frontend/src/pages/RegisterPage.tsx
@@ -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([]);
+ 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(null);
+ const passwordElement = useRef(null);
+ const confirmPasswordElement = useRef(null);
+
+ return (
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/app/resources/css/_404.scss b/frontend/src/pages/app/resources/css/_404.scss
new file mode 100644
index 0000000..d91252f
--- /dev/null
+++ b/frontend/src/pages/app/resources/css/_404.scss
@@ -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;
+ }
+}
diff --git a/frontend/src/pages/app/resources/css/style.scss b/frontend/src/pages/app/resources/css/style.scss
index 4cfe673..3e06041 100644
--- a/frontend/src/pages/app/resources/css/style.scss
+++ b/frontend/src/pages/app/resources/css/style.scss
@@ -10,6 +10,7 @@
@use "electron";
@use "dialogs/reply";
@use "download-app";
+@use "404" as not-found;
@use "lib/fonts/montserrat";
@use "lib/fonts/material-symbols";
diff --git a/frontend/src/pages/app/ui/screen/LoginScreen.tsx b/frontend/src/pages/app/ui/screen/LoginScreen.tsx
index 06df52f..168b0d8 100644
--- a/frontend/src/pages/app/ui/screen/LoginScreen.tsx
+++ b/frontend/src/pages/app/ui/screen/LoginScreen.tsx
@@ -7,14 +7,15 @@ import { API_BASE_URL } from "../../core/config";
import { useRef } from "react";
import type { TextField } from "mdui/components/text-field";
import { useAppState } from "../state";
+import { useNavigate } from "react-router-dom";
import { MaterialTextField } from "../components/core/TextField";
import { initialize, isSupported, startElectronReceiver, subscribe } from "../../utils/push-notifications";
import { isElectron } from "../../electron/electron";
export default function LoginScreen() {
const [alerts, updateAlerts] = useImmer([]);
- const setCurrentPage = useAppState(state => state.setCurrentPage);
const setUser = useAppState(state => state.setUser);
+ const navigate = useNavigate();
function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
@@ -67,7 +68,7 @@ export default function LoginScreen() {
console.error("Key setup failed:", e);
}
- setCurrentPage("chat");
+ navigate("/chat");
// Initialize notifications
try {
@@ -130,7 +131,7 @@ export default function LoginScreen() {
setCurrentPage("register")}>
+ onClick={() => navigate("/register")}>
Зарегистрируйтесь
diff --git a/frontend/src/pages/app/ui/screen/RegisterScreen.tsx b/frontend/src/pages/app/ui/screen/RegisterScreen.tsx
index a93efd3..ec16814 100644
--- a/frontend/src/pages/app/ui/screen/RegisterScreen.tsx
+++ b/frontend/src/pages/app/ui/screen/RegisterScreen.tsx
@@ -7,13 +7,14 @@ import { TextField } from "mdui/components/text-field";
import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types";
import { API_BASE_URL } from "../../core/config";
import { useAppState } from "../state";
+import { useNavigate } from "react-router-dom";
import { MaterialTextField } from "../components/core/TextField";
import { ensureKeysOnLogin } from "../../auth/crypto";
export default function RegisterScreen() {
const [alerts, updateAlerts] = useImmer([]);
- const setCurrentPage = useAppState(state => state.setCurrentPage);
const setUser = useAppState(state => state.setUser);
+ const navigate = useNavigate();
function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
@@ -83,7 +84,7 @@ export default function RegisterScreen() {
console.error("Key setup failed:", e);
}
- setCurrentPage("chat");
+ navigate("/chat");
} else {
const data: ErrorResponse = await response.json();
showAlert("danger", data.message || "Ошибка при регистрации");
@@ -136,7 +137,7 @@ export default function RegisterScreen() {
href="#"
id="login-link"
className="link"
- onClick={() => setCurrentPage("login")}>
+ onClick={() => navigate("/login")}>
Войдите
diff --git a/frontend/src/pages/app/ui/state.ts b/frontend/src/pages/app/ui/state.ts
index bf31eb4..c5db87e 100644
--- a/frontend/src/pages/app/ui/state.ts
+++ b/frontend/src/pages/app/ui/state.ts
@@ -10,7 +10,6 @@ import { API_BASE_URL } from "../core/config";
import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications";
import { isElectron } from "../electron/electron";
-type Page = "login" | "register" | "chat"
export type ChatTabs = "chats" | "channels" | "contacts" | "dms"
interface ActiveDM {
@@ -37,9 +36,6 @@ export interface UserState {
}
interface AppState {
- currentPage: Page;
- setCurrentPage: (page: Page) => void;
-
// Chat state
chat: ChatState;
addMessage: (message: Message) => void;
@@ -60,13 +56,10 @@ interface AppState {
user: UserState;
setUser: (token: string, user: User) => void;
logout: () => void;
- restoreUserFromStorage: () => void;
+ restoreUserFromStorage: () => Promise;
}
export const useAppState = create((set, get) => ({
- currentPage: "login", // default page
- setCurrentPage: (page: Page) => set({ currentPage: page }),
-
// Chat state
chat: {
messages: [],
@@ -191,8 +184,7 @@ export const useAppState = create((set, get) => ({
user: {
currentUser: null,
authToken: null
- },
- currentPage: "login"
+ }
}));
},
restoreUserFromStorage: async () => {
@@ -212,8 +204,7 @@ export const useAppState = create((set, get) => ({
user: {
currentUser: user,
authToken: token
- },
- currentPage: "chat"
+ }
}));
try {
diff --git a/package.json b/package.json
index 7a1dd91..ab204c5 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"mdui": "^2.1.4",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.3",
"tweetnacl": "^1.0.3",
"use-immer": "^0.11.0",
"zustand": "^5.0.8"