From 88fb7a18888e75b2568699d461507e213243e96d Mon Sep 17 00:00:00 2001 From: NikitaTorbenko <2015nekitciti@gmail.com> Date: Fri, 3 Apr 2026 21:49:39 +0300 Subject: [PATCH] feat: add bw themes --- .../src/app/providers/routing/routing.tsx | 13 +- .../src/modules/auth/store/useAuthStore.ts | 97 +++++++ frontend/src/modules/auth/types/auth.types.ts | 31 ++ .../modules/theme-bw/stores/theme.store.ts | 54 ++++ .../src/modules/theme-bw/ui/ThemeToggle.tsx | 21 ++ frontend/src/pages/auth.page.tsx | 130 +++++++++ frontend/src/pages/register.page.tsx | 203 +++++++++++++ frontend/src/shared/layouts/DefaultLayout.tsx | 55 ++++ frontend/src/shared/styles/themes.css | 273 +++--------------- 9 files changed, 642 insertions(+), 235 deletions(-) create mode 100644 frontend/src/modules/auth/store/useAuthStore.ts create mode 100644 frontend/src/modules/auth/types/auth.types.ts create mode 100644 frontend/src/modules/theme-bw/stores/theme.store.ts create mode 100644 frontend/src/modules/theme-bw/ui/ThemeToggle.tsx create mode 100644 frontend/src/pages/auth.page.tsx create mode 100644 frontend/src/pages/register.page.tsx create mode 100644 frontend/src/shared/layouts/DefaultLayout.tsx diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx index 62bb7ab..13d3c47 100644 --- a/frontend/src/app/providers/routing/routing.tsx +++ b/frontend/src/app/providers/routing/routing.tsx @@ -2,6 +2,9 @@ import { Suspense } from "react"; import { Routes as ReactRoutes, Route, Navigate } from "react-router-dom"; import { HomePage } from "@/pages/home.page"; import { ThemesPage } from "@/pages/themes.page"; +import { AuthPage } from "@/pages/auth.page"; +import { RegisterPage } from "@/pages/register.page"; +import { DefaultLayout } from "@/shared/layouts/DefaultLayout"; export const Routing = () => { return ( @@ -13,10 +16,14 @@ export const Routing = () => { } > - } /> - } /> + }> + } /> + } /> + } /> + } /> - } /> + } /> + ); diff --git a/frontend/src/modules/auth/store/useAuthStore.ts b/frontend/src/modules/auth/store/useAuthStore.ts new file mode 100644 index 0000000..ed22746 --- /dev/null +++ b/frontend/src/modules/auth/store/useAuthStore.ts @@ -0,0 +1,97 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import type { + AuthState, + LoginCredentials, + RegisterData, + User, +} from "../types/auth.types"; + +// Mock API functions - замените на реальные запросы +const mockLogin = async ( + credentials: LoginCredentials, +): Promise<{ user: User; token: string }> => { + // Имитация API запроса + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (credentials.login === "admin" && credentials.password === "admin") { + return { + user: { + id: "1", + login: credentials.login, + firstName: "Admin", + lastName: "User", + }, + token: "mock-jwt-token", + }; + } + throw new Error("Invalid credentials"); +}; + +const mockRegister = async ( + data: RegisterData, +): Promise<{ user: User; token: string }> => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return { + user: { + id: Date.now().toString(), + login: data.login, + firstName: data.firstName, + lastName: data.lastName, + }, + token: "mock-jwt-token", + }; +}; + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + isLoading: false, + error: null, + + login: async (credentials: LoginCredentials) => { + set({ isLoading: true, error: null }); + try { + const { user, token } = await mockLogin(credentials); + set({ user, token, isLoading: false }); + } catch (error) { + set({ + error: error instanceof Error ? error.message : "Login failed", + isLoading: false, + }); + throw error; + } + }, + + register: async (data: RegisterData) => { + set({ isLoading: true, error: null }); + try { + const { user, token } = await mockRegister(data); + set({ user, token, isLoading: false }); + } catch (error) { + set({ + error: + error instanceof Error ? error.message : "Registration failed", + isLoading: false, + }); + throw error; + } + }, + + logout: () => { + set({ user: null, token: null, error: null }); + }, + + clearError: () => { + set({ error: null }); + }, + }), + { + name: "auth-storage", + partialize: (state) => ({ token: state.token, user: state.user }), + }, + ), +); diff --git a/frontend/src/modules/auth/types/auth.types.ts b/frontend/src/modules/auth/types/auth.types.ts new file mode 100644 index 0000000..08de1ea --- /dev/null +++ b/frontend/src/modules/auth/types/auth.types.ts @@ -0,0 +1,31 @@ +export interface LoginCredentials { + login: string; + password: string; +} + +export interface RegisterData { + login: string; + password: string; + firstName: string; + lastName: string; +} + +export interface User { + id: string; + login: string; + firstName: string; + lastName: string; +} + +export interface AuthState { + user: User | null; + token: string | null; + isLoading: boolean; + error: string | null; + login: (credentials: LoginCredentials) => Promise; + register: (data: RegisterData) => Promise; + logout: () => void; + clearError: () => void; +} + +export type Theme = "light" | "dark"; diff --git a/frontend/src/modules/theme-bw/stores/theme.store.ts b/frontend/src/modules/theme-bw/stores/theme.store.ts new file mode 100644 index 0000000..10cffdc --- /dev/null +++ b/frontend/src/modules/theme-bw/stores/theme.store.ts @@ -0,0 +1,54 @@ +import type { Theme } from "@/modules/auth/types/auth.types"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface ThemeState { + theme: Theme; + toggleTheme: () => void; + setTheme: (theme: Theme) => void; +} + +export const useThemeStore = create()( + persist( + (set) => ({ + theme: "dark", + toggleTheme: () => { + set((state) => { + const newTheme = state.theme === "dark" ? "light" : "dark"; + // Применяем класс к documentElement + if (newTheme === "dark") { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + return { theme: newTheme }; + }); + }, + setTheme: (theme: Theme) => { + if (theme === "dark") { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + set({ theme }); + }, + }), + { + name: "theme-storage", + }, + ), +); + +// Инициализация темы при загрузке +if (typeof window !== "undefined") { + const storedTheme = localStorage.getItem("theme-storage"); + if (storedTheme) { + const { state } = JSON.parse(storedTheme); + if (state.theme === "dark") { + document.documentElement.classList.add("dark"); + } + } else { + // По умолчанию dark + document.documentElement.classList.add("dark"); + } +} diff --git a/frontend/src/modules/theme-bw/ui/ThemeToggle.tsx b/frontend/src/modules/theme-bw/ui/ThemeToggle.tsx new file mode 100644 index 0000000..75019a6 --- /dev/null +++ b/frontend/src/modules/theme-bw/ui/ThemeToggle.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { useThemeStore } from "../stores/theme.store"; +import { FiSun, FiMoon } from "react-icons/fi"; + +export const ThemeToggle: React.FC = () => { + const { theme, toggleTheme } = useThemeStore(); + + return ( + + {theme === "dark" ? ( + + ) : ( + + )} + + ); +}; diff --git a/frontend/src/pages/auth.page.tsx b/frontend/src/pages/auth.page.tsx new file mode 100644 index 0000000..d9f5cb6 --- /dev/null +++ b/frontend/src/pages/auth.page.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { FiUser, FiLock, FiLogIn } from "react-icons/fi"; +import { useAuthStore } from "@/modules/auth/store/useAuthStore"; + +export const AuthPage: React.FC = () => { + const navigate = useNavigate(); + const { login, isLoading, error, clearError, token } = useAuthStore(); + const [formData, setFormData] = useState({ + login: "", + password: "", + }); + + useEffect(() => { + if (token) { + navigate("/dashboard"); + } + }, [token, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await login(formData); + navigate("/dashboard"); + } catch (err) { + // Error is handled by store + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + if (error) clearError(); + }; + + return ( + + + {/* Card */} + + {/* Header */} + + + Welcome Back + + + Sign in to your account + + + + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Form */} + + + + Login + + + + + + + + + + Password + + + + + + + + + {isLoading ? ( + "Signing in..." + ) : ( + <> + + Sign In + > + )} + + + + {/* Footer */} + + + Don't have an account?{" "} + + Sign up + + + + + + + ); +}; diff --git a/frontend/src/pages/register.page.tsx b/frontend/src/pages/register.page.tsx new file mode 100644 index 0000000..b2b77a4 --- /dev/null +++ b/frontend/src/pages/register.page.tsx @@ -0,0 +1,203 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { FiUser, FiLock, FiUserPlus, FiMail } from "react-icons/fi"; +import { useAuthStore } from "@/modules/auth/store/useAuthStore"; + +export const RegisterPage: React.FC = () => { + const navigate = useNavigate(); + const { register, isLoading, error, clearError, token } = useAuthStore(); + const [formData, setFormData] = useState({ + login: "", + password: "", + confirmPassword: "", + firstName: "", + lastName: "", + }); + const [passwordError, setPasswordError] = useState(null); + + useEffect(() => { + if (token) { + navigate("/dashboard"); + } + }, [token, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (formData.password !== formData.confirmPassword) { + setPasswordError("Passwords do not match"); + return; + } + + setPasswordError(null); + + try { + await register({ + login: formData.login, + password: formData.password, + firstName: formData.firstName, + lastName: formData.lastName, + }); + navigate("/dashboard"); + } catch (err) { + // Error is handled by store + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + if (error) clearError(); + if (passwordError) setPasswordError(null); + }; + + return ( + + + {/* Card */} + + {/* Header */} + + + Create Account + + + Sign up to get started + + + + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Form */} + + + + + First Name + + + + + + + Last Name + + + + + + + + Login + + + + + + + + + + Password + + + + + + + + + + Confirm Password + + + + + + {passwordError && ( + + {passwordError} + + )} + + + + {isLoading ? ( + "Creating account..." + ) : ( + <> + + Sign Up + > + )} + + + + {/* Footer */} + + + Already have an account?{" "} + + Sign in + + + + + + + ); +}; diff --git a/frontend/src/shared/layouts/DefaultLayout.tsx b/frontend/src/shared/layouts/DefaultLayout.tsx new file mode 100644 index 0000000..76528bb --- /dev/null +++ b/frontend/src/shared/layouts/DefaultLayout.tsx @@ -0,0 +1,55 @@ +import { useAuthStore } from "@/modules/auth/store/useAuthStore"; +import { ThemeToggle } from "@/modules/theme-bw/ui/ThemeToggle"; +import React from "react"; +import { Outlet, useNavigate } from "react-router-dom"; + +interface DefaultLayoutProps { + children?: React.ReactNode; +} + +export const DefaultLayout: React.FC = ({ children }) => { + const { user, logout } = useAuthStore(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate("/login"); + }; + + return ( + + {/* Header */} + + + + {/* Logo */} + + HellreigN + + + {/* Right side */} + + + {user && ( + + + {user.firstName} {user.lastName} + + + Logout + + + )} + + + + + + {/* Main content */} + {children || } + + ); +}; diff --git a/frontend/src/shared/styles/themes.css b/frontend/src/shared/styles/themes.css index 6a8db1f..49c5323 100644 --- a/frontend/src/shared/styles/themes.css +++ b/frontend/src/shared/styles/themes.css @@ -1,175 +1,48 @@ -[data-theme="light"] { +@import "tailwindcss"; + +/* Кастомные темы для dark mode */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* Базовые стили */ +@layer base { + body { + @apply bg-white dark:bg-black text-gray-900 dark:text-white; + } +} + +/* Кастомные утилиты (опционально) */ +@layer utilities { + .transition-theme { + transition-property: background-color, border-color, color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; + } +} + +:root { --bg-primary: #ffffff; - --bg-secondary: #f8fafc; - --bg-tertiary: #f1f5f9; - --text-primary: #1e293b; - --text-secondary: #64748b; - --text-tertiary: #94a3b8; - --border-primary: #e2e8f0; - --border-secondary: #cbd5e1; - --accent-primary: #4f46e5; - --accent-secondary: #6366f1; - --accent-hover: #4338ca; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08), 0 1px 2px 0 rgba(0, 0, 0, 0.04); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --bg-secondary: #f5f5f5; + --text-primary: #000000; + --text-secondary: #333333; + --border: #e5e5e5; + --card-bg: #ffffff; + --input-bg: #ffffff; + --button-bg: #000000; + --button-text: #ffffff; + --button-hover: #333333; } [data-theme="dark"] { - --bg-primary: #0f172a; - --bg-secondary: #1e293b; - --bg-tertiary: #334155; - --text-primary: #f1f5f9; - --text-secondary: #cbd5e1; - --text-tertiary: #94a3b8; - --border-primary: #475569; - --border-secondary: #64748b; - --accent-primary: #5061fc; - --accent-secondary: #4866ff; - --accent-hover: #6366f1; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25), 0 1px 2px 0 rgba(0, 0, 0, 0.15); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -2px rgba(0, 0, 0, 0.15); -} - -[data-theme="nightowl"] { - --bg-primary: #011627; - --bg-secondary: #0d293e; - --bg-tertiary: #1d3b53; - --text-primary: #d6deeb; - --text-secondary: #b4c7e0; - --text-tertiary: #7c8da5; - --border-primary: #1d3b53; - --border-secondary: #2d4b63; - --accent-primary: #046390; - --accent-secondary: #065783; - --accent-hover: #38bdf8; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.35), 0 1px 2px 0 rgba(0, 0, 0, 0.25); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.35), 0 4px 6px -2px rgba(0, 0, 0, 0.25); -} - -[data-theme="sunset"] { - --bg-primary: #1c1917; - --bg-secondary: #292524; - --bg-tertiary: #44403c; - --text-primary: #fafaf9; - --text-secondary: #e7e5e4; - --text-tertiary: #a8a29e; - --border-primary: #57534e; - --border-secondary: #78716c; - --accent-primary: #fb923c; - --accent-secondary: #fdba74; - --accent-hover: #f97316; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); -} - -[data-theme="forest"] { - --bg-primary: #052e16; - --bg-secondary: #14532d; - --bg-tertiary: #166534; - --text-primary: #f0fdf4; - --text-secondary: #dcfce7; - --text-tertiary: #86efac; - --border-primary: #15803d; - --border-secondary: #16a34a; - --accent-primary: #309254; - --accent-secondary: #2cef74; - --accent-hover: #22c55e; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.35), 0 1px 2px 0 rgba(0, 0, 0, 0.25); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.35), 0 4px 6px -2px rgba(0, 0, 0, 0.25); -} - -[data-theme="ocean"] { - --bg-primary: #0c4a6e; - --bg-secondary: #155e75; - --bg-tertiary: #0e7490; - --text-primary: #f0fdfd; - --text-secondary: #cffafe; - --text-tertiary: #a5f3fc; - --border-primary: #0891b2; - --border-secondary: #06b6d4; - --accent-primary: #22d3ee; - --accent-secondary: #67e8f9; - --accent-hover: #06b6d4; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); -} - -[data-theme="lavender"] { - --bg-primary: #faf5ff; - --bg-secondary: #f3e8ff; - --bg-tertiary: #e9d5ff; - --text-primary: #581c87; - --text-secondary: #7e22ce; - --text-tertiary: #a855f7; - --border-primary: #d8b4fe; - --border-secondary: #c084fc; - --accent-primary: #a855f7; - --accent-secondary: #c084fc; - --accent-hover: #9333ea; - --shadow: - 0 1px 3px 0 rgba(168, 85, 247, 0.15), 0 1px 2px 0 rgba(168, 85, 247, 0.1); - --shadow-lg: - 0 10px 15px -3px rgba(168, 85, 247, 0.15), - 0 4px 6px -2px rgba(168, 85, 247, 0.1); -} - -[data-theme="coffee"] { - --bg-primary: #292524; - --bg-secondary: #44403c; - --bg-tertiary: #57534e; - --text-primary: #fafaf9; - --text-secondary: #e7e5e4; - --text-tertiary: #d6d3d1; - --border-primary: #78716c; - --border-secondary: #a8a29e; - --accent-primary: #d97706; - --accent-secondary: #f59e0b; - --accent-hover: #b45309; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.35), 0 1px 2px 0 rgba(0, 0, 0, 0.25); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.35), 0 4px 6px -2px rgba(0, 0, 0, 0.25); -} - -[data-theme="midnight"] { - --bg-primary: #0a0a0a; - --bg-secondary: #171717; - --bg-tertiary: #262626; - --text-primary: #fafafa; - --text-secondary: #d4d4d4; - --text-tertiary: #a3a3a3; - --border-primary: #404040; - --border-secondary: #525252; - --accent-primary: #3b82f6; - --accent-secondary: #60a5fa; - --accent-hover: #2563eb; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); -} - -[data-theme="rose"] { - --bg-primary: #fff1f2; - --bg-secondary: #ffe4e6; - --bg-tertiary: #fecdd3; - --text-primary: #881337; - --text-secondary: #be123c; - --text-tertiary: #e11d48; - --border-primary: #fda4af; - --border-secondary: #fb7185; - --accent-primary: #e11d48; - --accent-secondary: #f43f5e; - --accent-hover: #be123c; - --shadow: - 0 1px 3px 0 rgba(225, 29, 72, 0.15), 0 1px 2px 0 rgba(225, 29, 72, 0.1); - --shadow-lg: - 0 10px 15px -3px rgba(225, 29, 72, 0.15), - 0 4px 6px -2px rgba(225, 29, 72, 0.1); + --bg-primary: #000000; + --bg-secondary: #1a1a1a; + --text-primary: #ffffff; + --text-secondary: #cccccc; + --border: #333333; + --card-bg: #0a0a0a; + --input-bg: #1a1a1a; + --button-bg: #ffffff; + --button-text: #000000; + --button-hover: #cccccc; } body { @@ -179,67 +52,3 @@ body { background-color 0.3s ease, color 0.3s ease; } - -.bg-primary { - background-color: var(--bg-primary); -} - -.bg-secondary { - background-color: var(--bg-secondary); -} - -.bg-tertiary { - background-color: var(--bg-tertiary); -} - -.text-primary { - color: var(--text-primary); -} - -.text-secondary { - color: var(--text-secondary); -} - -.text-tertiary { - color: var(--text-tertiary); -} - -.border-primary { - border-color: var(--border-primary); -} - -.border-secondary { - border-color: var(--border-secondary); -} - -.border-accent { - border-color: var(--accent-primary); -} - -.accent-primary { - background-color: var(--accent-primary); -} - -.accent-secondary { - background-color: var(--accent-secondary); -} - -.text-accent { - color: var(--accent-primary); -} - -.shadow-regular { - box-shadow: var(--shadow); -} - -.shadow-large { - box-shadow: var(--shadow-lg); -} - -.hover-accent:hover { - background-color: var(--accent-hover); -} - -.hover-bg-secondary:hover { - background-color: var(--bg-secondary); -}
+ Sign in to your account +
+ Don't have an account?{" "} + + Sign up + +
+ Sign up to get started +
+ {passwordError} +
+ Already have an account?{" "} + + Sign in + +