337 lines
12 KiB
JavaScript
337 lines
12 KiB
JavaScript
"use client";
|
||
|
||
import React, { useEffect, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { FaUserCircle } from "react-icons/fa";
|
||
import TabBar from "../components/TabBar";
|
||
|
||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||
|
||
const ProfileSettingsPage = () => {
|
||
const router = useRouter();
|
||
|
||
const [avatarUrl, setAvatarUrl] = useState("");
|
||
const [firstName, setFirstName] = useState("");
|
||
const [lastName, setLastName] = useState("");
|
||
const [email, setEmail] = useState("");
|
||
const [phone, setPhone] = useState("");
|
||
const [address, setAddress] = useState("");
|
||
const [city, setCity] = useState("");
|
||
|
||
const [bio, setBio] = useState("");
|
||
|
||
const [loading, setLoading] = useState(true);
|
||
const [saveLoading, setSaveLoading] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const [success, setSuccess] = useState("");
|
||
|
||
useEffect(() => {
|
||
const fetchProfile = async () => {
|
||
if (!API_BASE) {
|
||
setError("API_BASE_URL не задан");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
const saved =
|
||
typeof window !== "undefined"
|
||
? localStorage.getItem("authUser")
|
||
: null;
|
||
const authUser = saved ? JSON.parse(saved) : null;
|
||
const accessToken = authUser?.accessToken;
|
||
if (!accessToken) {
|
||
setError("Вы не авторизованы");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/users/me`, {
|
||
headers: {
|
||
Accept: "application/json",
|
||
Authorization: `Bearer ${accessToken}`,
|
||
},
|
||
});
|
||
|
||
if (!res.ok) {
|
||
let msg = "Не удалось загрузить профиль";
|
||
try {
|
||
const data = await res.json();
|
||
if (data.error) msg = data.error;
|
||
} catch {
|
||
const text = await res.text();
|
||
if (text) msg = text;
|
||
}
|
||
setError(msg);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
const data = await res.json(); // UserProfile[file:519]
|
||
setFirstName(data.first_name || "");
|
||
setLastName(data.last_name || "");
|
||
setEmail(data.email || "");
|
||
setPhone(data.phone || "");
|
||
setAddress(data.address || "");
|
||
setCity(data.city || "");
|
||
setBio(data.bio || "");
|
||
setAvatarUrl(data.avatar_url || "");
|
||
setLoading(false);
|
||
} catch (e) {
|
||
setError(e.message || "Ошибка сети");
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchProfile();
|
||
}, []);
|
||
|
||
const handleSave = async (e) => {
|
||
e.preventDefault();
|
||
if (!API_BASE) return;
|
||
|
||
setError("");
|
||
setSuccess("");
|
||
setSaveLoading(true);
|
||
|
||
const saved =
|
||
typeof window !== "undefined"
|
||
? localStorage.getItem("authUser")
|
||
: null;
|
||
const authUser = saved ? JSON.parse(saved) : null;
|
||
const accessToken = authUser?.accessToken;
|
||
if (!accessToken) {
|
||
setError("Вы не авторизованы");
|
||
setSaveLoading(false);
|
||
return;
|
||
}
|
||
|
||
const body = {
|
||
first_name: firstName || undefined,
|
||
last_name: lastName || undefined,
|
||
phone: phone || undefined,
|
||
bio: bio || undefined,
|
||
address: address || undefined,
|
||
city: city || undefined,
|
||
};
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/users/me`, {
|
||
method: "PATCH",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
Authorization: `Bearer ${accessToken}`,
|
||
},
|
||
body: JSON.stringify(body),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
let msg = "Не удалось сохранить профиль";
|
||
try {
|
||
const data = await res.json();
|
||
if (data.error) msg = data.error;
|
||
} catch {
|
||
const text = await res.text();
|
||
if (text) msg = text;
|
||
}
|
||
setError(msg);
|
||
setSaveLoading(false);
|
||
return;
|
||
}
|
||
|
||
setSuccess("Профиль успешно сохранён");
|
||
setSaveLoading(false);
|
||
} catch (err) {
|
||
setError(err.message || "Ошибка сети");
|
||
setSaveLoading(false);
|
||
}
|
||
};
|
||
|
||
const fullName = [firstName, lastName].filter(Boolean).join(" ");
|
||
|
||
return (
|
||
<div className="min-h-screen w-full bg-[#90D2F9] flex justify-center px-4">
|
||
<div className="relative w-full max-w-md flex flex-col pb-20 pt-4">
|
||
{/* Header */}
|
||
<header className="flex items-center mb-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => router.back()}
|
||
className="text-white w-8 h-8 rounded-full flex items-center justify-center text-lg"
|
||
>
|
||
←
|
||
</button>
|
||
<h1 className="flex-1 text-center font-montserrat font-extrabold text-[20px] leading-[24px] text-white">
|
||
Настройки профиля
|
||
</h1>
|
||
<span className="w-8" />
|
||
</header>
|
||
|
||
{/* Карточка настроек */}
|
||
<main className="bg-white rounded-3xl p-4 flex flex-col items-center gap-4 shadow-lg">
|
||
{error && (
|
||
<p className="w-full text-center text-xs font-montserrat text-red-500">
|
||
{error}
|
||
</p>
|
||
)}
|
||
{success && (
|
||
<p className="w-full text-center text-xs font-montserrat text-green-600">
|
||
{success}
|
||
</p>
|
||
)}
|
||
{loading && !error && (
|
||
<p className="w-full text-center text-xs font-montserrat text-black">
|
||
Загрузка профиля...
|
||
</p>
|
||
)}
|
||
|
||
{/* Аватар (пока локально, без API) */}
|
||
<div className="flex flex-col items-center gap-2">
|
||
<div className="w-24 h-24 rounded-full bg-[#E5F3FB] flex items-center justify-center overflow-hidden">
|
||
{avatarUrl ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={avatarUrl}
|
||
alt="Аватар"
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<FaUserCircle className="text-[#72B8E2] w-20 h-20" />
|
||
)}
|
||
</div>
|
||
<label className="font-montserrat text-[12px] text-[#72B8E2] underline cursor-pointer">
|
||
Загрузить аватар
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
className="hidden"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
const url = URL.createObjectURL(file);
|
||
setAvatarUrl(url);
|
||
}}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<form onSubmit={handleSave} className="w-full flex flex-col gap-3">
|
||
{/* Имя */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Имя
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={firstName}
|
||
onChange={(e) => setFirstName(e.target.value)}
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text.white/70 outline-none border border-transparent focus:border-white/70"
|
||
placeholder="Введите имя"
|
||
/>
|
||
</div>
|
||
|
||
{/* Фамилия */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Фамилия
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={lastName}
|
||
onChange={(e) => setLastName(e.target.value)}
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text.white/70 outline-none border border-transparent focus:border-white/70"
|
||
placeholder="Введите фамилию"
|
||
/>
|
||
</div>
|
||
|
||
{/* Почта (только чтение) */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Почта
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={email}
|
||
disabled
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white opacity-70 cursor-not-allowed"
|
||
/>
|
||
</div>
|
||
|
||
{/* Телефон */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Телефон
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
value={phone}
|
||
onChange={(e) => setPhone(e.target.value)}
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm.font-montserrat text-white placeholder:text.white/70 outline-none border border-transparent focus:border-white/70"
|
||
placeholder="+7 (900) 000-00-00"
|
||
/>
|
||
</div>
|
||
|
||
{/* Адрес */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Адрес
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={address}
|
||
onChange={(e) => setAddress(e.target.value)}
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm.font-montserrat text-white placeholder:text.white/70 outline-none border border-transparent focus:border-white/70"
|
||
placeholder="Улица, дом, квартира"
|
||
/>
|
||
</div>
|
||
|
||
{/* Город */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
Город
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={city}
|
||
onChange={(e) => setCity(e.target.value)}
|
||
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text.white/70 outline-none border border-transparent focus:border-white/70"
|
||
placeholder="Например: Пермь"
|
||
/>
|
||
</div>
|
||
|
||
{/* О себе */}
|
||
<div className="flex flex-col gap-1">
|
||
<label className="font-montserrat text-[12px] text-black">
|
||
О себе
|
||
</label>
|
||
<textarea
|
||
value={bio}
|
||
onChange={(e) => setBio(e.target.value)}
|
||
rows={3}
|
||
className="w-full rounded-3xl bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70 resize-none"
|
||
placeholder="Расскажите о себе"
|
||
/>
|
||
</div>
|
||
|
||
{/* Кнопка сохранить */}
|
||
<button
|
||
type="submit"
|
||
disabled={saveLoading}
|
||
className="mt-2 w-full bg-[#94E067] rounded-full py-2.5 flex items-center justify-center disabled:opacity-60"
|
||
>
|
||
<span className="font-montserrat font-extrabold text-[14px] text-white">
|
||
{saveLoading ? "Сохранение..." : "Сохранить изменения"}
|
||
</span>
|
||
</button>
|
||
</form>
|
||
</main>
|
||
|
||
<TabBar />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProfileSettingsPage;
|