VolonteurMainPage

This commit is contained in:
fullofempt
2025-12-13 17:58:31 +05:00
parent 55c42a115d
commit 48d4db0e77
9 changed files with 609 additions and 7 deletions

109
app/ProfilePage/page.jsx Normal file
View File

@@ -0,0 +1,109 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { FaUserCircle, FaStar } from "react-icons/fa";
import TabBar from "../components/TabBar";
const ProfilePage = () => {
const router = useRouter();
const fullName = "Иванов Александр Сергеевич";
const birthDate = "12.03.1990";
const rating = 4.8;
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">
{/* Аватар */}
<FaUserCircle className="text-[#72B8E2] w-20 h-20" />
{/* ФИО и рейтинг */}
<div className="text-center space-y-1">
{/* <p className="font-montserrat font-extrabold text-[16px] text-black">
ФИО
</p> */}
<p className="font-montserrat font-bold text-[20px] text-black">
{fullName}
</p>
{/* Рейтинг + звезды */}
<div className="mt-2 flex items-center justify-center gap-2">
<span className="font-montserrat font-semibold text-[14px] text-black">
Рейтинг: {rating.toFixed(1)}
</span>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<FaStar
key={star}
size={18}
className={
star <= Math.round(rating)
? "text-[#F6E168] fill-[#F6E168]"
: "text-[#F6E168] fill-[#F6E168]/30"
}
/>
))}
</div>
</div>
</div>
{/* Контакты и день рождения */}
<div className="w-full bg-[#72B8E2] rounded-2xl p-3 text-white space-y-1">
<p className="font-montserrat text-[12px]">
Дата рождения: {birthDate}
</p>
<p className="font-montserrat text-[12px]">
Почта: example@mail.com
</p>
<p className="font-montserrat text-[12px]">
Телефон: +7 (900) 000-00-00
</p>
</div>
{/* Кнопки */}
<div className="w-full flex flex-col gap-2 mt-2">
<button
type="button"
onClick={() => router.push("/profileSettings")}
className="w-full bg-[#E0B267] rounded-full py-2 flex items-center justify-center"
>
<span className="font-montserrat font-extrabold text-[14px] text-white">
Редактировать профиль
</span>
</button>
<button
type="button"
className="w-full bg-[#E07567] rounded-full py-2 flex items-center justify-center"
>
<span className="font-montserrat font-extrabold text-[14px] text-white">
Выйти из аккаунта
</span>
</button>
</div>
</main>
<TabBar />
</div>
</div>
);
};
export default ProfilePage;

View File

@@ -43,7 +43,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
{/* Белая карточка как на макете */} {/* Белая карточка как на макете */}
<div className="flex-1 flex items-start justify-center"> <div className="flex-1 flex items-start justify-center">
<div className="w-full max-w-[360px] bg-white rounded-3xl p-4 flex flex-col gap-4 shadow-lg"> <div className="w-full max-w-[360px] bg-white rounded-2xl p-4 flex flex-col gap-4 shadow-lg">
{/* Статус + срок (берём цвет и текст из заявки) */} {/* Статус + срок (берём цвет и текст из заявки) */}
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<span <span
@@ -87,7 +87,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
{isRejected && ( {isRejected && (
<> <>
{request.rejectReason && ( {request.rejectReason && (
<div className="bg-[#FF8282] rounded-3xl p-3"> <div className="bg-[#FF8282] rounded-2xl p-3">
<p className="font-montserrat font-bold text-[12px] text-white mb-1"> <p className="font-montserrat font-bold text-[12px] text-white mb-1">
Причина отказа Причина отказа
</p> </p>
@@ -147,7 +147,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
<button <button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-full py-3 flex items-center justify-center" className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex items-center justify-center"
> >
<span className="font-montserrat font-extrabold text-[16px] text-white"> <span className="font-montserrat font-extrabold text-[16px] text-white">
{isRejected ? "Отправить комментарий" : "Оставить отзыв"} {isRejected ? "Отправить комментарий" : "Оставить отзыв"}

View File

@@ -32,6 +32,7 @@ const TabBar = () => {
</button> </button>
<button <button
type="button" type="button"
onClick={() => router.push("/ProfilePage")}
className="flex flex-col items-center gap-1 text-[#90D2F9]" className="flex flex-col items-center gap-1 text-[#90D2F9]"
> >
<FaCog size={20} /> <FaCog size={20} />

View File

@@ -0,0 +1,102 @@
"use client";
import React from "react";
import { FaTimesCircle } from "react-icons/fa";
const AcceptPopup = ({ request, isOpen, onClose, onAccept }) => {
if (!isOpen || !request) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* затемнение */}
<div
className="absolute inset-0 bg-black/40"
onClick={onClose}
/>
{/* карточка на всю страницу */}
<div className="relative z-10 w-full h-250px bg-white rounded-2xl px-4 pt-4 pb-6 flex flex-col">
{/* крестик */}
<button
type="button"
onClick={onClose}
className="absolute top-4 right-4 text-[#FF9494]"
>
<FaTimesCircle className="w-5 h-5" />
</button>
{/* Заголовок */}
<h2 className="font-montserrat font-extrabold text-[20px] leading-[22px] text-[#90D2F9] mb-1">
Задача
</h2>
<p className="text-[20px] leading-[14px] mt-5 font-montserrat mb-5">
{request.title}
</p>
{/* Сумма и время */}
<div className="flex items-center gap-3 mb-3">
<div className="w-full h-[40px] bg-[#90D2F9] rounded-full flex flex-col items-center justify-center">
<span className="text-[12px] leading-[11px] text-white font-semibold mb-2">
Сумма
</span>
<span className="text-[15px] leading-[13px] text-white font-semibold">
{request.amount || "2000 ₽"}
</span>
</div>
<div className="w-full h-[40px] bg-[#90D2F9] rounded-full flex flex-col items-center justify-center">
<span className="text-[12px] leading-[11px] text-white font-semibold mb-2">
Выполнить до
</span>
<span className="text-[15px] leading-[13px] text-white font-semibold">
{request.deadline || "17:00"}
</span>
</div>
</div>
{/* Список покупок / описание */}
<div className="w-full bg-[#E4E4E4] rounded-[20px] px-3 py-3 mb-3 max-h-[40vh] overflow-y-auto">
<p className="text-[15px] leading-[20px] font-montserrat text-black whitespace-pre-line">
{request.description ||
"Необходимо приобрести:\n1. Белый хлеб\n2. Молоко\n3. Колбаса\n4. Фрукты"}
</p>
</div>
{/* Данные человека */}
<div className="w-full flex flex-col gap-3 mb-4">
<p className="font-montserrat text-[20px] leading-[19px] font-medium">
Данные:
</p>
<p className="text-[20px] leading-[12px] font-montserrat">
ФИО: {request.fullName || "Клавдия Березова"}
</p>
<p className="text-[15px] leading-[12px] font-montserrat">
Место: {request.address}
</p>
{request.flat && (
<p className="text-[10px] leading-[12px] font-montserrat">
кв: {request.flat}
</p>
)}
{request.floor && (
<p className="text-[10px] leading-[12px] font-montserrat">
Этаж: {request.floor}
</p>
)}
</div>
{/* Кнопка отклика внизу */}
<button
type="button"
onClick={() => onAccept(request)}
className="mt-auto w-full h-[40px] bg-[#94E067] rounded-[10px] flex items-center justify-center"
>
<span className="font-montserrat font-bold text-[16px] leading-[19px] text-white">
Откликнуться
</span>
</button>
</div>
</div>
);
};
export default AcceptPopup;

View File

@@ -99,7 +99,7 @@ const HistoryRequestPage = () => {
<div className="w-8 h-8 rounded-full border border-white flex items-center justify-center"> <div className="w-8 h-8 rounded-full border border-white flex items-center justify-center">
<FaUser className="text-white text-sm" /> <FaUser className="text-white text-sm" />
</div> </div>
<p className="font-montserrat font-extrabold text-[11px] leading-[11px] text-white"> <p className="font-montserrat font-extrabold text-[20px] leading-[11px] text-white">
Александр Александр
</p> </p>
</div> </div>
@@ -111,7 +111,7 @@ const HistoryRequestPage = () => {
</button> </button>
</header> </header>
<h1 className="font-montserrat font-extrabold text-[18px] leading-[22px] text-white mb-3"> <h1 className="font-montserrat font-extrabold text-[20px] leading-[22px] text-white mb-3">
История заявок История заявок
</h1> </h1>

202
app/mainValounter/page.jsx Normal file
View File

@@ -0,0 +1,202 @@
"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 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 = [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 [selectedRequest, setSelectedRequest] = useState(null);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const openPopup = (req) => {
setSelectedRequest(req);
setIsPopupOpen(true);
};
const closePopup = () => {
setIsPopupOpen(false);
setSelectedRequest(null);
};
useEffect(() => {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
(pos) => {
setPosition([pos.coords.latitude, pos.coords.longitude]);
setHasLocation(true);
},
() => {
setHasLocation(false);
}
);
}, []);
const handleAccept = (req) => {
console.log("Откликнуться на заявку:", req.id);
// TODO: запрос на бэк
};
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-[11px] leading-[11px] text-white">
Александр
</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-[16px] leading-[20px] text-white mb-2">
Кому нужна помощь
</h1>
{/* Карта */}
<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">
{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-[12px] leading-[14px] text-black">
{req.title}
</p>
<p className="font-montserrat text-[10px] text-black">
{req.address}
</p>
{req.distance && (
<p className="font-montserrat text-[9px] text-gray-500">
Расстояние: {req.distance}
</p>
)}
<button
type="button"
onClick={() => handleAccept(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}
/>
</div>
);
};
export default MainVolunteerPage;

View File

@@ -0,0 +1,153 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { FaUserCircle } from "react-icons/fa";
import TabBar from "../components/TabBar";
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 handleSave = (e) => {
e.preventDefault();
console.log("Сохранить профиль:", {
avatarUrl,
fullName,
birthDate,
email,
phone,
});
// здесь будет запрос на бэк
};
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">
{/* Аватар */}
<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={fullName}
onChange={(e) => setFullName(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="date"
value={birthDate}
onChange={(e) => setBirthDate(e.target.value)}
className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white outline-none border border-transparent focus:border-white/70"
/>
</div>
{/* Почта */}
<div className="flex flex-col gap-1">
<label className="font-montserrat text-[12px] text-black">
Почта
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(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="example@mail.com"
/>
</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>
{/* Кнопка сохранить */}
<button
type="submit"
className="mt-2 w-full bg-[#94E067] rounded-full py-2.5 flex items-center justify-center"
>
<span className="font-montserrat font-extrabold text-[14px] text-white">
Сохранить изменения
</span>
</button>
</form>
</main>
<TabBar />
</div>
</div>
);
};
export default ProfileSettingsPage;

35
package-lock.json generated
View File

@@ -8,10 +8,12 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"leaflet": "^1.9.4",
"next": "16.0.10", "next": "16.0.10",
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1", "react-dom": "19.2.1",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@@ -1227,6 +1229,17 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -4514,6 +4527,12 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5416,6 +5435,20 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",

View File

@@ -9,10 +9,12 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"leaflet": "^1.9.4",
"next": "16.0.10", "next": "16.0.10",
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1", "react-dom": "19.2.1",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",