Merge pull request #4 from SlimusMinus/add_registration

added registration
This commit is contained in:
SlimusMinus
2026-05-04 00:49:29 +03:00
committed by GitHub
8 changed files with 742 additions and 172 deletions

View File

@@ -1,18 +1,12 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/AuthPage';
function App() {
return (
<BrowserRouter>
<Routes>
{/* Главная → сразу редирект на логин */}
<Route path="/" element={<Navigate to="/login" replace />} />
{/* Страница авторизации */}
<Route path="/login" element={<LoginPage />} />
{/* Сюда добавишь остальные страницы позже */}
{/* <Route path="/dashboard" element={<Dashboard />} /> */}
<Route path="/" element={<Navigate to="/register" replace />} />
<Route path="/register" element={<RegisterPage />} />
</Routes>
</BrowserRouter>
);

BIN
src/assets/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,9 +1,8 @@
import { s } from '../styles/loginStyles';
import keysIcon from '../assets/passwords_keys_6032.png';
export default function MagnifierLogo() {
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>
<radialGradient id="glassGrad" cx="38%" cy="35%" r="60%">
<stop offset="0%" stopColor="#7ffff0"/>

69
src/hooks/useRegister.js Normal file
View 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
View 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>
);
}

View File

@@ -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
View 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;
}

View File

@@ -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",
},
};