This commit is contained in:
fullofempt
2025-12-14 18:47:14 +05:00
parent bb833d956e
commit 433b9e896c
18 changed files with 2891 additions and 1260 deletions

View File

@@ -6,7 +6,9 @@ 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 }
@@ -27,61 +29,43 @@ const Popup = dynamic(
// центр Перми
const DEFAULT_POSITION = [58.0105, 56.2294];
const requests = [
{
id: 1,
title: "Приобрести продукты пенсионерке",
address: "г. Пермь, ул. Ленина 50, кв. 24, этаж 3",
coords: [58.0109, 56.2478], // район ул. Ленина
distance: "1.2 км",
},
{
id: 2,
title: "Приобрести медикаменты бабушке",
address: "г. Пермь, ул. Пушкина 24, кв. 12, этаж 1",
coords: [58.0135, 56.2320], // район ул. Пушкина
distance: "2.0 км",
},
{
id: 3,
title: "Сопроводить до поликлиники",
address: "г. Пермь, ул. Куйбышева 95, кв. 7, этаж 2",
coords: [58.0068, 56.2265], // район ул. Куйбышева
distance: "3.4 км",
},
{
id: 4,
title: "Сопроводить до поликлиники",
address: "г. Пермь, ул. Куйбышева 95, кв. 7, этаж 2",
coords: [58.0068, 56.2265], // район ул. Куйбышева
distance: "3.4 км",
},
{
id: 5,
title: "Сопроводить до поликлиники",
address: "г. Пермь, ул. Куйбышева 95, кв. 7, этаж 2",
coords: [58.0068, 56.2265], // район ул. Куйбышева
distance: "3.4 км",
},
];
const MainVolunteerPage = () => {
const [position, setPosition] = useState(DEFAULT_POSITION);
const [hasLocation, setHasLocation] = useState(false);
const [userName, setUserName] = useState("Волонтёр");
const [requests, setRequests] = useState([]); // заявки из /requests/nearby
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("");
// получить токен из localStorage
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(
@@ -95,9 +79,168 @@ const MainVolunteerPage = () => {
);
}, []);
const handleAccept = (req) => {
console.log("Откликнуться на заявку:", req.id);
// TODO: запрос на бэк
// загрузка имени волонтёра из /users/me[file:519]
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();
}, []);
// загрузка заявок рядом: /requests/nearby?lat=&lon=&radius=[file:519]
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(); // массив RequestWithDistance[file:519]
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);
}
};
// загружаем, когда уже знаем позицию
if (hasLocation || position !== DEFAULT_POSITION) {
fetchNearbyRequests();
} else {
// если геолокация не дала позицию — всё равно пробуем из центра
fetchNearbyRequests();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position, hasLocation]);
// отклик: POST /requests/{id}/responses[file:519]
const handleAccept = async (req, message = "") => {
if (!API_BASE || !req) 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;
}
setAcceptError(msg);
setAcceptLoading(false);
return;
}
const createdResponse = await res.json();
console.log("Отклик создан:", createdResponse);
setAcceptLoading(false);
closePopup();
} catch (e) {
setAcceptError(e.message || "Ошибка сети");
setAcceptLoading(false);
}
};
return (
@@ -110,7 +253,7 @@ const MainVolunteerPage = () => {
<FaUser className="text-white text-sm" />
</div>
<p className="font-montserrat font-extrabold text-[20px] leading-[11px] text-white">
Александр
{userName}
</p>
</div>
<button
@@ -125,6 +268,12 @@ const MainVolunteerPage = () => {
Кому нужна помощь
</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">
@@ -134,7 +283,7 @@ const MainVolunteerPage = () => {
style={{ width: "100%", height: "100%" }}
>
<TileLayer
attribution='&copy; OpenStreetMap contributors'
attribution="&copy; OpenStreetMap contributors"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Маркер волонтёра */}
@@ -155,6 +304,18 @@ const MainVolunteerPage = () => {
{/* Заявки ниже карты */}
<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}
@@ -174,8 +335,11 @@ const MainVolunteerPage = () => {
)}
<button
type="button"
onClick={() => handleAccept(req)}
className="mt-2 w-full bg-[#94E067] rounded-lg py-2 flex items-center justify-center"
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">
Откликнуться
@@ -187,16 +351,17 @@ const MainVolunteerPage = () => {
<TabBar />
</div>
<AcceptPopup
request={selectedRequest}
isOpen={isPopupOpen}
onClose={closePopup}
onAccept={handleAccept}
loading={acceptLoading}
error={acceptError}
/>
</div>
);
};
export default MainVolunteerPage;