From aee1213ff96dccc98ac1c03b61ea7a81cdd0d31b Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Mon, 4 May 2026 00:48:50 +0300 Subject: [PATCH] added registration --- src/App.js | 12 +- src/assets/success.png | Bin 0 -> 2458 bytes src/components/MagnifierLogo.jsx | 3 +- src/hooks/useRegister.js | 69 ++++++ src/pages/AuthPage.jsx | 380 +++++++++++++++++++++++++++++++ src/pages/LoginPage.jsx | 73 ------ src/styles/auth.css | 289 +++++++++++++++++++++++ src/styles/loginStyles.js | 88 ------- 8 files changed, 742 insertions(+), 172 deletions(-) create mode 100644 src/assets/success.png create mode 100644 src/hooks/useRegister.js create mode 100644 src/pages/AuthPage.jsx delete mode 100644 src/pages/LoginPage.jsx create mode 100644 src/styles/auth.css delete mode 100644 src/styles/loginStyles.js diff --git a/src/App.js b/src/App.js index 9bebcf3..3699b51 100644 --- a/src/App.js +++ b/src/App.js @@ -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 ( - {/* Главная → сразу редирект на логин */} - } /> - - {/* Страница авторизации */} - } /> - - {/* Сюда добавишь остальные страницы позже */} - {/* } /> */} + } /> + } /> ); diff --git a/src/assets/success.png b/src/assets/success.png new file mode 100644 index 0000000000000000000000000000000000000000..746946e85e84ac56b060239a3d893a9b434050da GIT binary patch literal 2458 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 Yf4y=_3PSj})c^nh07*qoM6N<$f))(8%m4rY literal 0 HcmV?d00001 diff --git a/src/components/MagnifierLogo.jsx b/src/components/MagnifierLogo.jsx index f394c2c..75f7232 100644 --- a/src/components/MagnifierLogo.jsx +++ b/src/components/MagnifierLogo.jsx @@ -1,9 +1,8 @@ -import { s } from '../styles/loginStyles'; import keysIcon from '../assets/passwords_keys_6032.png'; export default function MagnifierLogo() { return ( - + diff --git a/src/hooks/useRegister.js b/src/hooks/useRegister.js new file mode 100644 index 0000000..2a361af --- /dev/null +++ b/src/hooks/useRegister.js @@ -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 }; +} \ No newline at end of file diff --git a/src/pages/AuthPage.jsx b/src/pages/AuthPage.jsx new file mode 100644 index 0000000..6374036 --- /dev/null +++ b/src/pages/AuthPage.jsx @@ -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 ( + + ); +} + +// ─── Step dots ──────────────────────────────────────────────────────────────── +function StepDots({ current, total }) { + return ( +
+ {Array.from({ length: total }).map((_, i) => ( +
+ ))} +
+ ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// LOGIN FORM +// ═══════════════════════════════════════════════════════════════════════════════ +function LoginForm({ onSwitchToRegister }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPass, setShowPass] = useState(false); + const { login, loading, error } = useAuth(); + + return ( +
+

С возвращением

+ +
{ e.preventDefault(); login(email, password); }} noValidate> +
+ setEmail(e.target.value)} + autoComplete="off" + readOnly + onFocus={(e) => e.target.removeAttribute("readOnly")} + /> +
+ +
+ setPassword(e.target.value)} + style={{ paddingRight: "44px" }} + autoComplete="off" + readOnly + onFocus={(e) => e.target.removeAttribute("readOnly")} + /> + setShowPass(v => !v)} /> +
+ + {error &&

{error}

} + + +
+ + + +

+ Нет аккаунта?{" "} + +

+
+ ); +} + +// ─── 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 ( + <> +
+ + { + onChange("name", e.target.value); + setErrors(p => ({...p, name: ""})); + }} + autoComplete="off" + /> + {errors.name &&

{errors.name}

} +
+ +
+ + { + onChange("login", e.target.value); + setErrors(p => ({...p, login: ""})); + }} + autoComplete="off" + /> + + {checkingLogin &&

Проверяем...

} + + {!checkingLogin && data.login.length >= 3 && ( +

+ {loginExists ? "✗ Логин занят" : "✓ Логин свободен"} +

+ )} + + {errors.login &&

{errors.login}

} +
+ +
+ + +
+ { + onChange("password", e.target.value); + setErrors(p => ({...p, password: ""})); + }} + autoComplete="new-password" + /> + setShow1(v => !v)}/> +
+ + {data.password && ( + <> +
+ {[0, 1, 2, 3].map(i => ( +
+ ))} +
+

+ {strength.label} +

+ + )} + + {errors.password &&

{errors.password}

} +
+ + +
+ + +
+ { + onChange("password2", e.target.value); + setErrors(p => ({...p, password2: ""})); + }} + autoComplete="new-password" + /> + setShow2(v => !v)}/> +
+ + {data.password2 && ( +

+ {matched ? "✓ Совпадают" : "✗ Не совпадают"} +

+ )} + + {errors.password2 &&

{errors.password2}

} +
+ + + + ); +} + +// ─── step 2: confirm ────────────────────────────────────────────────────────── +function RegStep2({data, onSubmit, onBack, loading, error}) { + return ( + <> +
+
+ Имя + {data.name} +
+
+
+ Логин + @{data.login} +
+
+ {error &&

{error}

} + + + + ); +} + +// ─── success ────────────────────────────────────────────────────────────────── +function RegSuccess({name, onLogin}) { + return ( +
+ success +

Добро пожаловать, {name}!

+

Аккаунт создан успешно.

+ +
+ ); +} + +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 ( +
+ +
+ ); + } + + return ( +
+

Создайте аккаунт

+ + + {step === 0 && ( + setStep(1)} /> + )} + {step === 1 && ( + register({ name: form.name, login: form.login, password: form.password })} + onBack={() => setStep(0)} + loading={loading} + error={error} + /> + )} + + {step === 0 && ( +

+ Уже есть аккаунт?{" "} + +

+ )} +
+ ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 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 ( +
+
+ + {FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => ( + + ))} + +
+ + +
+ +
+

Находка

+ + {mode === "login" + ? switchTo("register")} /> + : switchTo("login")} /> + } +
+
+ ); +} diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx deleted file mode 100644 index 9b2a347..0000000 --- a/src/pages/LoginPage.jsx +++ /dev/null @@ -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 ( - -
-
- - {FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => ( - - ))} - -
- - -
- -
- -

Находка

- -
{ e.preventDefault(); login(loginValue, password); }} noValidate> -
- setEmail(e.target.value)} - style={s.input} className="li" - autoComplete="off" name="naidishchey-email-x7k" - readOnly onFocus={(e) => e.target.removeAttribute("readOnly")} - /> -
- -
- setPassword(e.target.value)} - style={{ ...s.input, paddingRight: "44px" }} className="li" - autoComplete="off" name="naidishchey-pass-x7k" - readOnly onFocus={(e) => e.target.removeAttribute("readOnly")} - /> - -
- - {error &&

{error}

} - - -
- - - -

- Нет аккаунта? Зарегистрироваться -

- -
-
- ); -} \ No newline at end of file diff --git a/src/styles/auth.css b/src/styles/auth.css new file mode 100644 index 0000000..d7f5ec4 --- /dev/null +++ b/src/styles/auth.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/loginStyles.js b/src/styles/loginStyles.js deleted file mode 100644 index ba45b63..0000000 --- a/src/styles/loginStyles.js +++ /dev/null @@ -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", - }, -}; \ No newline at end of file