frontend #1
@@ -2,6 +2,9 @@ import { Suspense } from "react";
|
|||||||
import { Routes as ReactRoutes, Route, Navigate } from "react-router-dom";
|
import { Routes as ReactRoutes, Route, Navigate } from "react-router-dom";
|
||||||
import { HomePage } from "@/pages/home.page";
|
import { HomePage } from "@/pages/home.page";
|
||||||
import { ThemesPage } from "@/pages/themes.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 = () => {
|
export const Routing = () => {
|
||||||
return (
|
return (
|
||||||
@@ -13,10 +16,14 @@ export const Routing = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ReactRoutes>
|
<ReactRoutes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route element={<DefaultLayout />}>
|
||||||
<Route path="/themes" element={<ThemesPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/auth" element={<AuthPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="/themes" element={<ThemesPage />} />
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Route>
|
||||||
</ReactRoutes>
|
</ReactRoutes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<AuthState>()(
|
||||||
|
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 }),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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<void>;
|
||||||
|
register: (data: RegisterData) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Theme = "light" | "dark";
|
||||||
@@ -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<ThemeState>()(
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<FiSun className="w-5 h-5 text-yellow-500" />
|
||||||
|
) : (
|
||||||
|
<FiMoon className="w-5 h-5 text-gray-700" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
if (error) clearError();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-[25%] flex items-center justify-center bg-white dark:bg-black transition-colors duration-200">
|
||||||
|
<div className="w-full max-w-md px-8">
|
||||||
|
{/* Card */}
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-800">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Welcome Back
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Sign in to your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-800 rounded text-red-700 dark:text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Login
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FiUser className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="login"
|
||||||
|
value={formData.login}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Enter your login"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FiLock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
"Signing in..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FiLogIn />
|
||||||
|
Sign In
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="text-gray-900 dark:text-white hover:underline font-medium"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<string | null>(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<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
if (error) clearError();
|
||||||
|
if (passwordError) setPasswordError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-black transition-colors duration-200">
|
||||||
|
<div className="w-full max-w-md px-8">
|
||||||
|
{/* Card */}
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-800">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Create Account
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Sign up to get started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-800 rounded text-red-700 dark:text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="firstName"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="John"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Last Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="lastName"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Login
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FiUser className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="login"
|
||||||
|
value={formData.login}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Choose a login"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FiLock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Create a password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FiLock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-gray-500 dark:focus:ring-gray-400 transition-colors"
|
||||||
|
placeholder="Confirm your password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{passwordError && (
|
||||||
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{passwordError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
"Creating account..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FiUserPlus />
|
||||||
|
Sign Up
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to="/auth"
|
||||||
|
className="text-gray-900 dark:text-white hover:underline font-medium"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<DefaultLayoutProps> = ({ children }) => {
|
||||||
|
const { user, logout } = useAuthStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
navigate("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white dark:bg-black transition-colors duration-200">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-black sticky top-0 z-50">
|
||||||
|
<div className="container mx-auto px-4 py-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
HellreigN
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ThemeToggle />
|
||||||
|
{user && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main className="min-h-[calc(100vh-61px)]">{children || <Outlet />}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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-primary: #ffffff;
|
||||||
--bg-secondary: #f8fafc;
|
--bg-secondary: #f5f5f5;
|
||||||
--bg-tertiary: #f1f5f9;
|
--text-primary: #000000;
|
||||||
--text-primary: #1e293b;
|
--text-secondary: #333333;
|
||||||
--text-secondary: #64748b;
|
--border: #e5e5e5;
|
||||||
--text-tertiary: #94a3b8;
|
--card-bg: #ffffff;
|
||||||
--border-primary: #e2e8f0;
|
--input-bg: #ffffff;
|
||||||
--border-secondary: #cbd5e1;
|
--button-bg: #000000;
|
||||||
--accent-primary: #4f46e5;
|
--button-text: #ffffff;
|
||||||
--accent-secondary: #6366f1;
|
--button-hover: #333333;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--bg-primary: #0f172a;
|
--bg-primary: #000000;
|
||||||
--bg-secondary: #1e293b;
|
--bg-secondary: #1a1a1a;
|
||||||
--bg-tertiary: #334155;
|
--text-primary: #ffffff;
|
||||||
--text-primary: #f1f5f9;
|
--text-secondary: #cccccc;
|
||||||
--text-secondary: #cbd5e1;
|
--border: #333333;
|
||||||
--text-tertiary: #94a3b8;
|
--card-bg: #0a0a0a;
|
||||||
--border-primary: #475569;
|
--input-bg: #1a1a1a;
|
||||||
--border-secondary: #64748b;
|
--button-bg: #ffffff;
|
||||||
--accent-primary: #5061fc;
|
--button-text: #000000;
|
||||||
--accent-secondary: #4866ff;
|
--button-hover: #cccccc;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -179,67 +52,3 @@ body {
|
|||||||
background-color 0.3s ease,
|
background-color 0.3s ease,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user