added registration
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user