WIPVOLONT

This commit is contained in:
fullofempt
2025-12-14 21:14:55 +05:00
parent 433b9e896c
commit 0df52352a8
12 changed files with 893 additions and 440 deletions

View File

@@ -8,7 +8,7 @@ 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,7 +27,7 @@ const Popup = dynamic(
);
// центр Перми
const DEFAULT_POSITION = [58.0105, 56.2294];
const DEFAULT_POSITION = [57.997962, 56.147201];
const MainVolunteerPage = () => {
const [position, setPosition] = useState(DEFAULT_POSITION);
@@ -35,7 +35,7 @@ const MainVolunteerPage = () => {
const [userName, setUserName] = useState("Волонтёр");
const [requests, setRequests] = useState([]); // заявки из /requests/nearby
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
@@ -45,7 +45,26 @@ const MainVolunteerPage = () => {
const [acceptLoading, setAcceptLoading] = useState(false);
const [acceptError, setAcceptError] = useState("");
// получить токен из localStorage
// фикс иконок 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");
@@ -65,7 +84,7 @@ const MainVolunteerPage = () => {
setAcceptError("");
};
// геолокация волонтёра
// геолокация
useEffect(() => {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
@@ -79,7 +98,7 @@ const MainVolunteerPage = () => {
);
}, []);
// загрузка имени волонтёра из /users/me[file:519]
// профиль
useEffect(() => {
const fetchProfile = async () => {
if (!API_BASE) return;
@@ -96,20 +115,18 @@ const MainVolunteerPage = () => {
if (!res.ok) return;
const data = await res.json();
const fullName =
[data.first_name, data.last_name]
.filter(Boolean)
.join(" ")
.trim() || data.email;
[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) {
@@ -136,12 +153,15 @@ const MainVolunteerPage = () => {
offset: "0",
});
const res = await fetch(`${API_BASE}/requests/nearby?${params.toString()}`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
const res = await fetch(
`${API_BASE}/requests/nearby?${params.toString()}`,
{
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!res.ok) {
let msg = "Не удалось загрузить заявки рядом";
@@ -157,24 +177,23 @@ const MainVolunteerPage = () => {
return;
}
const data = await res.json(); // массив RequestWithDistance[file:519]
const data = await res.json();
const mapped = data.map((item) => ({
id: item.id,
title: item.title,
description: item.description, // <= добавили
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, // <= уже есть
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) {
@@ -183,19 +202,17 @@ const MainVolunteerPage = () => {
}
};
// загружаем, когда уже знаем позицию
if (hasLocation || position !== DEFAULT_POSITION) {
fetchNearbyRequests();
} else {
// если геолокация не дала позицию — всё равно пробуем из центра
fetchNearbyRequests();
}
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;
if (!API_BASE || !req || !req.id) {
setAcceptError("Некорректная заявка (нет id)");
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setAcceptError("Вы не авторизованы");
@@ -214,7 +231,7 @@ const MainVolunteerPage = () => {
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(
message ? { message } : {} // поле message опционально
message ? { message } : {} // message опционален по схеме
),
});
@@ -227,14 +244,13 @@ const MainVolunteerPage = () => {
const text = await res.text();
if (text) msg = text;
}
console.error("Ошибка отклика:", msg);
setAcceptError(msg);
setAcceptLoading(false);
return;
}
const createdResponse = await res.json();
console.log("Отклик создан:", createdResponse);
await res.json(); // VolunteerResponse
setAcceptLoading(false);
closePopup();
} catch (e) {
@@ -243,6 +259,8 @@ const MainVolunteerPage = () => {
}
};
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">
@@ -286,13 +304,11 @@ const MainVolunteerPage = () => {
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>
@@ -339,10 +355,10 @@ const MainVolunteerPage = () => {
e.stopPropagation();
openPopup(req);
}}
className="mt-2 w-full bg-[#94E067] rounded-lg py-2 flex.items-center justify-center"
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>
@@ -356,9 +372,9 @@ const MainVolunteerPage = () => {
request={selectedRequest}
isOpen={isPopupOpen}
onClose={closePopup}
onAccept={handleAccept}
loading={acceptLoading}
error={acceptError}
// onAccept={handleAccept}
// loading={acceptLoading}
// error={acceptError}
/>
</div>
);