- Заявка от {request.createdAt}
+ const urgencyText = (() => {
+ switch (request.urgency) {
+ case "low":
+ return "Низкая";
+ case "medium":
+ return "Средняя";
+ case "high":
+ return "Высокая";
+ case "urgent":
+ return "Срочно";
+ default:
+ return null;
+ }
+ })();
+
+ const place = [request.address, request.city].filter(Boolean).join(", ");
+ const requesterName = request.requesterName || "Заявитель";
+
+ return (
+
+ );
};
export default RequestDetailsModal;
diff --git a/app/components/acceptPopUp.jsx b/app/components/acceptPopUp.jsx
index 8220f01..310e19d 100644
--- a/app/components/acceptPopUp.jsx
+++ b/app/components/acceptPopUp.jsx
@@ -3,9 +3,55 @@
import React from "react";
import { FaTimesCircle } from "react-icons/fa";
-const AcceptPopup = ({ request, isOpen, onClose, onAccept }) => {
+const AcceptPopup = ({ request, isOpen, onClose, onAccept, loading, error }) => {
if (!isOpen || !request) return null;
+ const title = request.title;
+ const description =
+ request.description ||
+ "Описание недоступно. Откройте заявку для подробностей.";
+
+ const baseAddress = request.address || "Адрес не указан";
+ const city = request.city ? `, ${request.city}` : "";
+ const place = `${baseAddress}${city}`;
+
+ const deadline = request.desired_completion_date
+ ? new Date(request.desired_completion_date).toLocaleString("ru-RU", {
+ day: "2-digit",
+ month: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ : "Не указано";
+
+ const phone = request.contact_phone || request.phone;
+ const contactNotes = request.contact_notes || request.contactNotes;
+
+ const urgencyText = (() => {
+ switch (request.urgency) {
+ case "low":
+ return "Низкая";
+ case "medium":
+ return "Средняя";
+ case "high":
+ return "Высокая";
+ case "urgent":
+ return "Срочно";
+ default:
+ return null;
+ }
+ })();
+
+ const handleClick = () => {
+ // здесь видно, с каким id ты стучишься в /requests/{id}/responses
+ console.log("Отклик на заявку из попапа:", {
+ id: request.id,
+ title: request.title,
+ raw: request,
+ });
+ onAccept(request);
+ };
+
return (
{/* затемнение */}
@@ -30,68 +76,71 @@ const AcceptPopup = ({ request, isOpen, onClose, onAccept }) => {
Задача
- {request.title}
+ {title}
- {/* Сумма и время */}
-
+ {/* Только время выполнить до */}
+
-
- Сумма
-
-
- {request.amount || "2000 ₽"}
-
-
-
-
+
Выполнить до
- {request.deadline || "17:00"}
+ {deadline}
- {/* Список покупок / описание */}
+ {/* Описание + доп.инфа */}
- {request.description ||
- "Необходимо приобрести:\n1. Белый хлеб\n2. Молоко\n3. Колбаса\n4. Фрукты"}
+ {description}
+
+ {urgencyText && (
+
+ Срочность:
+ {urgencyText}
+
+ )}
+ {phone && (
+
+ Телефон:
+ {phone}
+
+ )}
+ {contactNotes && (
+
+ Комментарий к контакту:
+ {contactNotes}
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
- {/* Данные человека */}
+ {/* Данные места */}
Данные:
-
- ФИО: {request.fullName || "Клавдия Березова"}
-
- Место: {request.address}
+ Место: {place}
- {request.flat && (
-
- кв: {request.flat}
-
- )}
- {request.floor && (
-
- Этаж: {request.floor}
-
- )}
{/* Кнопка отклика внизу */}
diff --git a/app/context/AuthContext.jsx b/app/context/AuthContext.jsx
index 10862aa..7197f48 100644
--- a/app/context/AuthContext.jsx
+++ b/app/context/AuthContext.jsx
@@ -5,71 +5,133 @@ import { useRouter } from "next/navigation";
const AuthContext = createContext(null);
-// фейковые пользователи (3 логина/пароля)
-const USERS = [
- {
- id: 1,
- role: "user", // обычный пользователь
- name: "Пользователь",
- login: "user@mail.com",
- password: "user123",
- },
- {
- id: 2,
- role: "volunteer",
- name: "Волонтёр",
- login: "vol@mail.com",
- password: "vol123",
- },
- {
- id: 3,
- role: "moderator",
- name: "Модератор",
- login: "mod@mail.com",
- password: "mod123",
- },
-];
+// базовый URL из YAML (у себя можешь вынести в .env)
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
export const AuthProvider = ({ children }) => {
- const [user, setUser] = useState(null); // {id, role, name, login}
+ const [user, setUser] = useState(null); // {id, email, role, name, accessToken, refreshToken}
const [loading, setLoading] = useState(true);
const router = useRouter();
- // Поднимаем пользователя из localStorage, чтобы контекст сохранялся между перезагрузками
+ // поднимаем пользователя из localStorage
useEffect(() => {
- const saved = typeof window !== "undefined" ? localStorage.getItem("authUser") : null;
+ const saved =
+ typeof window !== "undefined"
+ ? localStorage.getItem("authUser")
+ : null;
if (saved) {
setUser(JSON.parse(saved));
}
setLoading(false);
}, []);
- const login = async (login, password) => {
- // имитация запроса на бэк
- const found = USERS.find(
- (u) => u.login === login && u.password === password
- );
- if (!found) {
- throw new Error("Неверный логин или пароль");
+ // основная авторизация: запрос на /auth/login
+ const login = async (email, password) => {
+ const res = await fetch(`${API_BASE}/auth/login`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({ email, password }),
+ });
+
+ // удобно смотреть в Postman: этот же URL, метод, тело из JSON[file:519]
+ // в Postman просто скопируй URL и тело — увидишь точный JSON-ответ
+
+ if (!res.ok) {
+ // читаем тело как текст, чтобы в консоли / Postman было понятно
+ let errorMessage = "Неверный логин или пароль";
+ try {
+ const data = await res.json();
+ if (data.error) {
+ errorMessage = data.error;
+ }
+ } catch {
+ const text = await res.text();
+ if (text) errorMessage = text;
+ }
+ throw new Error(errorMessage);
}
+ const data = await res.json();
+ // ожидаемый формат по YAML: AuthResponse[file:519]
+ // Примерно:
+ // {
+ // "access_token": "...",
+ // "refresh_token": "...",
+ // "token_type": "bearer",
+ // "user": { "id": 1, "email": "...", ... }
+ // }
+
const authUser = {
- id: found.id,
- role: found.role,
- name: found.name,
- login: found.login,
+ id: data.user?.id,
+ email: data.user?.email,
+ name: data.user?.first_name || data.user?.email,
+ // роль пока не знаем наверняка — вытащим отдельным запросом
+ role: null,
+ accessToken: data.access_token,
+ refreshToken: data.refresh_token,
};
+ // 1) сохраняем токены/пользователя
setUser(authUser);
localStorage.setItem("authUser", JSON.stringify(authUser));
- // после логина перенаправляем на стартовую страницу по роли
- if (found.role === "user") router.push("/home");
- if (found.role === "volunteer") router.push("/mainValounter");
- if (found.role === "moderator") router.push("/moderatorMain");
+ // 2) тянем роли пользователя (GET /users/me/roles)[file:519]
+ try {
+ const rolesRes = await fetch(`${API_BASE}/users/me/roles`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${data.access_token}`,
+ Accept: "application/json",
+ },
+ });
+
+ if (rolesRes.ok) {
+ const roles = await rolesRes.json(); // массив объектов Role[file:519]
+ // ищем первую подходящую роль
+ const roleNames = roles.map((r) => r.name);
+ let appRole = null;
+ if (roleNames.includes("requester")) appRole = "requester";
+ if (roleNames.includes("volunteer")) appRole = "volunteer";
+ if (roleNames.includes("moderator")) appRole = "moderator";
+ if (roleNames.includes("admin")) appRole = "moderator"; // можно перекинуть в модераторский интерфейс
+
+ const updatedUser = { ...authUser, role: appRole };
+ setUser(updatedUser);
+ localStorage.setItem("authUser", JSON.stringify(updatedUser));
+
+ // 3) редирект по роли (как у тебя было)
+ if (appRole === "requester") router.push("/home");
+ else if (appRole === "volunteer") router.push("/mainValounter");
+ else if (appRole === "moderator") router.push("/moderatorMain");
+ else router.push("/home"); // запасной вариант
+ } else {
+ // если роли не достали, всё равно пускаем как обычного пользователя
+ router.push("/home");
+ }
+ } catch (e) {
+ console.error("Ошибка получения ролей:", e);
+ router.push("/home");
+ }
};
- const logout = () => {
+ const logout = async () => {
+ try {
+ if (user?.accessToken) {
+ await fetch(`${API_BASE}/auth/logout`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${user.accessToken}`,
+ Accept: "application/json",
+ },
+ });
+ }
+ } catch (e) {
+ console.error("Ошибка logout:", e);
+ }
+
setUser(null);
localStorage.removeItem("authUser");
router.push("/login");
diff --git a/app/createRequest/page.jsx b/app/createRequest/page.jsx
index 79ff8b8..cb18b8c 100644
--- a/app/createRequest/page.jsx
+++ b/app/createRequest/page.jsx
@@ -1,38 +1,187 @@
"use client";
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
-import { FaClock, FaNewspaper, FaHome, FaCog, FaBell, FaUser } from "react-icons/fa";
+import { FaBell, FaUser } from "react-icons/fa";
import TabBar from "../components/TabBar";
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
+
const CreateRequestPage = () => {
const router = useRouter();
const [title, setTitle] = useState("");
- const [date, setDate] = useState("");
+ const [date, setDate] = useState(""); // desired_completion_date
const [time, setTime] = useState("");
const [description, setDescription] = useState("");
- const [note, setNote] = useState("");
+ const [note, setNote] = useState(""); // contact_notes
- const isFormValid = title && date && time && description;
+ const [address, setAddress] = useState("");
+ const [city, setCity] = useState("");
+ const [phone, setPhone] = useState("");
+ const [urgency, setUrgency] = useState("medium");
+ const [latitude, setLatitude] = useState("");
+ const [longitude, setLongitude] = useState("");
+ const [geoError, setGeoError] = useState("");
- const handleSubmit = (e) => {
+ const [error, setError] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [userName, setUserName] = useState("Пользователь");
+ const [profileError, setProfileError] = useState("");
+
+ const isFormValid =
+ title &&
+ date &&
+ time &&
+ description &&
+ address &&
+ city &&
+ urgency &&
+ latitude &&
+ longitude;
+
+ // профиль
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) return;
+
+ const saved =
+ typeof window !== "undefined"
+ ? localStorage.getItem("authUser")
+ : null;
+ const authUser = saved ? JSON.parse(saved) : null;
+ const accessToken = authUser?.accessToken;
+ if (!accessToken) return;
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!res.ok) {
+ setProfileError("Не удалось загрузить профиль");
+ return;
+ }
+
+ const data = await res.json();
+ const fullName =
+ [data.first_name, data.last_name]
+ .filter(Boolean)
+ .join(" ")
+ .trim() || data.email;
+ setUserName(fullName);
+ } catch (e) {
+ setProfileError("Ошибка загрузки профиля");
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ useEffect(() => {
+ if (!("geolocation" in navigator)) {
+ setGeoError("Геолокация не поддерживается браузером");
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ (pos) => {
+ const { latitude, longitude } = pos.coords;
+ setLatitude(latitude.toFixed(6));
+ setLongitude(longitude.toFixed(6));
+ setGeoError("");
+ },
+ (err) => {
+ console.error("Geolocation error:", err);
+ setGeoError("Не удалось получить геолокацию, введите координаты вручную");
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 60000,
+ }
+ );
+ }, []);
+
+ const handleSubmit = async (e) => {
e.preventDefault();
- if (!isFormValid) return;
+ if (!isFormValid || !API_BASE) return;
- console.log({
- title,
- date,
- time,
- description,
- note,
- });
- // TODO: запрос на бэк
+ try {
+ setError("");
+ setIsSubmitting(true);
+
+ const saved =
+ typeof window !== "undefined"
+ ? localStorage.getItem("authUser")
+ : null;
+ const authUser = saved ? JSON.parse(saved) : null;
+ const accessToken = authUser?.accessToken;
+
+ if (!accessToken) {
+ setError("Вы не авторизованы");
+ setIsSubmitting(false);
+ return;
+ }
+
+ const desiredDateTime = new Date(`${date}T${time}:00`);
+ const desired_completion_date = desiredDateTime.toISOString();
+
+ const body = {
+ request_type_id: 1, // можно потом вынести в селект
+ title,
+ description,
+ latitude: Number(latitude),
+ longitude: Number(longitude),
+ address,
+ city,
+ desired_completion_date,
+ urgency, // low | medium | high | urgent
+ contact_phone: phone || null,
+ contact_notes: note || null,
+ };
+
+ const res = await fetch(`${API_BASE}/requests`, {
+ method: "POST",
+ 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);
+ setIsSubmitting(false);
+ return;
+ }
+
+ const created = await res.json();
+ console.log("Заявка создана:", created);
+
+ router.push("/home");
+ } catch (err) {
+ setError(err.message || "Ошибка сети");
+ setIsSubmitting(false);
+ }
};
return (
-
+
diff --git a/app/historyRequest/page.jsx b/app/historyRequest/page.jsx
index 1694663..13e82f4 100644
--- a/app/historyRequest/page.jsx
+++ b/app/historyRequest/page.jsx
@@ -1,94 +1,163 @@
"use client";
-import React, { useState } from "react";
-import { FaBell, FaUser, FaStar } from "react-icons/fa";
+import React, { useState, useEffect } from "react";
+import { FaBell, FaUser } from "react-icons/fa";
import TabBar from "../components/TabBar";
import RequestDetailsModal from "../components/RequestDetailsModal";
-const requests = [
- {
- id: 1,
- title: "Приобрести продукты пенсионерке",
- status: "Отклонена",
- statusColor: "#FF8282",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- rejectReason: "Адрес вне зоны обслуживания",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 2,
- title: "Приобрести продукты пенсионерке",
- status: "Принята",
- statusColor: "#94E067",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 3,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 4,
- title: "Приобрести продукты пенсионерке",
- status: "Выполнена",
- statusColor: "#71A5E9",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 5,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 6,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 7,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
-];
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
+
+// маппинг статусов API -> текст/цвет для UI
+const statusMap = {
+ pending_moderation: { label: "На модерации", color: "#E9D171" },
+ approved: { label: "Принята", color: "#94E067" },
+ in_progress: { label: "В процессе", color: "#E971E1" },
+ completed: { label: "Выполнена", color: "#71A5E9" },
+ cancelled: { label: "Отменена", color: "#FF8282" },
+ rejected: { label: "Отклонена", color: "#FF8282" },
+};
const HistoryRequestPage = () => {
+ const [requests, setRequests] = useState([]);
const [selectedRequest, setSelectedRequest] = useState(null);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
- const handleOpen = (req) => {
- setSelectedRequest(req);
- };
+ const [userName, setUserName] = useState("Пользователь");
+ const [profileError, setProfileError] = useState("");
- const handleClose = () => {
- setSelectedRequest(null);
- };
+ // профиль: /users/me
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) return;
+
+ const saved =
+ typeof window !== "undefined"
+ ? localStorage.getItem("authUser")
+ : null;
+ const authUser = saved ? JSON.parse(saved) : null;
+ const accessToken = authUser?.accessToken;
+ if (!accessToken) return;
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!res.ok) {
+ setProfileError("Не удалось загрузить профиль");
+ return;
+ }
+
+ const data = await res.json();
+ const fullName =
+ [data.first_name, data.last_name]
+ .filter(Boolean)
+ .join(" ")
+ .trim() || data.email;
+ setUserName(fullName);
+ } catch {
+ setProfileError("Ошибка загрузки профиля");
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ // заявки: /requests/my
+ useEffect(() => {
+ const fetchRequests = 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}/requests/my`, {
+ 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();
+ console.log("requests/my data:", data);
+
+ const mapped = data.map((item) => {
+ const rawStatus =
+ typeof item.status === "string"
+ ? item.status
+ : item.status?.request_status;
+
+ const m = statusMap[rawStatus] || {
+ label: rawStatus || "unknown",
+ color: "#E2E2E2",
+ };
+
+ const created = new Date(item.created_at);
+ const createdAt = created.toLocaleDateString("ru-RU");
+ const time = created.toLocaleTimeString("ru-RU", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ return {
+ id: item.id,
+ title: item.title,
+ status: m.label,
+ statusColor: m.color,
+ createdAt,
+ date: createdAt,
+ time,
+ description: item.description,
+ // если позже появятся причина/оценка — можно добавить сюда
+ };
+ });
+
+ setRequests(mapped);
+ setLoading(false);
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setLoading(false);
+ }
+ };
+
+ fetchRequests();
+ }, []);
+
+ const handleOpen = (req) => setSelectedRequest(req);
+ const handleClose = () => setSelectedRequest(null);
return (
@@ -99,9 +168,16 @@ const HistoryRequestPage = () => {
-
- Александр
-
+
+
+ {userName}
+
+ {profileError && (
+
+ {profileError}
+
+ )}
+
{
История заявок
+ {error && (
+
+ {error}
+
+ )}
+
{/* Список заявок */}
+ {loading && (
+
+ Загрузка истории...
+
+ )}
+
+ {!loading && requests.length === 0 && !error && (
+
+ История модерации пуста
+
+ )}
+
{requests.map((req) => (
handleOpen(req)}
- className="w-full text-left bg-white rounded-xl px-3 py-2 flex flex-col gap-1"
+ className="w-full text-left bg.white rounded-xl px-3 py-2 flex flex-col gap-1"
>
- {/* верхняя строка: статус + дата/время */}
{
>
{req.status}
-
+
{req.date}
@@ -116,12 +236,10 @@ const HistoryRequestModeratorPage = () => {
- {/* Заголовок заявки */}
{req.title}
- {/* Краткое ФИО/адрес */}
{req.fullName}
@@ -129,8 +247,7 @@ const HistoryRequestModeratorPage = () => {
{req.address}
- {/* Кнопка "Развернуть" */}
-
+
Развернуть
@@ -139,13 +256,11 @@ const HistoryRequestModeratorPage = () => {
))}
- {/* Попап модератора */}
{selectedRequest && (
)}
diff --git a/app/moderatorMain/page.jsx b/app/moderatorMain/page.jsx
index bd73e72..4cf6e7a 100644
--- a/app/moderatorMain/page.jsx
+++ b/app/moderatorMain/page.jsx
@@ -1,73 +1,163 @@
"use client";
-import React, { useState } from "react";
-import { FaBell, FaUser, FaStar } from "react-icons/fa";
+import React, { useEffect, useState } from "react";
+import { FaBell, FaUser } from "react-icons/fa";
import TabBar from "../components/TabBar";
import RequestDetailsModal from "../components/ModeratorRequestDetailsModal";
-const requests = [
- {
- id: 1,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 2,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 3,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 4,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 5,
- title: "Приобрести продукты пенсионерке",
- status: "На модерации",
- statusColor: "#E9D171",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- }
-];
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
+
+const statusMap = {
+ pending_moderation: { label: "На модерации", color: "#E9D171" },
+ approved: { label: "Принята", color: "#94E067" },
+ in_progress: { label: "В процессе", color: "#E971E1" },
+ completed: { label: "Выполнена", color: "#71A5E9" },
+ cancelled: { label: "Отменена", color: "#FF8282" },
+ rejected: { label: "Отклонена", color: "#FF8282" },
+};
const HistoryRequestPage = () => {
+ const [requests, setRequests] = useState([]);
const [selectedRequest, setSelectedRequest] = useState(null);
- const handleOpen = (req) => {
- setSelectedRequest(req);
+ const [moderatorName, setModeratorName] = useState("Модератор");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const getAccessToken = () => {
+ if (typeof window === "undefined") return null;
+ const saved = localStorage.getItem("authUser");
+ const authUser = saved ? JSON.parse(saved) : null;
+ return authUser?.accessToken || null;
};
- const handleClose = () => {
- setSelectedRequest(null);
- };
+ // профиль модератора
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) return;
+ const token = getAccessToken();
+ if (!token) return;
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ if (!res.ok) return;
+ const data = await res.json();
+ const fullName =
+ [data.first_name, data.last_name].filter(Boolean).join(" ").trim() ||
+ data.email;
+ setModeratorName(fullName);
+ } catch {
+ // дефолт остаётся
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ // список заявок на модерации
+ useEffect(() => {
+ const fetchRequestsForModeration = async () => {
+ if (!API_BASE) {
+ setError("API_BASE_URL не задан");
+ setLoading(false);
+ return;
+ }
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ // ЗДЕСЬ ИСПРАВЬ ЭНДПОИНТ ПОД СВОЙ БЭК:
+ const res = await fetch(`${API_BASE}/moderation/requests/pending`, {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ const text = await res.text();
+ let data = null;
+ if (text) {
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = null;
+ }
+ }
+
+ if (!res.ok) {
+ let msg = "Не удалось загрузить заявки";
+ if (data && typeof data === "object" && data.error) {
+ msg = data.error;
+ } else if (text) {
+ msg = text;
+ }
+ setError(msg);
+ setLoading(false);
+ return;
+ }
+
+ const list = Array.isArray(data) ? data : [];
+
+ // только pending_moderation
+ const pending = list.filter(
+ (item) =>
+ item.status &&
+ item.status.request_status === "pending_moderation"
+ );
+
+ const mapped = pending.map((item) => {
+ const rawStatus = item.status?.request_status || "pending_moderation";
+ const m = statusMap[rawStatus] || {
+ label: rawStatus,
+ color: "#E2E2E2",
+ };
+
+ const created = new Date(item.created_at);
+ const createdAt = created.toLocaleDateString("ru-RU");
+ const time = created.toLocaleTimeString("ru-RU", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ return {
+ id: item.id,
+ title: item.title,
+ description: item.description,
+ status: m.label,
+ statusColor: m.color,
+ createdAt,
+ date: createdAt,
+ time,
+ address: item.address,
+ city: item.city,
+ urgency: item.urgency,
+ rawStatus,
+ };
+ });
+
+ setRequests(mapped);
+ setLoading(false);
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setLoading(false);
+ }
+ };
+
+ fetchRequestsForModeration();
+ }, []);
+
+ const handleOpen = (req) => setSelectedRequest(req);
+ const handleClose = () => setSelectedRequest(null);
return (
@@ -79,7 +169,7 @@ const HistoryRequestPage = () => {
- Александр
+ {moderatorName}
{
- История заявок
+ Активные Заявки
+ {error && (
+
+ {error}
+
+ )}
+
{/* Список заявок */}
+ {loading && (
+
+ Загрузка заявок...
+
+ )}
+
+ {!loading && requests.length === 0 && !error && (
+
+ Заявок на модерации пока нет
+
+ )}
+
{requests.map((req) => (
{
onClick={() => handleOpen(req)}
className="w-full text-left bg-white rounded-xl px-3 py-2 flex flex-col gap-1"
>
- {/* верхняя строка: статус + дата/время */}
{req.status}
@@ -121,12 +228,10 @@ const HistoryRequestPage = () => {
- {/* Заголовок заявки */}
{req.title}
- {/* Кнопка "Развернуть" */}
Развернуть
@@ -136,9 +241,11 @@ const HistoryRequestPage = () => {
))}
- {/* Попап */}
{selectedRequest && (
-
+
)}
@@ -148,91 +255,3 @@ const HistoryRequestPage = () => {
};
export default HistoryRequestPage;
-
-// const RequestDetailsModal = ({ request, onClose }) => {
-// const isDone = request.status === "Выполнена";
-
-// return (
-//
-//
-// {/* Белая карточка */}
-//
-// {/* Шапка попапа */}
-//
-//
-// ←
-//
-//
-// Заявка от {request.createdAt}
-//
-//
-//
-
-// {/* Статус + срок */}
-//
-//
-// Выполнена
-//
-//
-//
-// До {request.date.replace("До ", "")}
-//
-//
-// {request.time}
-//
-//
-//
-
-// {/* Название задачи */}
-//
-// {request.title}
-//
-
-// {/* Блок отзыва */}
-// {isDone && (
-//
-//
-// Отзыв
-//
-//
-// Здесь будет текст отзыва с бэка.
-//
-//
-// )}
-
-// {/* Оценка волонтера */}
-//
-//
-// Оценить волонтера
-//
-//
-// {[1, 2, 3, 4, 5].map((star) => (
-//
-// ))}
-//
-//
-
-// {/* Кнопка оставить отзыв */}
-// {isDone && (
-//
-//
-// Оставить отзыв
-//
-//
-// )}
-//
-//
-//
-// );
-// };
-
diff --git a/app/profileSettings/page.jsx b/app/profileSettings/page.jsx
index 78fc86b..4b23487 100644
--- a/app/profileSettings/page.jsx
+++ b/app/profileSettings/page.jsx
@@ -1,31 +1,155 @@
"use client";
-import React, { useState } from "react";
+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 [fullName, setFullName] = useState("Иванов Александр Сергеевич");
- const [birthDate, setBirthDate] = useState("1990-03-12");
- const [email, setEmail] = useState("example@mail.com");
- const [phone, setPhone] = useState("+7 (900) 000-00-00");
+ 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 handleSave = (e) => {
+ 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();
- console.log("Сохранить профиль:", {
- avatarUrl,
- fullName,
- birthDate,
- email,
- phone,
- });
- // здесь будет запрос на бэк
+ 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 (
@@ -46,7 +170,23 @@ const ProfileSettingsPage = () => {
{/* Карточка настроек */}
- {/* Аватар */}
+ {error && (
+
+ {error}
+
+ )}
+ {success && (
+
+ {success}
+
+ )}
+ {loading && !error && (
+
+ Загрузка профиля...
+
+ )}
+
+ {/* Аватар (пока локально, без API) */}
+ {/* Адрес */}
+
+
+ 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="Улица, дом, квартира"
+ />
+
+
+ {/* Город */}
+
+
+ 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="Например: Пермь"
+ />
+
+
+ {/* О себе */}
+
+
+
+
{/* Кнопка сохранить */}
- Сохранить изменения
+ {saveLoading ? "Сохранение..." : "Сохранить изменения"}
diff --git a/app/reg/page.jsx b/app/reg/page.jsx
index 0e88df6..38579b5 100644
--- a/app/reg/page.jsx
+++ b/app/reg/page.jsx
@@ -4,48 +4,94 @@ import React, { useState } from "react";
import { useRouter } from "next/navigation";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const RegPage = () => {
const router = useRouter();
+ const [firstName, setFirstName] = useState(""); // имя
+ const [lastName, setLastName] = useState(""); // фамилия
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const [checkboxError, setCheckboxError] = useState(false);
+ const [authError, setAuthError] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
const isEmailValid = emailRegex.test(email);
- const isFormValid = isEmailValid && password.length > 0;
+ const isFormValid =
+ isEmailValid &&
+ password.length > 0 &&
+ firstName.trim().length > 0 &&
+ lastName.trim().length > 0;
- const handleSubmit = (e) => {
+ const handleSubmit = async (e) => {
e.preventDefault();
if (!rememberMe) {
setCheckboxError(true);
return;
}
-
setCheckboxError(false);
- if (!isFormValid) return;
+ if (!isFormValid || !API_BASE) return;
- console.log("Email:", email, "Password:", password, "Remember:", rememberMe);
- router.push("/regCode");
+ try {
+ setAuthError("");
+ setIsSubmitting(true);
+
+ const res = await fetch(`${API_BASE}/auth/register`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ email,
+ password,
+ first_name: firstName,
+ last_name: lastName,
+ // остальные поля можно не отправлять, если не обязательные
+ }),
+ });
+
+ 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;
+ }
+ setAuthError(msg);
+ setIsSubmitting(false);
+ return;
+ }
+
+ const data = await res.json();
+ console.log("Регистрация успешна, ответ API:", data);
+ router.push("/regCode");
+ } catch (err) {
+ setAuthError(err.message || "Ошибка сети");
+ setIsSubmitting(false);
+ }
};
return (
- {/* Красный баннер ошибки по чекбоксу */}
- {checkboxError && (
+ {(checkboxError || authError) && (
- Вы не согласны с условиями использования
+ {checkboxError
+ ? "Вы не согласны с условиями использования"
+ : authError}
)}
- {/* Кнопка Назад */}
{
Регистрация
- {/* Пустой блок для выравнивания по центру заголовка */}
diff --git a/app/valounterHistoryRequest/page.jsx b/app/valounterHistoryRequest/page.jsx
index f2ec8cd..65195f0 100644
--- a/app/valounterHistoryRequest/page.jsx
+++ b/app/valounterHistoryRequest/page.jsx
@@ -1,56 +1,148 @@
"use client";
-import React, { useState } from "react";
-import { FaBell, FaUser, FaStar } from "react-icons/fa";
+import React, { useEffect, useState } from "react";
+import { FaBell, FaUser } from "react-icons/fa";
import TabBar from "../components/TabBar";
import RequestDetailsModal from "../components/ValounterRequestDetailsModal";
-const requests = [
- {
- id: 4,
- title: "Приобрести продукты пенсионерке",
- status: "Выполнена",
- statusColor: "#71A5E9",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 5,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 6,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
- {
- id: 7,
- title: "Приобрести продукты пенсионерке",
- status: "В процессе",
- statusColor: "#E971E1",
- date: "До 28.11.2025",
- time: "13:00",
- createdAt: "28.11.2025",
- description: "Купить продукты и принести по адресу.",
- },
-];
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
+
+const statusMap = {
+ pending_moderation: { label: "На модерации", color: "#E9D171" },
+ approved: { label: "Принята", color: "#94E067" },
+ in_progress: { label: "В процессе", color: "#E971E1" },
+ completed: { label: "Выполнена", color: "#71A5E9" },
+ cancelled: { label: "Отменена", color: "#FF8282" },
+ rejected: { label: "Отклонена", color: "#FF8282" },
+};
const HistoryRequestPage = () => {
+ const [userName, setUserName] = useState("Волонтёр");
+
+ const [requests, setRequests] = useState([]); // истории заявок волонтёра
const [selectedRequest, setSelectedRequest] = useState(null);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
+
+ const getAccessToken = () => {
+ if (typeof window === "undefined") return null;
+ const saved = localStorage.getItem("authUser");
+ const authUser = saved ? JSON.parse(saved) : null;
+ return authUser?.accessToken || null;
+ };
+
+ // подгружаем имя
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) return;
+ const token = getAccessToken();
+ if (!token) return;
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ if (!res.ok) return;
+ const data = await res.json();
+ const fullName =
+ [data.first_name, data.last_name]
+ .filter(Boolean)
+ .join(" ")
+ .trim() || data.email;
+ setUserName(fullName);
+ } catch {
+ // оставляем дефолт
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ // загружаем историю заявок волонтёра
+ useEffect(() => {
+ const fetchVolunteerRequests = async () => {
+ if (!API_BASE) {
+ setError("API_BASE_URL не задан");
+ setLoading(false);
+ return;
+ }
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ // вариант 1 (рекомендуется на бэке): отдельный эндпоинт, здесь предположим, что бек отдаёт RequestListItem[]
+ const res = await fetch(`${API_BASE}/requests/my?role=volunteer`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ 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(); // массив RequestListItem[file:519]
+
+ const mapped = data.map((item) => {
+ const m = statusMap[item.status] || {
+ label: item.status,
+ color: "#E2E2E2",
+ };
+
+ const created = new Date(item.created_at);
+ const createdAt = created.toLocaleDateString("ru-RU");
+ const time = created.toLocaleTimeString("ru-RU", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ return {
+ id: item.id,
+ title: item.title,
+ status: m.label,
+ statusColor: m.color,
+ createdAt,
+ date: createdAt,
+ time,
+ description: item.description,
+ address: item.address,
+ city: item.city,
+ requesterName: item.requester_name,
+ requestTypeName: item.request_type_name,
+ rawStatus: item.status,
+ };
+ });
+
+ setRequests(mapped);
+ setLoading(false);
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setLoading(false);
+ }
+ };
+
+ fetchVolunteerRequests();
+ }, []);
+
const handleOpen = (req) => {
setSelectedRequest(req);
};
@@ -69,7 +161,7 @@ const HistoryRequestPage = () => {
- Александр
+ {userName}
{
История заявок
+ {error && (
+
+ {error}
+
+ )}
+
{/* Список заявок */}
+ {loading && (
+
+ Загрузка заявок...
+
+ )}
+
+ {!loading && requests.length === 0 && !error && (
+
+ У вас пока нет заявок
+
+ )}
+
{requests.map((req) => (
handleOpen(req)}
- className="w-full text-left bg-white rounded-xl px-3 py-2 flex flex-col gap-1"
+ className="w-full text-left bg-white rounded-xl px-3.py-2 flex flex-col gap-1"
>
{/* верхняя строка: статус + дата/время */}
@@ -117,7 +227,7 @@ const HistoryRequestPage = () => {
{/* Кнопка "Развернуть" */}
-
+
Развернуть
@@ -138,91 +248,3 @@ const HistoryRequestPage = () => {
};
export default HistoryRequestPage;
-
-// const RequestDetailsModal = ({ request, onClose }) => {
-// const isDone = request.status === "Выполнена";
-
-// return (
-//
-//
-// {/* Белая карточка */}
-//
-// {/* Шапка попапа */}
-//
-//
-// ←
-//
-//
-// Заявка от {request.createdAt}
-//
-//
-//
-
-// {/* Статус + срок */}
-//
-//
-// Выполнена
-//
-//
-//
-// До {request.date.replace("До ", "")}
-//
-//
-// {request.time}
-//
-//
-//
-
-// {/* Название задачи */}
-//
-// {request.title}
-//
-
-// {/* Блок отзыва */}
-// {isDone && (
-//
-//
-// Отзыв
-//
-//
-// Здесь будет текст отзыва с бэка.
-//
-//
-// )}
-
-// {/* Оценка волонтера */}
-//
-//
-// Оценить волонтера
-//
-//
-// {[1, 2, 3, 4, 5].map((star) => (
-//
-// ))}
-//
-//
-
-// {/* Кнопка оставить отзыв */}
-// {isDone && (
-//
-//
-// Оставить отзыв
-//
-//
-// )}
-//
-//
-//
-// );
-// };
-
diff --git a/app/valounterProfilePage/page.jsx b/app/valounterProfilePage/page.jsx
index 0791322..dd4bd3d 100644
--- a/app/valounterProfilePage/page.jsx
+++ b/app/valounterProfilePage/page.jsx
@@ -1,21 +1,95 @@
"use client";
-import React from "react";
+import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { FaUserCircle, FaStar } from "react-icons/fa";
import TabBar from "../components/TabBar";
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
+
const ValounterProfilePage = () => {
const router = useRouter();
- const fullName = "Иванов Александр Сергеевич";
- const birthDate = "12.03.1990";
- const rating = 4.8;
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const getAccessToken = () => {
+ if (typeof window === "undefined") return null;
+ const saved = localStorage.getItem("authUser");
+ const authUser = saved ? JSON.parse(saved) : null;
+ return authUser?.accessToken || null;
+ };
+
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) {
+ setError("API_BASE_URL не задан");
+ setLoading(false);
+ return;
+ }
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ 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]
+ setProfile(data);
+ setLoading(false);
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setLoading(false);
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ const fullName =
+ profile &&
+ ([profile.first_name, profile.last_name].filter(Boolean).join(" ") ||
+ profile.email);
+
+ const rating =
+ profile && profile.volunteer_rating != null
+ ? Number(profile.volunteer_rating)
+ : null;
+
+ const birthDateText = profile?.created_at
+ ? new Date(profile.created_at).toLocaleDateString("ru-RU")
+ : "—";
+
+ const email = profile?.email || "—";
+ const phone = profile?.phone || "—";
return (
- {/* Header с кнопкой назад и заголовком по центру */}
+ {/* Header */}
- {/* Карточка профиля */}
- {/* Аватар */}
-
-
- {/* ФИО и рейтинг */}
-
- {/*
- ФИО
-
*/}
-
- {fullName}
+ {loading && (
+
+ Загрузка профиля...
+ )}
- {/* Рейтинг + звезды */}
-
-
- Рейтинг: {rating.toFixed(1)}
-
-
- {[1, 2, 3, 4, 5].map((star) => (
-
- ))}
+ {error && !loading && (
+
+ {error}
+
+ )}
+
+ {!loading && profile && (
+ <>
+ {/* Аватар */}
+ {profile.avatar_url ? (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ ) : (
+
+ )}
+
+ {/* ФИО и рейтинг */}
+
+
+ {fullName}
+
+
+ {rating != null && (
+
+
+ Рейтинг: {rating.toFixed(1)}
+
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ))}
+
+
+ )}
-
-
- {/* Контакты и день рождения */}
-
-
- Дата рождения: {birthDate}
-
-
- Почта: example@mail.com
-
-
- Телефон: +7 (900) 000-00-00
-
-
+ {/* Контакты и «дата рождения» (условно) */}
+
+
+ Дата регистрации: {birthDateText}
+
+
Почта: {email}
+
+ Телефон: {phone}
+
+
- {/* Кнопки */}
-
- router.push("/valounterProfileSettings")}
- className="w-full bg-[#E0B267] rounded-full py-2 flex items-center justify-center"
- >
-
- Редактировать профиль
-
-
-
-
- Выйти из аккаунта
-
-
-
+ {/* Кнопки */}
+
+ router.push("/valounterProfileSettings")}
+ className="w-full bg-[#E0B267] rounded-full py-2 flex.items-center justify-center"
+ >
+
+ Редактировать профиль
+
+
+ {
+ if (typeof window !== "undefined") {
+ localStorage.removeItem("authUser");
+ }
+ router.push("/");
+ }}
+ >
+
+ Выйти из аккаунта
+
+
+
+ >
+ )}
diff --git a/app/valounterProfileSettings/page.jsx b/app/valounterProfileSettings/page.jsx
index e2f4e58..a29c7f0 100644
--- a/app/valounterProfileSettings/page.jsx
+++ b/app/valounterProfileSettings/page.jsx
@@ -1,31 +1,146 @@
"use client";
-import React, { useState } from "react";
+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 ValounterProfileSettingsPage = () => {
const router = useRouter();
const [avatarUrl, setAvatarUrl] = useState("");
- const [fullName, setFullName] = useState("Иванов Александр Сергеевич");
- const [birthDate, setBirthDate] = useState("1990-03-12");
- const [email, setEmail] = useState("example@mail.com");
- const [phone, setPhone] = useState("+7 (900) 000-00-00");
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [email, setEmail] = useState("");
+ const [phone, setPhone] = useState("");
- const handleSave = (e) => {
- e.preventDefault();
- console.log("Сохранить профиль:", {
- avatarUrl,
- fullName,
- birthDate,
- email,
- phone,
- });
- // здесь будет запрос на бэк
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [saveMessage, setSaveMessage] = useState("");
+
+ const getAccessToken = () => {
+ if (typeof window === "undefined") return null;
+ const saved = localStorage.getItem("authUser");
+ const authUser = saved ? JSON.parse(saved) : null;
+ return authUser?.accessToken || null;
};
+ // загрузить профиль
+ useEffect(() => {
+ const fetchProfile = async () => {
+ if (!API_BASE) {
+ setError("API_BASE_URL не задан");
+ setLoading(false);
+ return;
+ }
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const res = await fetch(`${API_BASE}/users/me`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ 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 || "");
+ setAvatarUrl(data.avatar_url || "");
+ setLoading(false);
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setLoading(false);
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ const handleSave = async (e) => {
+ e.preventDefault();
+ if (!API_BASE) return;
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setError("");
+ setSaveMessage("");
+
+ const body = {
+ first_name: firstName || null,
+ last_name: lastName || null,
+ phone: phone || null,
+ // email обычно не меняют через этот эндпоинт, но если бек разрешает — можно добавить
+ };
+
+ const res = await fetch(`${API_BASE}/users/me`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ 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);
+ setSaving(false);
+ return;
+ }
+
+ const updated = await res.json();
+ setSaveMessage("Изменения сохранены");
+ setSaving(false);
+
+ setFirstName(updated.first_name || "");
+ setLastName(updated.last_name || "");
+ setPhone(updated.phone || "");
+ } catch (e) {
+ setError(e.message || "Ошибка сети");
+ setSaving(false);
+ }
+ };
+
+ const fullName = [firstName, lastName].filter(Boolean).join(" ");
+
return (