global refactoring

This commit is contained in:
SlimusMinus
2026-06-02 01:10:56 +03:00
parent b53207166b
commit fbdcada3d2
26 changed files with 738 additions and 559 deletions

View File

@@ -1,380 +0,0 @@
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,138 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import "../styles/feed.css";
import { API_URL } from "../config/constants";
import Sidebar from "../components/Sidebar";
import RightSidebar from "../components/RightSidebar";
import AdCard from "../components/AdCard";
import PostModal from "../components/PostModal";
import MenuIcon from "../components/icons/MenuIcon";
export default function FeedPage() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeNav, setActiveNav] = useState("Главная");
const [modalOpen, setModalOpen] = useState(false);
const [editAd, setEditAd] = useState(null);
const [ads, setAds] = useState([]);
const [liked, setLiked] = useState(new Set());
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const loadFeed = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await fetch(`${API_URL}/api/v1/posts`);
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
const data = await res.json();
setAds(Array.isArray(data) ? data : (data.items ?? []));
} catch (err) {
setError(err.message || "Не удалось загрузить объявления");
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadFeed(); }, [loadFeed]);
const toggleLike = useCallback((id) => {
setLiked(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}, []);
// перезагружаем ленту чтобы получить images из БД
const handleNewAd = useCallback(() => loadFeed(), [loadFeed]);
const handleEditSave = useCallback(() => loadFeed(), [loadFeed]);
const renderContent = () => {
if (loading) {
return (
<div className="f-center">
<div className="f-spinner-wrap">
<div className="f-spinner" />
Загружаем объявления...
</div>
</div>
);
}
if (error) {
return (
<div className="f-center">
<div className="f-center-icon">⚠️</div>
<div className="f-center-title">Не удалось загрузить ленту</div>
<div className="f-center-sub">{error}</div>
</div>
);
}
if (!ads.length) {
return (
<div className="f-center">
<div className="f-center-icon">🔍</div>
<div className="f-center-title">Объявлений пока нет</div>
<div className="f-center-sub">
Будьте первым — разместите объявление<br />о потере или находке
</div>
</div>
);
}
return ads.map(ad => (
<AdCard
key={ad.id}
ad={ad}
liked={liked.has(ad.id)}
onLike={toggleLike}
onEdit={setEditAd}
/>
));
};
return (
<div className="f-app">
<Sidebar
open={sidebarOpen}
activeNav={activeNav}
onClose={() => setSidebarOpen(false)}
onNavClick={setActiveNav}
/>
<div className="f-main">
<div className="f-topbar">
<button className="f-menu-btn" onClick={() => setSidebarOpen(v => !v)}>
<MenuIcon />
</button>
<div className="f-topbar-center">
<button className="f-post-btn" onClick={() => setModalOpen(true)}>
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
Разместить объявление
</button>
</div>
<div className="f-topbar-spacer" />
</div>
<div className="f-wrap">
<div className="f-feed">
{renderContent()}
</div>
<RightSidebar />
</div>
</div>
{modalOpen && (
<PostModal
onClose={() => setModalOpen(false)}
onSuccess={handleNewAd}
/>
)}
{editAd && (
<PostModal
initialData={editAd}
onClose={() => setEditAd(null)}
onSuccess={handleEditSave}
/>
)}
</div>
);
}