Files
frontend/app/mainValounter/page.jsx
fullofempt 0df52352a8 WIPVOLONT
2025-12-14 21:14:55 +05:00

384 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { FaUser, FaCog } from "react-icons/fa";
import TabBar from "../components/TabBar";
import AcceptPopup from "../components/acceptPopUp";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
// динамический импорт карты (только клиент)
const MapContainer = dynamic(
() => import("react-leaflet").then((m) => m.MapContainer),
{ ssr: false }
);
const TileLayer = dynamic(
() => import("react-leaflet").then((m) => m.TileLayer),
{ ssr: false }
);
const Marker = dynamic(
() => import("react-leaflet").then((m) => m.Marker),
{ ssr: false }
);
const Popup = dynamic(
() => import("react-leaflet").then((m) => m.Popup),
{ ssr: false }
);
// центр Перми
const DEFAULT_POSITION = [57.997962, 56.147201];
const MainVolunteerPage = () => {
const [position, setPosition] = useState(DEFAULT_POSITION);
const [hasLocation, setHasLocation] = useState(false);
const [userName, setUserName] = useState("Волонтёр");
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [selectedRequest, setSelectedRequest] = useState(null);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [acceptLoading, setAcceptLoading] = useState(false);
const [acceptError, setAcceptError] = useState("");
// фикс иконок leaflet: только на клиенте
useEffect(() => {
const setupLeafletIcons = async () => {
if (typeof window === "undefined") return;
const L = (await import("leaflet")).default;
await import("leaflet/dist/leaflet.css");
// @ts-ignore
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: "/leaflet/marker-icon-2x.png",
iconUrl: "/leaflet/marker-icon.png",
shadowUrl: "/leaflet/marker-shadow.png",
});
};
setupLeafletIcons();
}, []);
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 openPopup = (req) => {
setSelectedRequest(req);
setIsPopupOpen(true);
setAcceptError("");
};
const closePopup = () => {
setIsPopupOpen(false);
setSelectedRequest(null);
setAcceptError("");
};
// геолокация
useEffect(() => {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
(pos) => {
setPosition([pos.coords.latitude, pos.coords.longitude]);
setHasLocation(true);
},
() => {
setHasLocation(false);
}
);
}, []);
// профиль
useEffect(() => {
const fetchProfile = async () => {
if (!API_BASE) return;
const accessToken = getAccessToken();
if (!accessToken) return;
try {
const res = await fetch(`${API_BASE}/users/me`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
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 fetchNearbyRequests = async () => {
if (!API_BASE) {
setError("API_BASE_URL не задан");
setLoading(false);
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setError("Вы не авторизованы");
setLoading(false);
return;
}
const [lat, lon] = position;
try {
const params = new URLSearchParams({
lat: String(lat),
lon: String(lon),
radius: "5000",
limit: "50",
offset: "0",
});
const res = await fetch(
`${API_BASE}/requests/nearby?${params.toString()}`,
{
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();
const mapped = data.map((item) => ({
id: item.id,
title: item.title,
description: item.description,
address: item.address,
city: item.city,
urgency: item.urgency,
contact_phone: item.contact_phone,
contact_notes: item.contact_notes,
desired_completion_date: item.desired_completion_date,
coords: [item.latitude ?? lat, item.longitude ?? lon],
distance: item.distance_meters
? `${(item.distance_meters / 1000).toFixed(1)} км`
: null,
}));
setRequests(mapped);
setLoading(false);
} catch (e) {
setError(e.message || "Ошибка сети");
setLoading(false);
}
};
fetchNearbyRequests();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position, hasLocation]);
// отклик
// волонтёр СОДАЁТ отклик
const handleAccept = async (req, message = "") => {
if (!API_BASE || !req || !req.id) {
setAcceptError("Некорректная заявка (нет id)");
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setAcceptError("Вы не авторизованы");
return;
}
try {
setAcceptLoading(true);
setAcceptError("");
const res = await fetch(`${API_BASE}/requests/${req.id}/responses`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(
message ? { message } : {} // message опционален по схеме
),
});
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;
}
console.error("Ошибка отклика:", msg);
setAcceptError(msg);
setAcceptLoading(false);
return;
}
await res.json(); // VolunteerResponse
setAcceptLoading(false);
closePopup();
} catch (e) {
setAcceptError(e.message || "Ошибка сети");
setAcceptLoading(false);
}
};
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 justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full border border-white flex items-center justify-center">
<FaUser className="text-white text-sm" />
</div>
<p className="font-montserrat font-extrabold text-[20px] leading-[11px] text-white">
{userName}
</p>
</div>
<button
type="button"
className="w-8 h-8 rounded-full border border-white flex items-center justify-center"
>
<FaCog className="text-white text-sm" />
</button>
</header>
<h1 className="font-montserrat font-extrabold text-[20px] leading-[20px] text-white mb-2">
Кому нужна помощь
</h1>
{error && (
<p className="mb-2 text-xs font-montserrat text-red-200">
{error}
</p>
)}
{/* Карта */}
<div className="w-full bg-transparent mb-3">
<div className="w-full h-[250px] bg-[#D9D9D9] rounded-2xl overflow-hidden">
<MapContainer
center={position}
zoom={13}
style={{ width: "100%", height: "100%" }}
>
<TileLayer
attribution="&copy; OpenStreetMap contributors"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{hasLocation && (
<Marker position={position}>
<Popup>Вы здесь</Popup>
</Marker>
)}
{requests.map((req) => (
<Marker key={req.id} position={req.coords}>
<Popup>{req.title}</Popup>
</Marker>
))}
</MapContainer>
</div>
</div>
{/* Заявки ниже карты */}
<main className="space-y-3">
{loading && (
<p className="text-white text-sm font-montserrat">
Загрузка заявок...
</p>
)}
{!loading && requests.length === 0 && !error && (
<p className="text-white text-sm font-montserrat">
Рядом пока нет заявок
</p>
)}
{requests.map((req) => (
<div
key={req.id}
className="bg-white rounded-xl px-3 py-2 flex flex-col gap-1"
onClick={() => openPopup(req)}
>
<p className="font-montserrat font-semibold text-[15px] leading-[14px] text-black">
{req.title}
</p>
<p className="font-montserrat text-[15px] text-black">
{req.address}
</p>
{req.distance && (
<p className="font-montserrat text-[12px] text-gray-500">
Расстояние: {req.distance}
</p>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
openPopup(req);
}}
className="mt-2 w-full bg-[#94E067] rounded-lg py-2 flex items-center justify-center"
>
<span className="font-montserrat font-bold text-[14px] text-white">
Информация
</span>
</button>
</div>
))}
</main>
<TabBar />
</div>
<AcceptPopup
request={selectedRequest}
isOpen={isPopupOpen}
onClose={closePopup}
// onAccept={handleAccept}
// loading={acceptLoading}
// error={acceptError}
/>
</div>
);
};
export default MainVolunteerPage;