WIP API
This commit is contained in:
@@ -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 (
|
||||
<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">
|
||||
<div className="relative w-full max-w-md flex flex-col pb-20 pt-4">
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -41,8 +190,13 @@ const CreateRequestPage = () => {
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-montserrat font-extrabold text-[20px] leading-[11px] text-white">
|
||||
Александр
|
||||
{userName}
|
||||
</p>
|
||||
{profileError && (
|
||||
<p className="text-[10px] text-red-200 font-montserrat mt-1">
|
||||
{profileError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -57,12 +211,19 @@ const CreateRequestPage = () => {
|
||||
Создать заявку
|
||||
</h1>
|
||||
|
||||
{/* Ошибка */}
|
||||
{error && (
|
||||
<div className="mb-2 bg-red-500 text-white text-xs font-montserrat px-3 py-2 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Карточка с формой */}
|
||||
<main className="bg-white rounded-xl p-4 flex flex-col gap-3">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col">
|
||||
{/* Что сделать */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Что сделать
|
||||
</label>
|
||||
<input
|
||||
@@ -74,6 +235,71 @@ const CreateRequestPage = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Адрес */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Адрес
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
|
||||
placeholder="ул. Ленина, д. 10, кв. 5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Город */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Город
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
|
||||
placeholder="Например: Пермь"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Координаты */}
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Широта (lat)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={latitude}
|
||||
onChange={(e) => setLatitude(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
|
||||
placeholder="55.751244"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Долгота (lon)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.000001"
|
||||
value={longitude}
|
||||
onChange={(e) => setLongitude(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200"
|
||||
placeholder="37.618423"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{geoError && (
|
||||
<p className="mt-1 text-[10px] text-yellow-200 font-montserrat">
|
||||
{geoError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
{/* Дата и Время */}
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
@@ -95,11 +321,42 @@ const CreateRequestPage = () => {
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white outline-none focus:ring-2 focus:ring-blue-200"
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white outline-none focus:ring-2 focus:ring-blue-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Срочность */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Срочность
|
||||
</label>
|
||||
<select
|
||||
value={urgency}
|
||||
onChange={(e) => setUrgency(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white outline-none focus:ring-2 focus:ring-blue-200"
|
||||
>
|
||||
<option value="low">Низкая</option>
|
||||
<option value="medium">Средняя</option>
|
||||
<option value="high">Высокая</option>
|
||||
<option value="urgent">Срочно</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Телефон для связи */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
Телефон для связи
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200"
|
||||
placeholder="+7 900 000 00 00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Описание */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
@@ -109,26 +366,26 @@ const CreateRequestPage = () => {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white.placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
placeholder="Подробно опишите, что нужно сделать"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Дополнительная информация */}
|
||||
{/* Дополнительно */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-montserrat font-bold text-[10px] text-white/90">
|
||||
<label className="font-montserrat.font-bold text-[10px] text-white/90">
|
||||
Дополнительно
|
||||
</label>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white.placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
|
||||
placeholder="Комментарий (необязательно)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Добавить фото */}
|
||||
{/* Добавить фото — пока без API */}
|
||||
<div className="flex items-center gap-3 mt-5">
|
||||
<button
|
||||
type="button"
|
||||
@@ -148,20 +405,18 @@ const CreateRequestPage = () => {
|
||||
{/* Кнопка Отправить */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isFormValid}
|
||||
className={`mt-5 w-full rounded-lg py-3 text-center font-montserrat font-bold text-sm transition-colors
|
||||
${
|
||||
isFormValid
|
||||
? "bg-[#94E067] text-white hover:bg-green-600"
|
||||
: "bg-[#94E067]/60 text-white/70 cursor-not-allowed"
|
||||
disabled={!isFormValid || isSubmitting}
|
||||
className={`mt-5 w-full rounded-lg py-3 text-center font-montserrat font-bold text-sm transition-colors
|
||||
${isFormValid && !isSubmitting
|
||||
? "bg-[#94E067] text-white hover:bg-green-600"
|
||||
: "bg-[#94E067]/60 text-white/70 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
Отправить
|
||||
{isSubmitting ? "Отправка..." : "Отправить"}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
{/* TabBar снизу, во всю ширину */}
|
||||
<TabBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user