Merge pull request #4 from SlimusMinus/add_registration
added registration
This commit is contained in:
12
src/App.js
12
src/App.js
@@ -1,18 +1,12 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import LoginPage from './pages/LoginPage';
|
import RegisterPage from './pages/AuthPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Главная → сразу редирект на логин */}
|
<Route path="/" element={<Navigate to="/register" replace />} />
|
||||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|
||||||
{/* Страница авторизации */}
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
|
||||||
|
|
||||||
{/* Сюда добавишь остальные страницы позже */}
|
|
||||||
{/* <Route path="/dashboard" element={<Dashboard />} /> */}
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
BIN
src/assets/success.png
Normal file
BIN
src/assets/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,9 +1,8 @@
|
|||||||
import { s } from '../styles/loginStyles';
|
|
||||||
import keysIcon from '../assets/passwords_keys_6032.png';
|
import keysIcon from '../assets/passwords_keys_6032.png';
|
||||||
|
|
||||||
export default function MagnifierLogo() {
|
export default function MagnifierLogo() {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" style={s.logoSvg}>
|
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" className="logoSvg">
|
||||||
<defs>
|
<defs>
|
||||||
<radialGradient id="glassGrad" cx="38%" cy="35%" r="60%">
|
<radialGradient id="glassGrad" cx="38%" cy="35%" r="60%">
|
||||||
<stop offset="0%" stopColor="#7ffff0"/>
|
<stop offset="0%" stopColor="#7ffff0"/>
|
||||||
|
|||||||
69
src/hooks/useRegister.js
Normal file
69
src/hooks/useRegister.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {API_URL} from "../config/constants";
|
||||||
|
|
||||||
|
|
||||||
|
export function useCheckLogin(login) {
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
const [exists, setExists] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!login || login.length < 3) {
|
||||||
|
setExists(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
setChecking(true);
|
||||||
|
const res = await fetch(`${API_URL}/api/v1/auth/check-login?login=${login}`);
|
||||||
|
const json = await res.json();
|
||||||
|
setExists(json.exists);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [login]);
|
||||||
|
|
||||||
|
return { checking, exists };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useRegister() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const register = async ({ name, login, password }) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/api/v1/auth/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name, login, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.message || "Ошибка регистрации");
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = "/register";
|
||||||
|
}, 2500);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { register, loading, error, success };
|
||||||
|
}
|
||||||
380
src/pages/AuthPage.jsx
Normal file
380
src/pages/AuthPage.jsx
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import MagnifierLogo from "../components/MagnifierLogo";
|
||||||
|
import { useAuth } from "../hooks/useAuth";
|
||||||
|
import { useRegister } from "../hooks/useRegister";
|
||||||
|
import {API_URL, FLOAT_ICONS} from "../config/constants";
|
||||||
|
import "../styles/global.css";
|
||||||
|
import "../styles/auth.css";
|
||||||
|
import likeSmile from '../assets/success.png';
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useCheckLogin } from "../hooks/useRegister";
|
||||||
|
|
||||||
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
function getStrength(val) {
|
||||||
|
if (!val) return { level: 0, label: "", color: "transparent", cls: "" };
|
||||||
|
let s = 0;
|
||||||
|
if (val.length >= 6) s++;
|
||||||
|
if (val.length >= 10) s++;
|
||||||
|
if (/[A-Z]/.test(val) || /[0-9]/.test(val)) s++;
|
||||||
|
if (/[^A-Za-z0-9]/.test(val)) s++;
|
||||||
|
const labels = ["", "Слабый", "Средний", "Хороший", "Отличный"];
|
||||||
|
const colors = ["", "#ff6b6b", "#ffd166", "#2de0c8", "#2de0c8"];
|
||||||
|
const cls = ["", "sw", "sm", "ss", "ss"];
|
||||||
|
return { level: s, label: labels[s], color: colors[s], cls: cls[s] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Shared eye button ────────────────────────────────────────────────────────
|
||||||
|
function EyeBtn({ show, onToggle}) {
|
||||||
|
return (
|
||||||
|
<button type="button" className="eye-btn" onClick={onToggle}>
|
||||||
|
{show ? "🔒" : "👁"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step dots ────────────────────────────────────────────────────────────────
|
||||||
|
function StepDots({ current, total }) {
|
||||||
|
return (
|
||||||
|
<div className="step-dots">
|
||||||
|
{Array.from({ length: total }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`dot ${i === current ? "dot-active" : i < current ? "dot-done" : ""}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// LOGIN FORM
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
function LoginForm({ onSwitchToRegister }) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [showPass, setShowPass] = useState(false);
|
||||||
|
const { login, loading, error } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-body">
|
||||||
|
<p className="form-subtitle">С возвращением</p>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); login(email, password); }} noValidate>
|
||||||
|
<div className="field-wrap">
|
||||||
|
<input
|
||||||
|
className="field"
|
||||||
|
type="email"
|
||||||
|
placeholder="Введите email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
readOnly
|
||||||
|
onFocus={(e) => e.target.removeAttribute("readOnly")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-wrap" style={{ position: "relative" }}>
|
||||||
|
<input
|
||||||
|
className="field"
|
||||||
|
type={showPass ? "text" : "password"}
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
style={{ paddingRight: "44px" }}
|
||||||
|
autoComplete="off"
|
||||||
|
readOnly
|
||||||
|
onFocus={(e) => e.target.removeAttribute("readOnly")}
|
||||||
|
/>
|
||||||
|
<EyeBtn show={showPass} onToggle={() => setShowPass(v => !v)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="err-msg">{error}</p>}
|
||||||
|
|
||||||
|
<button type="submit" className="btn-primary" disabled={loading}>
|
||||||
|
{loading ? "Входим..." : "Войти"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button className="ghost-btn">Забыли пароль?</button>
|
||||||
|
|
||||||
|
<p className="switch-row">
|
||||||
|
Нет аккаунта?{" "}
|
||||||
|
<button className="link-btn" onClick={onSwitchToRegister}>
|
||||||
|
Зарегистрироваться
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── step 0+1: name + login + passwords ───────────────────────────────────────
|
||||||
|
function RegStep01({ data, onChange, onNext }) {
|
||||||
|
const [show1, setShow1] = useState(false);
|
||||||
|
const [show2, setShow2] = useState(false);
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
const strength = getStrength(data.password);
|
||||||
|
const matched = data.password && data.password === data.password2;
|
||||||
|
|
||||||
|
const { checking: checkingLogin, exists: loginExists } = useCheckLogin(data.login);
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const e = {};
|
||||||
|
if (!data.name.trim()) e.name = "Введите имя";
|
||||||
|
if (!data.login.trim()) e.login = "Введите логин";
|
||||||
|
else if (data.login.length < 3) e.login = "Минимум 3 символа";
|
||||||
|
if (data.password.length < 6) e.password = "Минимум 6 символов";
|
||||||
|
else if (data.password !== data.password2) e.password2 = "Пароли не совпадают";
|
||||||
|
|
||||||
|
setErrors(e);
|
||||||
|
return !Object.keys(e).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="field-wrap">
|
||||||
|
<label className="field-label">Имя пользователя</label>
|
||||||
|
<input
|
||||||
|
className={`field ${errors.name ? "field-err" : ""}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Как вас зовут?"
|
||||||
|
value={data.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange("name", e.target.value);
|
||||||
|
setErrors(p => ({...p, name: ""}));
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="err-msg">{errors.name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-wrap">
|
||||||
|
<label className="field-label">Логин</label>
|
||||||
|
<input
|
||||||
|
className={`field ${errors.login || loginExists ? "field-err"
|
||||||
|
: ""}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Придумайте логин"
|
||||||
|
value={data.login}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange("login", e.target.value);
|
||||||
|
setErrors(p => ({...p, login: ""}));
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{checkingLogin && <p className="hint-msg">Проверяем...</p>}
|
||||||
|
|
||||||
|
{!checkingLogin && data.login.length >= 3 && (
|
||||||
|
<p className="err-msg"
|
||||||
|
style={{color: loginExists ? "#ff6b6b" : "#2de0c8"}}>
|
||||||
|
{loginExists ? "✗ Логин занят" : "✓ Логин свободен"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errors.login && <p className="err-msg">{errors.login}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-wrap">
|
||||||
|
<label className="field-label">Пароль</label>
|
||||||
|
|
||||||
|
<div className="input-wrap">
|
||||||
|
<input
|
||||||
|
className={`field ${errors.password ? "field-err" : ""}`}
|
||||||
|
type={show1 ? "text" : "password"}
|
||||||
|
placeholder="Минимум 6 символов"
|
||||||
|
value={data.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange("password", e.target.value);
|
||||||
|
setErrors(p => ({...p, password: ""}));
|
||||||
|
}}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<EyeBtn show={show1} onToggle={() => setShow1(v => !v)}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.password && (
|
||||||
|
<>
|
||||||
|
<div className="strength-bar">
|
||||||
|
{[0, 1, 2, 3].map(i => (
|
||||||
|
<div key={i}
|
||||||
|
className={`seg ${i < strength.level ? strength.cls
|
||||||
|
: ""}`}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="strength-label" style={{color: strength.color}}>
|
||||||
|
{strength.label}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errors.password && <p className="err-msg">{errors.password}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="field-wrap">
|
||||||
|
<label className="field-label">Повторите пароль</label>
|
||||||
|
|
||||||
|
<div className="input-wrap">
|
||||||
|
<input
|
||||||
|
className={`field ${data.password2 ? (matched ? "field-ok"
|
||||||
|
: "field-err") : ""}`}
|
||||||
|
type={show2 ? "text" : "password"}
|
||||||
|
placeholder="Введите ещё раз"
|
||||||
|
value={data.password2}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange("password2", e.target.value);
|
||||||
|
setErrors(p => ({...p, password2: ""}));
|
||||||
|
}}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<EyeBtn show={show2} onToggle={() => setShow2(v => !v)}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.password2 && (
|
||||||
|
<p className="err-msg"
|
||||||
|
style={{color: matched ? "#2de0c8" : "#ff6b6b"}}>
|
||||||
|
{matched ? "✓ Совпадают" : "✗ Не совпадают"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errors.password2 && <p className="err-msg">{errors.password2}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn-primary" onClick={() => validate() && onNext()}>
|
||||||
|
Далее →
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── step 2: confirm ──────────────────────────────────────────────────────────
|
||||||
|
function RegStep2({data, onSubmit, onBack, loading, error}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="confirm-box">
|
||||||
|
<div className="confirm-row">
|
||||||
|
<span className="confirm-label">Имя</span>
|
||||||
|
<span className="confirm-val">{data.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-divider"/>
|
||||||
|
<div className="confirm-row">
|
||||||
|
<span className="confirm-label">Логин</span>
|
||||||
|
<span className="confirm-val accent">@{data.login}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <p className="err-msg" style={{
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: "10px"
|
||||||
|
}}>{error}</p>}
|
||||||
|
<button className="btn-primary" onClick={onSubmit} disabled={loading}>
|
||||||
|
{loading ? "Создаём..." : "Создать аккаунт ✓"}
|
||||||
|
</button>
|
||||||
|
<button className="back-btn" onClick={onBack}>← Назад</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── success ──────────────────────────────────────────────────────────────────
|
||||||
|
function RegSuccess({name, onLogin}) {
|
||||||
|
return (
|
||||||
|
<div className="success-wrap">
|
||||||
|
<img src={likeSmile} alt="success" className="success-icon"/>
|
||||||
|
<p
|
||||||
|
className="success-title">Добро пожаловать, {name}!</p>
|
||||||
|
<p className="success-sub">Аккаунт создан успешно.</p>
|
||||||
|
<button className="btn-primary" style={{marginTop: "20px"}}
|
||||||
|
onClick={onLogin}>
|
||||||
|
Войти →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegisterForm({onSwitchToLogin}) {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [form, setForm] = useState(
|
||||||
|
{name: "", login: "", password: "", password2: ""});
|
||||||
|
const {register, loading, error, success} = useRegister();
|
||||||
|
|
||||||
|
const setField = useCallback((key, val) => setForm(p => ({ ...p, [key]: val })), []);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="form-body">
|
||||||
|
<RegSuccess name={form.name} onLogin={onSwitchToLogin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-body">
|
||||||
|
<p className="form-subtitle">Создайте аккаунт</p>
|
||||||
|
<StepDots current={step} total={2} />
|
||||||
|
|
||||||
|
{step === 0 && (
|
||||||
|
<RegStep01 data={form} onChange={setField} onNext={() => setStep(1)} />
|
||||||
|
)}
|
||||||
|
{step === 1 && (
|
||||||
|
<RegStep2
|
||||||
|
data={form}
|
||||||
|
onSubmit={() => register({ name: form.name, login: form.login, password: form.password })}
|
||||||
|
onBack={() => setStep(0)}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 0 && (
|
||||||
|
<p className="switch-row">
|
||||||
|
Уже есть аккаунт?{" "}
|
||||||
|
<button className="link-btn" onClick={onSwitchToLogin}>Войти</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// AUTH PAGE — единственная страница, переключает login ↔ register
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
export default function AuthPage() {
|
||||||
|
const [mode, setMode] = useState("login"); // "login" | "register"
|
||||||
|
const [animating, setAnimating] = useState(false);
|
||||||
|
|
||||||
|
const switchTo = (next) => {
|
||||||
|
if (animating) return;
|
||||||
|
setAnimating(true);
|
||||||
|
// Небольшая задержка — карточка уходит, потом приходит новая
|
||||||
|
setTimeout(() => {
|
||||||
|
setMode(next);
|
||||||
|
setAnimating(false);
|
||||||
|
}, 320);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-page">
|
||||||
|
<div className="bg-glow" />
|
||||||
|
|
||||||
|
{FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => (
|
||||||
|
<img
|
||||||
|
key={i} src={icon} alt="" className="fi"
|
||||||
|
style={{ ...pos, animationDelay: delay, width: size || "7rem", height: size || "7rem", filter: "grayscale(100%)" }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={`auth-card ${animating ? "card-out" : "card-in"}`}>
|
||||||
|
<button className="dots-btn">⋮</button>
|
||||||
|
|
||||||
|
<div className="logo-wrap">
|
||||||
|
<MagnifierLogo />
|
||||||
|
</div>
|
||||||
|
<h1 className="auth-title">Находка</h1>
|
||||||
|
|
||||||
|
{mode === "login"
|
||||||
|
? <LoginForm onSwitchToRegister={() => switchTo("register")} />
|
||||||
|
: <RegisterForm onSwitchToLogin={() => switchTo("login")} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import MagnifierLogo from "../components/MagnifierLogo";
|
|
||||||
import { useAuth } from "../hooks/useAuth";
|
|
||||||
import { FLOAT_ICONS } from "../config/constants";
|
|
||||||
import { s } from "../styles/loginStyles";
|
|
||||||
import "../styles/global.css";
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
const [loginValue, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [showPass, setShowPass] = useState(false);
|
|
||||||
|
|
||||||
const { login, loginGoogle, loginYandex, loginVK, loading, error } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
<div style={s.page}>
|
|
||||||
<div style={s.bgGlow} />
|
|
||||||
|
|
||||||
{FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => (
|
|
||||||
<img key={i} src={icon} alt="" className="fi" style={{ ...pos, animationDelay: delay, width: size || "7rem", height: size || "7rem", filter: "grayscale(100%) " }} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div style={s.card} className="card-in">
|
|
||||||
<button style={s.dotsBtn}>⋮</button>
|
|
||||||
|
|
||||||
<div style={s.logoWrap}>
|
|
||||||
<MagnifierLogo />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 style={s.title}>Находка</h1>
|
|
||||||
|
|
||||||
<form onSubmit={(e) => { e.preventDefault(); login(loginValue, password); }} noValidate>
|
|
||||||
<div style={s.inputWrap}>
|
|
||||||
<input
|
|
||||||
type="email" placeholder="Введите email"
|
|
||||||
value={loginValue} onChange={(e) => setEmail(e.target.value)}
|
|
||||||
style={s.input} className="li"
|
|
||||||
autoComplete="off" name="naidishchey-email-x7k"
|
|
||||||
readOnly onFocus={(e) => e.target.removeAttribute("readOnly")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ ...s.inputWrap, position: "relative" }}>
|
|
||||||
<input
|
|
||||||
type={showPass ? "text" : "password"} placeholder="Введите пароль"
|
|
||||||
value={password} onChange={(e) => setPassword(e.target.value)}
|
|
||||||
style={{ ...s.input, paddingRight: "44px" }} className="li"
|
|
||||||
autoComplete="off" name="naidishchey-pass-x7k"
|
|
||||||
readOnly onFocus={(e) => e.target.removeAttribute("readOnly")}
|
|
||||||
/>
|
|
||||||
<button type="button" style={s.eyeBtn} onClick={() => setShowPass(v => !v)}>
|
|
||||||
{showPass ? "🙈" : "👁"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p style={s.errMsg}>{error}</p>}
|
|
||||||
|
|
||||||
<button type="submit" style={s.btnLogin} className="btn-l" disabled={loading}>
|
|
||||||
{loading ? "Входим..." : "Войти"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<button style={s.forgotBtn}>Забыли пароль?</button>
|
|
||||||
|
|
||||||
<p style={s.regRow}>
|
|
||||||
Нет аккаунта? <a href="/register" style={s.link}>Зарегистрироваться</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
289
src/styles/auth.css
Normal file
289
src/styles/auth.css
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap');
|
||||||
|
|
||||||
|
/* ─── Page ──────────────────────────────────────────────────────────────────── */
|
||||||
|
.auth-page {
|
||||||
|
font-family: 'Nunito', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: radial-gradient(ellipse at 30% 40%, #1a2340 0%, #0d1018 60%);
|
||||||
|
overflow: hidden; position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-glow {
|
||||||
|
position: fixed; top: 40%; right: 0;
|
||||||
|
width: 500px; height: 400px;
|
||||||
|
background: radial-gradient(ellipse, #0f2a22 0%, transparent 70%);
|
||||||
|
pointer-events: none; opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Floating icons ────────────────────────────────────────────────────────── */
|
||||||
|
.fi {
|
||||||
|
position: fixed;
|
||||||
|
opacity: 0.13;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
animation: float 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(-6deg); }
|
||||||
|
50% { transform: translateY(-18px) rotate(6deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Card ──────────────────────────────────────────────────────────────────── */
|
||||||
|
.auth-card {
|
||||||
|
position: relative; z-index: 10; width: 375px;
|
||||||
|
background: linear-gradient(155deg, #1d2436 0%, #131720 100%);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 30px; padding: 36px 38px 30px;
|
||||||
|
box-shadow: 0 40px 100px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.03);
|
||||||
|
/* height transition — карточка плавно меняет размер при смене формы */
|
||||||
|
transition: height 0.35s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Появление карточки / контента */
|
||||||
|
.card-in {
|
||||||
|
animation: cardIn .45s cubic-bezier(.34,1.56,.64,1) both;
|
||||||
|
}
|
||||||
|
@keyframes cardIn {
|
||||||
|
from { opacity: 0; transform: scale(.92) translateY(30px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Уход карточки при переключении */
|
||||||
|
.card-out {
|
||||||
|
animation: cardOut .3s cubic-bezier(.4,0,.6,1) both;
|
||||||
|
}
|
||||||
|
@keyframes cardOut {
|
||||||
|
from { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
to { opacity: 0; transform: scale(.94) translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контент формы — появляется снизу при смене */
|
||||||
|
.form-body {
|
||||||
|
animation: formIn .4s cubic-bezier(.34,1.56,.64,1) both;
|
||||||
|
}
|
||||||
|
@keyframes formIn {
|
||||||
|
from { opacity: 0; transform: translateY(22px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Переход между шагами регистрации */
|
||||||
|
@keyframes stepIn {
|
||||||
|
from { opacity: 0; transform: translateX(18px) scale(.98); }
|
||||||
|
to { opacity: 1; transform: translateX(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Logo ──────────────────────────────────────────────────────────────────── */
|
||||||
|
.logo-wrap { text-align: center; margin-bottom: 2px; }
|
||||||
|
|
||||||
|
@keyframes logoPulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 18px rgba(45,224,200,.45)); }
|
||||||
|
50% { filter: drop-shadow(0 0 38px rgba(45,224,200,.8)); }
|
||||||
|
}
|
||||||
|
/* Apply this class to your MagnifierLogo svg if needed */
|
||||||
|
.logo-pulse { animation: logoPulse 3s ease-in-out infinite; }
|
||||||
|
|
||||||
|
/* ─── Typography ────────────────────────────────────────────────────────────── */
|
||||||
|
.auth-title {
|
||||||
|
text-align: center; color: #fff;
|
||||||
|
font-size: 2rem; font-weight: 900;
|
||||||
|
letter-spacing: -0.5px; margin: 12px 0 0;
|
||||||
|
}
|
||||||
|
.form-subtitle {
|
||||||
|
text-align: center; color: rgba(255,255,255,0.35);
|
||||||
|
font-size: 0.83rem; margin: 4px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Step dots ─────────────────────────────────────────────────────────────── */
|
||||||
|
.step-dots {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
gap: 6px; margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
transition: all 0.35s cubic-bezier(.34,1.56,.64,1);
|
||||||
|
}
|
||||||
|
.dot-active {
|
||||||
|
width: 26px; border-radius: 4px;
|
||||||
|
background: #2de0c8;
|
||||||
|
}
|
||||||
|
.dot-done {
|
||||||
|
background: rgba(45,224,200,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Fields ────────────────────────────────────────────────────────────────── */
|
||||||
|
.field-wrap { margin-bottom: 12px; }
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
display: block; color: rgba(255,255,255,0.45);
|
||||||
|
font-size: 0.73rem; font-weight: 700;
|
||||||
|
letter-spacing: 0.6px; margin-bottom: 5px;
|
||||||
|
text-transform: uppercase; font-family: 'Nunito', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
width: 100%; box-sizing: border-box;
|
||||||
|
background: rgba(255,255,255,0.055);
|
||||||
|
border: 1.5px solid rgba(255,255,255,0.09);
|
||||||
|
border-radius: 14px; padding: 13px 18px;
|
||||||
|
color: #fff; font-family: 'Nunito', sans-serif;
|
||||||
|
font-size: 0.93rem; outline: none;
|
||||||
|
transition: border-color .2s, background .2s, box-shadow .2s;
|
||||||
|
}
|
||||||
|
.field:focus {
|
||||||
|
border-color: #2de0c8;
|
||||||
|
background: rgba(45,224,200,.07);
|
||||||
|
box-shadow: 0 0 0 3px rgba(45,224,200,.12);
|
||||||
|
}
|
||||||
|
.field::placeholder { color: rgba(255,255,255,.32); }
|
||||||
|
|
||||||
|
.field-err { border-color: rgba(255,107,107,0.55) !important; box-shadow: 0 0 0 3px rgba(255,107,107,0.09) !important; }
|
||||||
|
.field-ok { border-color: rgba(45,224,200,0.45) !important; }
|
||||||
|
|
||||||
|
.eye-btn {
|
||||||
|
position: absolute; right: 13px; top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
color: rgba(255,255,255,0.35); font-size: 1.05rem; line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrap .eye-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 13px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrap .field {
|
||||||
|
padding-right: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Password strength ──────────────────────────────────────────────────────── */
|
||||||
|
.strength-bar { display: flex; gap: 4px; margin-top: 7px; }
|
||||||
|
.seg {
|
||||||
|
flex: 1; height: 3px; border-radius: 2px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.sw { background: #ff6b6b; }
|
||||||
|
.sm { background: #ffd166; }
|
||||||
|
.ss { background: #2de0c8; }
|
||||||
|
.strength-label { font-size: 0.72rem; text-align: right; margin-top: 3px; }
|
||||||
|
|
||||||
|
/* ─── Messages ──────────────────────────────────────────────────────────────── */
|
||||||
|
.err-msg {
|
||||||
|
color: #ff6b6b; font-size: 0.8rem;
|
||||||
|
margin-top: 5px; padding-left: 2px;
|
||||||
|
font-family: 'Nunito', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Confirm box ───────────────────────────────────────────────────────────── */
|
||||||
|
.confirm-box {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 18px; padding: 16px 20px; margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.confirm-row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.confirm-label { color: rgba(255,255,255,0.4); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
.confirm-val { color: #fff; font-weight: 700; }
|
||||||
|
.confirm-val.accent { color: #2de0c8; }
|
||||||
|
.confirm-divider { height: 1px; background: rgba(255,255,255,0.06); margin: 10px 0; }
|
||||||
|
|
||||||
|
/* ─── Buttons ───────────────────────────────────────────────────────────────── */
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg, #1fc9b0, #2de0c8, #1fc9b0);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
border: none; border-radius: 14px; padding: 15px;
|
||||||
|
color: #082320; font-family: 'Nunito', sans-serif;
|
||||||
|
font-size: 1.05rem; font-weight: 900; cursor: pointer;
|
||||||
|
margin-top: 6px;
|
||||||
|
box-shadow: 0 6px 24px rgba(45,224,200,0.32);
|
||||||
|
animation: shimmer 3s linear infinite;
|
||||||
|
transition: opacity .15s, transform .15s;
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) { opacity: .88; transform: translateY(-2px); }
|
||||||
|
.btn-primary:active:not(:disabled) { transform: translateY(0); }
|
||||||
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-btn {
|
||||||
|
display: block; width: 100%; text-align: center;
|
||||||
|
color: #2de0c8; font-size: 0.87rem; margin-top: 13px;
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
font-family: 'Nunito', sans-serif;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.ghost-btn:hover { opacity: 0.75; }
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: block; width: 100%; text-align: center;
|
||||||
|
color: rgba(255,255,255,0.35); font-size: 0.85rem; margin-top: 11px;
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
font-family: 'Nunito', sans-serif;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
.back-btn:hover { color: rgba(255,255,255,0.6); }
|
||||||
|
|
||||||
|
/* ─── Switch row ────────────────────────────────────────────────────────────── */
|
||||||
|
.switch-row {
|
||||||
|
text-align: center; color: rgba(255,255,255,0.4);
|
||||||
|
font-size: 0.87rem; margin-top: 13px;
|
||||||
|
}
|
||||||
|
.link-btn {
|
||||||
|
color: #2de0c8; background: none; border: none;
|
||||||
|
cursor: pointer; font-family: 'Nunito', sans-serif;
|
||||||
|
font-size: inherit; font-weight: 700; padding: 0;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.link-btn:hover { opacity: 0.75; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* ─── Dots btn ──────────────────────────────────────────────────────────────── */
|
||||||
|
.dots-btn {
|
||||||
|
position: absolute; top: 18px; right: 20px;
|
||||||
|
background: none; border: none;
|
||||||
|
color: rgba(255,255,255,0.25); font-size: 1.3rem;
|
||||||
|
cursor: pointer; letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Success ───────────────────────────────────────────────────────────────── */
|
||||||
|
.success-wrap { text-align: center; padding: 16px 0 8px; }
|
||||||
|
.success-icon {
|
||||||
|
font-size: 3.8rem; display: block; margin-bottom: 14px;
|
||||||
|
animation: pop .55s cubic-bezier(.34,1.56,.64,1) both;
|
||||||
|
}
|
||||||
|
@keyframes pop {
|
||||||
|
from { transform: scale(0) rotate(-20deg); opacity: 0; }
|
||||||
|
to { transform: scale(1) rotate(0deg); opacity: 1; }
|
||||||
|
}
|
||||||
|
.success-title { color: #2de0c8; font-size: 1.45rem; font-weight: 900; margin-bottom: 6px; }
|
||||||
|
.success-sub { color: rgba(255,255,255,0.4); font-size: 0.88rem; line-height: 1.5; }
|
||||||
|
|
||||||
|
.success-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
background-color: #2de0c8;
|
||||||
|
-webkit-mask: url("../assets/success.png") no-repeat center;
|
||||||
|
mask: url("../assets/success.png") no-repeat center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoSvg {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
animation: logoPulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
export const s = {
|
|
||||||
page: {
|
|
||||||
fontFamily: "'Nunito', sans-serif",
|
|
||||||
minHeight: "100vh",
|
|
||||||
display: "flex", alignItems: "center", justifyContent: "center",
|
|
||||||
background: "radial-gradient(ellipse at 30% 40%, #1a2340 0%, #0d1018 60%)",
|
|
||||||
overflow: "hidden", position: "relative",
|
|
||||||
},
|
|
||||||
bgGlow: {
|
|
||||||
position: "fixed", top: "40%", right: 0,
|
|
||||||
width: "500px", height: "400px",
|
|
||||||
background: "radial-gradient(ellipse, #0f2a22 0%, transparent 70%)",
|
|
||||||
pointerEvents: "none", opacity: 0.7,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
position: "relative", zIndex: 10, width: "360px",
|
|
||||||
background: "linear-gradient(155deg, #1d2436 0%, #131720 100%)",
|
|
||||||
border: "1px solid rgba(255,255,255,0.08)",
|
|
||||||
borderRadius: "30px", padding: "40px 38px 34px",
|
|
||||||
boxShadow: "0 40px 100px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.03)",
|
|
||||||
},
|
|
||||||
dotsBtn: {
|
|
||||||
position: "absolute", top: "18px", right: "20px",
|
|
||||||
background: "none", border: "none",
|
|
||||||
color: "rgba(255,255,255,0.25)", fontSize: "1.3rem",
|
|
||||||
cursor: "pointer", letterSpacing: "3px",
|
|
||||||
},
|
|
||||||
logoWrap: { textAlign: "center", marginBottom: "2px" },
|
|
||||||
logoSvg: {
|
|
||||||
width: "130px", height: "130px",
|
|
||||||
animation: "logoPulse 3s ease-in-out infinite",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: "center", color: "#fff",
|
|
||||||
fontSize: "2rem", fontWeight: 900,
|
|
||||||
letterSpacing: "-0.5px", margin: "14px 0 26px",
|
|
||||||
},
|
|
||||||
inputWrap: { marginBottom: "13px" },
|
|
||||||
input: {
|
|
||||||
width: "100%", boxSizing: "border-box",
|
|
||||||
background: "rgba(255,255,255,0.055)",
|
|
||||||
border: "1.5px solid rgba(255,255,255,0.09)",
|
|
||||||
borderRadius: "14px", padding: "14px 18px",
|
|
||||||
color: "#fff", fontFamily: "'Nunito', sans-serif",
|
|
||||||
fontSize: "0.93rem", outline: "none",
|
|
||||||
},
|
|
||||||
eyeBtn: {
|
|
||||||
position: "absolute", right: "13px", top: "50%",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
background: "none", border: "none", cursor: "pointer",
|
|
||||||
color: "rgba(255,255,255,0.35)", fontSize: "1.05rem", lineHeight: 1,
|
|
||||||
},
|
|
||||||
errMsg: {
|
|
||||||
color: "#ff6b6b", fontSize: "0.83rem",
|
|
||||||
textAlign: "center", margin: "6px 0 10px",
|
|
||||||
},
|
|
||||||
btnLogin: {
|
|
||||||
width: "100%",
|
|
||||||
background: "linear-gradient(90deg, #1fc9b0, #2de0c8, #1fc9b0)",
|
|
||||||
backgroundSize: "200% 100%",
|
|
||||||
border: "none", borderRadius: "14px", padding: "15px",
|
|
||||||
color: "#082320", fontFamily: "'Nunito', sans-serif",
|
|
||||||
fontSize: "1.05rem", fontWeight: 900, cursor: "pointer", marginTop: "6px",
|
|
||||||
boxShadow: "0 6px 24px rgba(45,224,200,0.35)",
|
|
||||||
animation: "shimmer 3s linear infinite",
|
|
||||||
},
|
|
||||||
forgotBtn: {
|
|
||||||
display: "block", width: "100%", textAlign: "center",
|
|
||||||
color: "#2de0c8", fontSize: "0.87rem", marginTop: "14px",
|
|
||||||
background: "none", border: "none", cursor: "pointer",
|
|
||||||
fontFamily: "'Nunito', sans-serif",
|
|
||||||
},
|
|
||||||
regRow: {
|
|
||||||
textAlign: "center", color: "rgba(255,255,255,0.4)",
|
|
||||||
fontSize: "0.87rem", marginTop: "9px",
|
|
||||||
},
|
|
||||||
link: { color: "#2de0c8", textDecoration: "none" },
|
|
||||||
divider: { display: "flex", alignItems: "center", gap: "10px", margin: "18px 0 14px" },
|
|
||||||
divText: { color: "rgba(255,255,255,0.25)", fontSize: "0.78rem" },
|
|
||||||
socials: { display: "flex", gap: "10px" },
|
|
||||||
socBtn: {
|
|
||||||
flex: 1, padding: "11px 0", borderRadius: "12px",
|
|
||||||
border: "1.5px solid rgba(255,255,255,0.09)",
|
|
||||||
background: "rgba(255,255,255,0.04)",
|
|
||||||
cursor: "pointer", fontWeight: 800,
|
|
||||||
fontFamily: "'Nunito', sans-serif", fontSize: "0.88rem", color: "#fff",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user