Files
nahodka-front/src/components/AuthModal.jsx
2026-10-06 01:00:10 +03:00

246 lines
9.0 KiB
JavaScript

// src/components/AuthModal.jsx
import { useState, useEffect } from "react";
import { useTranslation } from 'react-i18next';
import CloseIcon from "./icons/CloseIcon";
import { useAuthContext } from "../context/AuthContext";
import { login as apiLogin, register as apiRegister } from "../api/apiClient";
/**
* AuthModal — модальное окно входа и регистрации прямо на странице ленты.
* Открывается когда неавторизованный пользователь пытается создать объявление.
*/
export default function AuthModal({ onClose }) {
const { t } = useTranslation();
const [mode, setMode] = useState("login"); // "login" | "register"
const { login: saveAuth } = useAuthContext();
useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => { document.body.style.overflow = prev; };
}, []);
return (
<div className="f-modal-bg auth-modal-bg" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="f-modal auth-modal">
<div className="f-modal-head">
<span className="f-modal-title">
{mode === "login" ? t('auth.loginModalTitle') : t('auth.registerModalTitle')}
</span>
<button className="f-close-btn" type="button" onClick={onClose}>
<CloseIcon />
</button>
</div>
{mode === "login"
? <LoginFormInline
saveAuth={saveAuth}
onClose={onClose}
onSwitch={() => setMode("register")}
/>
: <RegisterFormInline
saveAuth={saveAuth}
onClose={onClose}
onSwitch={() => setMode("login")}
/>
}
</div>
</div>
);
}
// ─── Login form (inline modal) ──────────────────────────────────────────────
function LoginFormInline({ saveAuth, onClose, onSwitch }) {
const { t } = useTranslation();
const [login, setLogin] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
if (!login || !password) { setError(t('auth.enterLoginPassword')); return; }
setLoading(true); setError("");
try {
const { token, refreshToken, user } = await apiLogin(
login, password, t('auth.invalidCredentials')
);
saveAuth(user, token, refreshToken);
onClose();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div className="f-m-group">
<label className="f-m-label">{t('auth.loginLabel')}</label>
<input
className="f-m-field"
type="text"
placeholder={t('auth.yourLogin')}
value={login}
onChange={e => { setLogin(e.target.value); setError(""); }}
autoComplete="off"
/>
</div>
<div className="f-m-group">
<label className="f-m-label">{t('auth.passwordLabel')}</label>
<input
className="f-m-field"
type="password"
placeholder={t('auth.yourPassword')}
value={password}
onChange={e => { setPassword(e.target.value); setError(""); }}
/>
</div>
{error && <p className="f-err-msg auth-form-error">{error}</p>}
<SubmitButton loading={loading} label={t('auth.login')} />
<p style={{textAlign: "center", marginTop: 14, color: "var(--muted)", fontSize: ".85rem"}}>
{t('auth.noAccount')}{" "}
<button type="button" onClick={onSwitch}
style={{background: "none", border: "none", color: "var(--teal)", cursor: "pointer", fontWeight: 700, fontSize: ".85rem", fontFamily: "var(--font)",}}
>
{t('auth.register')}
</button>
</p>
</form>
);
}
// ─── Register form (inline modal) ──────────────────────────────────────────
function RegisterFormInline({ saveAuth, onClose, onSwitch }) {
const { t } = useTranslation();
const [form, setForm] = useState({
name: "", login: "", password: "", password2: "",
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [errors, setErrors] = useState({});
const setField = (key, val) => {
setForm(p => ({ ...p, [key]: val }));
setErrors(p => ({ ...p, [key]: "" }));
};
const validate = () => {
const e = {};
if (!form.name.trim()) e.name = t('auth.validateNameRequired');
if (!form.login.trim()) e.login = t('auth.validateLoginRequired');
else if (form.login.length < 3) e.login = t('auth.loginMinChars');
if (form.password.length < 6) e.password = t('auth.validatePasswordMin');
if (form.password !== form.password2) e.password2 = t('auth.validatePasswordMatch');
setErrors(e);
return !Object.keys(e).length;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
setLoading(true); setError("");
try {
await apiRegister(form.name, form.login, form.password, t('auth.registrationError'));
let session;
try {
session = await apiLogin(form.login, form.password, t('auth.autoLoginFailed'));
} catch {
throw new Error(t('auth.accountCreatedLoginManually'));
}
const { token, refreshToken, user } = session;
saveAuth(user, token, refreshToken);
onClose();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div className="f-m-group">
<label className="f-m-label">{t('auth.nameLabel')}</label>
<input
className={`f-m-field${errors.name ? " err" : ""}`}
type="text"
placeholder={t('auth.namePlaceholder')}
value={form.name}
onChange={e => setField("name", e.target.value)}
/>
{errors.name && <p className="f-err-msg">{errors.name}</p>}
</div>
<div className="f-m-group">
<label className="f-m-label">{t('auth.loginLabel')}</label>
<input
className={`f-m-field${errors.login ? " err" : ""}`}
type="text"
placeholder={t('auth.loginPlaceholder')}
value={form.login}
onChange={e => setField("login", e.target.value)}
/>
{errors.login && <p className="f-err-msg">{errors.login}</p>}
</div>
<div className="f-m-row">
<div className="f-m-group">
<label className="f-m-label">{t('auth.passwordLabel')}</label>
<input
className={`f-m-field${errors.password ? " err" : ""}`}
type="password"
placeholder={t('auth.passwordMinShort')}
value={form.password}
onChange={e => setField("password", e.target.value)}
/>
{errors.password && <p className="f-err-msg">{errors.password}</p>}
</div>
<div className="f-m-group">
<label className="f-m-label">{t('auth.repeatLabel')}</label>
<input
className={`f-m-field${errors.password2 ? " err" : ""}`}
type="password"
placeholder={t('auth.repeatPlaceholder')}
value={form.password2}
onChange={e => setField("password2", e.target.value)}
/>
{errors.password2 && <p className="f-err-msg">{errors.password2}</p>}
</div>
</div>
{error && <p className="f-err-msg auth-form-error">{error}</p>}
<SubmitButton loading={loading} label={t('auth.createAccountBtn')} />
<p style={{textAlign: "center", marginTop: 14, color: "var(--muted)", fontSize: ".85rem"}}>
{t('auth.hasAccount')}{" "}
<button type="button" onClick={onSwitch}
style={{background: "none", border: "none", color: "var(--teal)", cursor: "pointer", fontWeight: 700, fontSize: ".85rem", fontFamily: "var(--font)",}}>
{t('auth.loginLink')}
</button>
</p>
</form>
);
}
// ─── Shared submit button ──────────────────────────────────────────────────
function SubmitButton({ loading, label }) {
return (
<button className="f-m-submit auth-submit-btn" type="submit" disabled={loading}>
<div className="f-m-submit-row">
{loading && <div className="f-m-spinner" />}
{label}
</div>
</button>
);
}