fixes
This commit is contained in:
parent
089f5064a3
commit
da29133989
|
|
@ -1,22 +0,0 @@
|
|||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- echo "Building Docker image..."
|
||||
- docker build -t travelmarine-frontend:latest .
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
needs: ["build"]
|
||||
script:
|
||||
- echo "Restarting container..."
|
||||
- docker ps -a --filter 'name=^/travelmarine-frontend$' --format '{{.Names}}' | grep -q '^travelmarine-frontend$' && docker rm -f travelmarine-frontend || true
|
||||
- docker run -d --name travelmarine-frontend --restart unless-stopped -p 127.0.0.1:3000:3000 travelmarine-frontend:latest
|
||||
when: on_success
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"eslint-config-next": "15.5.5",
|
||||
"tailwindcss": "^4",
|
||||
"turbo": "^2.6.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,6 @@
|
|||
"eslint-config-next": "15.5.5",
|
||||
"tailwindcss": "^4",
|
||||
"turbo": "^2.6.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface CatalogItemShortDto {
|
||||
export interface CatalogItemShortDto {
|
||||
id?: number;
|
||||
name: string;
|
||||
length: number;
|
||||
|
|
@ -12,12 +12,12 @@ interface CatalogItemShortDto {
|
|||
isBestOffer?: boolean;
|
||||
}
|
||||
|
||||
interface MainPageCatalogResponseDto {
|
||||
export interface MainPageCatalogResponseDto {
|
||||
featuredYacht: CatalogItemShortDto;
|
||||
restYachts: CatalogItemShortDto[];
|
||||
}
|
||||
|
||||
interface CatalogFilteredResponseDto {
|
||||
export interface CatalogFilteredResponseDto {
|
||||
items: CatalogItemShortDto[];
|
||||
total: number;
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ interface Review {
|
|||
description: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
export interface User {
|
||||
userId?: number;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
|
|
@ -51,7 +51,7 @@ interface User {
|
|||
ogrn?: number;
|
||||
}
|
||||
|
||||
interface CatalogItemLongDto extends CatalogItemShortDto {
|
||||
export interface CatalogItemLongDto extends CatalogItemShortDto {
|
||||
year: number;
|
||||
comfortCapacity: number;
|
||||
maxCapacity: number;
|
||||
|
|
@ -75,3 +75,12 @@ interface Yacht {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ReservationDto {
|
||||
yachtId: number;
|
||||
reservatorId: number;
|
||||
startUtc: number;
|
||||
endUtc: number;
|
||||
id: number;
|
||||
yacht: CatalogItemLongDto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,40 @@ interface AuthDataType {
|
|||
rememberMe: boolean;
|
||||
}
|
||||
|
||||
interface JWTPayload {
|
||||
user_id?: number;
|
||||
userId?: number;
|
||||
sub?: string;
|
||||
id?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const parseJWT = (token: string): JWTPayload | null => {
|
||||
try {
|
||||
const base64Url = token.split(".")[1];
|
||||
if (!base64Url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (error) {
|
||||
console.error("Ошибка при парсинге JWT токена:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const useAuthentificate = () => {
|
||||
const client = useApiClient();
|
||||
const setToken = useAuthStore((state) => state.setToken);
|
||||
const setUserId = useAuthStore((state) => state.setUserId);
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["auth"],
|
||||
|
|
@ -25,6 +56,21 @@ const useAuthentificate = () => {
|
|||
|
||||
setToken(access_token, authData.rememberMe);
|
||||
|
||||
// Парсим JWT токен и извлекаем userId
|
||||
const payload = parseJWT(access_token);
|
||||
if (payload) {
|
||||
const userId =
|
||||
payload.user_id ||
|
||||
payload.userId ||
|
||||
payload.sub ||
|
||||
payload.id ||
|
||||
null;
|
||||
|
||||
if (userId) {
|
||||
setUserId(userId, authData.rememberMe);
|
||||
}
|
||||
}
|
||||
|
||||
return access_token;
|
||||
},
|
||||
onError: () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { DatePicker } from "@/components/ui/date-picker";
|
|||
import { GuestPicker } from "@/components/form/guest-picker";
|
||||
import { format } from "date-fns";
|
||||
import { calculateTotalPrice, formatPrice } from "@/lib/utils";
|
||||
import { CatalogItemLongDto } from "@/api/types";
|
||||
|
||||
interface BookingWidgetProps {
|
||||
price: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { User } from "@/api/types";
|
||||
|
||||
export function ContactInfo({ firstName, companyName, inn, ogrn }: User) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { CatalogItemLongDto } from "@/api/types";
|
||||
|
||||
interface YachtCharacteristicsProps {
|
||||
yacht: CatalogItemLongDto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { GuestPicker } from "@/components/form/guest-picker";
|
|||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { formatSpeed } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CatalogItemLongDto } from "@/api/types";
|
||||
|
||||
export default function YachtDetailPage() {
|
||||
const { id } = useParams();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
|
|||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils";
|
||||
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
||||
import { CatalogFilteredResponseDto } from "@/api/types";
|
||||
|
||||
export interface CatalogFilters {
|
||||
search: string;
|
||||
|
|
|
|||
|
|
@ -13,9 +13,19 @@ import Image from "next/image";
|
|||
import Icon from "@/components/ui/icon";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { GuestDatePicker, GuestDatePickerValue } from "@/components/form/guest-date-picker";
|
||||
import { formatMinCost, formatWidth, getImageUrl, calculateTotalPrice, formatPrice } from "@/lib/utils";
|
||||
import {
|
||||
GuestDatePicker,
|
||||
GuestDatePickerValue,
|
||||
} from "@/components/form/guest-date-picker";
|
||||
import {
|
||||
formatMinCost,
|
||||
formatWidth,
|
||||
getImageUrl,
|
||||
calculateTotalPrice,
|
||||
formatPrice,
|
||||
} from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CatalogItemShortDto } from "@/api/types";
|
||||
|
||||
export default function FeaturedYacht({
|
||||
yacht,
|
||||
|
|
@ -38,7 +48,11 @@ export default function FeaturedYacht({
|
|||
|
||||
// Расчет итоговой стоимости
|
||||
const getTotalPrice = () => {
|
||||
if (!bookingData.date || !bookingData.departureTime || !bookingData.arrivalTime) {
|
||||
if (
|
||||
!bookingData.date ||
|
||||
!bookingData.departureTime ||
|
||||
!bookingData.arrivalTime
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -62,24 +76,14 @@ export default function FeaturedYacht({
|
|||
return;
|
||||
}
|
||||
|
||||
// Форматируем дату в формат yyyy-MM-dd
|
||||
const dateString = format(bookingData.date, "yyyy-MM-dd");
|
||||
|
||||
// Кодируем время для URL (00:00 -> 00%3A00)
|
||||
const encodedDepartureTime = encodeURIComponent(bookingData.departureTime);
|
||||
const encodedArrivalTime = encodeURIComponent(bookingData.arrivalTime);
|
||||
|
||||
// Вычисляем общее количество гостей
|
||||
const totalGuests = bookingData.adults + bookingData.children;
|
||||
|
||||
// Формируем URL с параметрами
|
||||
const params = new URLSearchParams({
|
||||
yachtId: yacht.id.toString(),
|
||||
departureDate: dateString,
|
||||
departureTime: encodedDepartureTime,
|
||||
arrivalDate: dateString, // Используем ту же дату для arrival
|
||||
arrivalTime: encodedArrivalTime,
|
||||
guests: totalGuests.toString(),
|
||||
departureDate: format(bookingData.date, "yyyy-MM-dd"),
|
||||
departureTime: bookingData.departureTime,
|
||||
arrivalDate: format(bookingData.date, "yyyy-MM-dd"),
|
||||
arrivalTime: bookingData.arrivalTime,
|
||||
guests: (bookingData.adults + bookingData.children).toString(),
|
||||
});
|
||||
|
||||
// Переходим на страницу подтверждения
|
||||
|
|
@ -97,7 +101,8 @@ export default function FeaturedYacht({
|
|||
<div
|
||||
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative lg:hidden"
|
||||
style={{
|
||||
backgroundImage: "url(/images/badge-bg.jpg)",
|
||||
backgroundImage:
|
||||
"url(/images/badge-bg.jpg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
|
|
@ -110,10 +115,14 @@ export default function FeaturedYacht({
|
|||
|
||||
{/* Header with yacht name and length */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-bold">{yacht.name}</h2>
|
||||
<h2 className="text-3xl font-bold">
|
||||
{yacht.name}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Icon size={16} name="width" />
|
||||
<span className="text-lg">{formatWidth(yacht.length)}</span>
|
||||
<span className="text-lg">
|
||||
{formatWidth(yacht.length)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -148,15 +157,22 @@ export default function FeaturedYacht({
|
|||
<div className="relative">
|
||||
<Image
|
||||
src={getImageUrl(thumb)}
|
||||
alt={`${yacht.name} view ${idx + 1}`}
|
||||
alt={`${
|
||||
yacht.name
|
||||
} view ${idx + 1}`}
|
||||
width={80}
|
||||
height={60}
|
||||
className={`w-20 h-16 object-cover rounded-[8px] cursor-pointer border-2 transition-all ${
|
||||
selectedImage === thumb
|
||||
selectedImage ===
|
||||
thumb
|
||||
? "border-[#008299]"
|
||||
: "border-gray-200 hover:border-gray-400"
|
||||
}`}
|
||||
onClick={() => handleThumbnailClick(thumb)}
|
||||
onClick={() =>
|
||||
handleThumbnailClick(
|
||||
thumb
|
||||
)
|
||||
}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -193,7 +209,8 @@ export default function FeaturedYacht({
|
|||
<div
|
||||
className="text-white flex items-center justify-center py-2 rounded-full text-center mb-6 relative hidden lg:flex"
|
||||
style={{
|
||||
backgroundImage: "url(/images/badge-bg.jpg)",
|
||||
backgroundImage:
|
||||
"url(/images/badge-bg.jpg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
|
|
@ -228,15 +245,21 @@ export default function FeaturedYacht({
|
|||
variant="gradient"
|
||||
className="font-bold text-white h-[64px] w-full px-8"
|
||||
onClick={handleBookClick}
|
||||
disabled={!bookingData.date || !yacht.id}
|
||||
disabled={
|
||||
!bookingData.date || !yacht.id
|
||||
}
|
||||
>
|
||||
Забронировать
|
||||
</Button>
|
||||
|
||||
{/* Total price */}
|
||||
<div className="flex justify-between items-center text-l mt-6 font-bold text-gray-800">
|
||||
<span className="font-normal">Итого:</span>
|
||||
<span>{formatPrice(getTotalPrice())} ₽</span>
|
||||
<span className="font-normal">
|
||||
Итого:
|
||||
</span>
|
||||
<span>
|
||||
{formatPrice(getTotalPrice())} ₽
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
formatWidth,
|
||||
getImageUrl,
|
||||
} from "@/lib/utils";
|
||||
import { CatalogItemShortDto, MainPageCatalogResponseDto } from "@/api/types";
|
||||
|
||||
export default function YachtGrid() {
|
||||
const client = useApiClient();
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
|
||||
import { useEffect, useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { getImageUrl, formatPrice, calculateTotalPrice } from "@/lib/utils";
|
||||
import { parseISO } from "date-fns";
|
||||
import { CatalogItemLongDto } from "@/api/types";
|
||||
|
||||
function ConfirmPageContent() {
|
||||
const [yacht, setYacht] = useState<CatalogItemLongDto | null>(null);
|
||||
|
|
@ -154,6 +156,40 @@ function ConfirmPageContent() {
|
|||
: `${guestCount} гостей`
|
||||
: "Не выбрано";
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: ["create-reservation", yachtId],
|
||||
mutationFn: async () => {
|
||||
if (
|
||||
!departureDate ||
|
||||
!departureTime ||
|
||||
!yachtId ||
|
||||
!arrivalDate ||
|
||||
!arrivalTime
|
||||
) {
|
||||
throw new Error("Ошибка получения данных бронирования");
|
||||
}
|
||||
|
||||
const departureDateTime = new Date(
|
||||
`${departureDate}T${departureTime}`
|
||||
);
|
||||
const arrivalDateTime = new Date(`${arrivalDate}T${arrivalTime}`);
|
||||
|
||||
const startUtc = Math.floor(departureDateTime.getTime() / 1000);
|
||||
const endUtc = Math.floor(arrivalDateTime.getTime() / 1000);
|
||||
|
||||
const body = {
|
||||
startUtc,
|
||||
endUtc,
|
||||
yachtId: Number(yachtId),
|
||||
reservatorId: Number("userId"), // TODO
|
||||
};
|
||||
|
||||
await client.post("/reservations", body);
|
||||
|
||||
router.push("/profile/reservations");
|
||||
},
|
||||
});
|
||||
|
||||
if (!yacht) {
|
||||
return <div />;
|
||||
}
|
||||
|
|
@ -304,7 +340,11 @@ function ConfirmPageContent() {
|
|||
Скидка (DISCOUNT50):
|
||||
</span>
|
||||
<span className="text-[#2D908D] font-bold">
|
||||
-{formatPrice(totalPrice * 0.5)} Р
|
||||
-
|
||||
{formatPrice(
|
||||
totalPrice * 0.5
|
||||
)}{" "}
|
||||
Р
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -358,7 +398,7 @@ function ConfirmPageContent() {
|
|||
variant="default"
|
||||
className="w-full h-[56px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full transition-colors duration-200"
|
||||
disabled={totalHours === 0}
|
||||
onClick={() => router.push("/profile/reservations")}
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
|
|
@ -454,10 +494,12 @@ function ConfirmPageContent() {
|
|||
{isPromocodeApplied && (
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[#333333]">
|
||||
Скидка (DISCOUNT50):
|
||||
Скидка
|
||||
(DISCOUNT50):
|
||||
</span>
|
||||
<span className="text-[#2D908D] font-bold">
|
||||
-{formatPrice(
|
||||
-
|
||||
{formatPrice(
|
||||
totalPrice *
|
||||
0.5
|
||||
)}{" "}
|
||||
|
|
@ -591,7 +633,7 @@ function ConfirmPageContent() {
|
|||
size="lg"
|
||||
className="flex-shrink-0 h-[64px] w-[270px] bg-[#2D908D] hover:bg-[#007088] text-white font-bold rounded-full p-0 transition-colors duration-200 hover:shadow-lg"
|
||||
disabled={totalHours === 0}
|
||||
onClick={() => router.push("/profile/reservations")}
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,76 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import ProfileSidebar from "@/app/profile/components/ProfileSidebar";
|
||||
import { User, Clock, MoveHorizontal, Users } from "lucide-react";
|
||||
|
||||
interface Reservation {
|
||||
id: string;
|
||||
yachtName: string;
|
||||
yachtImage: string;
|
||||
ownerName: string;
|
||||
ownerAvatar?: string;
|
||||
length: number;
|
||||
capacity: number;
|
||||
departureDate: string;
|
||||
departureTime: string;
|
||||
arrivalDate: string;
|
||||
arrivalTime: string;
|
||||
guests: number;
|
||||
paymentType: string;
|
||||
totalPrice: number;
|
||||
paymentStatus: "pending" | "paid" | "confirmed";
|
||||
}
|
||||
|
||||
// Моковые данные для демонстрации
|
||||
const mockReservations: Record<string, Reservation[]> = {
|
||||
new: [
|
||||
{
|
||||
id: "1",
|
||||
yachtName: "KALLISTE",
|
||||
yachtImage: "/images/yachts/yacht1.jpg",
|
||||
ownerName: "Денис",
|
||||
length: 14,
|
||||
capacity: 10,
|
||||
departureDate: "9 Авг, 2025",
|
||||
departureTime: "00:00",
|
||||
arrivalDate: "9 Авг, 2025",
|
||||
arrivalTime: "02:00",
|
||||
guests: 1,
|
||||
paymentType: "Полная оплата",
|
||||
totalPrice: 52800,
|
||||
paymentStatus: "pending",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
yachtName: "Señorita",
|
||||
yachtImage: "/images/yachts/yacht2.jpg",
|
||||
ownerName: "Денис",
|
||||
length: 14,
|
||||
capacity: 10,
|
||||
departureDate: "17 Авг, 2025",
|
||||
departureTime: "00:00",
|
||||
arrivalDate: "17 Авг, 2025",
|
||||
arrivalTime: "03:00",
|
||||
guests: 1,
|
||||
paymentType: "Полная оплата",
|
||||
totalPrice: 75240,
|
||||
paymentStatus: "pending",
|
||||
},
|
||||
],
|
||||
active: [],
|
||||
confirmed: [],
|
||||
archive: [],
|
||||
};
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import useAuthStore from "@/stores/useAuthStore";
|
||||
import { ReservationDto } from "@/api/types";
|
||||
import { formatWidth } from "@/lib/utils";
|
||||
|
||||
export default function ReservationsPage() {
|
||||
const [activeTab, setActiveTab] = useState<"new" | "active" | "confirmed" | "archive">("new");
|
||||
const reservations = mockReservations[activeTab];
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"new" | "active" | "confirmed" | "archive"
|
||||
>("new");
|
||||
const [reservationsData, setReservationsData] = useState<ReservationDto[]>(
|
||||
[]
|
||||
);
|
||||
const apiClient = useApiClient();
|
||||
const { getUserId } = useAuthStore();
|
||||
const userId = getUserId();
|
||||
|
||||
const formatPrice = (price: number): string => {
|
||||
return new Intl.NumberFormat("ru-RU").format(price) + " Р";
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
apiClient
|
||||
.get<ReservationDto[]>(`/reservations/user/${userId}`)
|
||||
.then((response) => {
|
||||
setReservationsData(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Ошибка при загрузке бронирований:", error);
|
||||
});
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
// @TODO: Залупа с годом, надо скачать dayjs
|
||||
const formatUtcDate = (timestamp: number): string => {
|
||||
const date = new Date(timestamp);
|
||||
const day = String(date.getUTCDate()).padStart(2, "0");
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const year = date.getUTCFullYear();
|
||||
const hours = String(date.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
return `${day}.${month}.${year} - ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -103,16 +75,18 @@ export default function ReservationsPage() {
|
|||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab("new")}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${activeTab === "new"
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${
|
||||
activeTab === "new"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
>
|
||||
Новые брони ({mockReservations.new.length})
|
||||
Новые брони ({reservationsData.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("active")}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${activeTab === "active"
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${
|
||||
activeTab === "active"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
|
|
@ -121,7 +95,8 @@ export default function ReservationsPage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("confirmed")}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${activeTab === "confirmed"
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${
|
||||
activeTab === "confirmed"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
|
|
@ -130,7 +105,8 @@ export default function ReservationsPage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("archive")}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${activeTab === "archive"
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full border transition-colors ${
|
||||
activeTab === "archive"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
|
|
@ -141,23 +117,34 @@ export default function ReservationsPage() {
|
|||
|
||||
{/* Reservations List */}
|
||||
<div className="space-y-8">
|
||||
{reservations.length === 0 ? (
|
||||
{reservationsData.length === 0 ? (
|
||||
<div className="text-center py-12 text-[#999999]">
|
||||
Нет бронирований в этой категории
|
||||
</div>
|
||||
) : (
|
||||
reservations.map((reservation, index) => (
|
||||
reservationsData.map((reservation, index) => (
|
||||
<div
|
||||
key={reservation.id}
|
||||
className={`overflow-hidden bg-white ${index !== reservations.length - 1 ? 'pb-8 border-b border-gray-200' : ''}`}
|
||||
className={`overflow-hidden bg-white ${
|
||||
index !==
|
||||
reservationsData.length - 1
|
||||
? "pb-8 border-b border-gray-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
{/* Image Section */}
|
||||
<div className="relative rounded-[12px] overflow-hidden w-90 h-90 flex-shrink-0">
|
||||
<Image
|
||||
src={reservation.yachtImage}
|
||||
alt={reservation.yachtName}
|
||||
src={
|
||||
reservation.yacht
|
||||
.mainImageUrl
|
||||
}
|
||||
alt={
|
||||
reservation.yacht
|
||||
.name
|
||||
}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
|
|
@ -173,7 +160,12 @@ export default function ReservationsPage() {
|
|||
Владелец
|
||||
</span>
|
||||
<span className="text-sm text-[#333333] font-bold">
|
||||
{reservation.ownerName}
|
||||
{
|
||||
reservation
|
||||
.yacht
|
||||
.owner
|
||||
.firstName
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -187,7 +179,11 @@ export default function ReservationsPage() {
|
|||
className="text-white"
|
||||
/>
|
||||
<span>
|
||||
{reservation.length} метров
|
||||
{formatWidth(
|
||||
reservation
|
||||
.yacht
|
||||
.length
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,7 +194,13 @@ export default function ReservationsPage() {
|
|||
size={16}
|
||||
className="text-white"
|
||||
/>
|
||||
<span>{reservation.capacity}</span>
|
||||
<span>
|
||||
{
|
||||
reservation
|
||||
.yacht
|
||||
.maxCapacity
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -214,8 +216,9 @@ export default function ReservationsPage() {
|
|||
Выход:
|
||||
</div>
|
||||
<div>
|
||||
{reservation.departureDate} -{" "}
|
||||
{reservation.departureTime}
|
||||
{formatUtcDate(
|
||||
reservation.startUtc
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#333333] w-full flex justify-between">
|
||||
|
|
@ -223,8 +226,9 @@ export default function ReservationsPage() {
|
|||
Заход:
|
||||
</div>
|
||||
<div>
|
||||
{reservation.arrivalDate} -{" "}
|
||||
{reservation.arrivalTime}
|
||||
{formatUtcDate(
|
||||
reservation.endUtc
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#333333] w-full flex justify-between">
|
||||
|
|
@ -232,24 +236,38 @@ export default function ReservationsPage() {
|
|||
Гости:
|
||||
</div>
|
||||
<div>
|
||||
{reservation.guests}
|
||||
{/* @TODO: Добавить количество гостей */}
|
||||
{/* {
|
||||
reservation.guests
|
||||
} */}
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#333333] w-full flex justify-between">
|
||||
<div>
|
||||
Тип оплаты:
|
||||
Тип
|
||||
оплаты:
|
||||
</div>
|
||||
<div>
|
||||
{reservation.paymentType}
|
||||
{/* @TODO: Добавить тип оплаты */}
|
||||
{/* {
|
||||
reservation.paymentType
|
||||
} */}
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-[#333333]">
|
||||
<Clock
|
||||
size={16}
|
||||
size={
|
||||
16
|
||||
}
|
||||
className="text-[#999999]"
|
||||
/>
|
||||
<span>
|
||||
По местному времени яхты
|
||||
По
|
||||
местному
|
||||
времени
|
||||
яхты
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -257,17 +275,26 @@ export default function ReservationsPage() {
|
|||
<div className="pt-3 border-t border-[#DFDFDF]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-bold text-[#333333]">
|
||||
Итого:{" "}
|
||||
{/* @TODO: Добавить итоговую стоимость */}
|
||||
{/* Итого:{" "}
|
||||
{formatPrice(
|
||||
reservation.totalPrice
|
||||
)}{" "}{reservation.paymentStatus ===
|
||||
)}{" "}
|
||||
{reservation.paymentStatus ===
|
||||
"pending" && (
|
||||
<span className="text-base font-bold text-red-500">
|
||||
(в ожидании оплаты)
|
||||
(в
|
||||
ожидании
|
||||
оплаты)
|
||||
</span>
|
||||
)} */}
|
||||
Итого: 78000{" "}
|
||||
<span className="text-base font-bold text-red-500">
|
||||
(в
|
||||
ожидании
|
||||
оплаты)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,44 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import ProfileSidebar from "@/app/profile/components/ProfileSidebar";
|
||||
import { MoveHorizontal, Users } from "lucide-react";
|
||||
import { getImageUrl, formatMinCost } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface Yacht {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
length: number;
|
||||
capacity: number;
|
||||
minCost: number;
|
||||
status: "active" | "moderation" | "archive";
|
||||
}
|
||||
|
||||
// Моковые данные для демонстрации
|
||||
const mockYachts: Yacht[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "KALLISTE",
|
||||
image: "/images/yachts/yacht1.jpg",
|
||||
length: 14,
|
||||
capacity: 10,
|
||||
minCost: 26400,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Señorita",
|
||||
image: "/images/yachts/yacht2.jpg",
|
||||
length: 14,
|
||||
capacity: 10,
|
||||
minCost: 37620,
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
import useAuthStore from "@/stores/useAuthStore";
|
||||
import { useEffect, useState } from "react";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
import { CatalogItemShortDto } from "@/api/types";
|
||||
|
||||
export default function YachtsPage() {
|
||||
const yachts = mockYachts;
|
||||
const [yachts, setYachts] = useState<CatalogItemShortDto[]>([]);
|
||||
const apiClient = useApiClient();
|
||||
const { getUserId } = useAuthStore();
|
||||
const userId = getUserId();
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
apiClient
|
||||
.get<CatalogItemShortDto[]>(`/catalog/user/${userId}`)
|
||||
.then((response) => {
|
||||
setYachts(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Ошибка при загрузке яхт:", error);
|
||||
});
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<main className="bg-[#f4f4f4]">
|
||||
|
|
@ -68,7 +58,9 @@ export default function YachtsPage() {
|
|||
<div className="flex-1 bg-white rounded-[16px] p-8">
|
||||
{/* Header with Add Button */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-[#333333]">Мои яхты</h2>
|
||||
<h2 className="text-2xl font-bold text-[#333333]">
|
||||
Мои яхты
|
||||
</h2>
|
||||
<Link href="/profile/yachts/add">
|
||||
<Button variant="gradient" size="default">
|
||||
Добавить
|
||||
|
|
@ -98,7 +90,7 @@ export default function YachtsPage() {
|
|||
<div className="relative rounded-[12px] overflow-hidden w-90 h-90 flex-shrink-0">
|
||||
<Image
|
||||
src={getImageUrl(
|
||||
yacht.image
|
||||
yacht.mainImageUrl
|
||||
)}
|
||||
alt={yacht.name}
|
||||
fill
|
||||
|
|
@ -129,9 +121,7 @@ export default function YachtsPage() {
|
|||
className="text-white"
|
||||
/>
|
||||
<span>
|
||||
{
|
||||
yacht.capacity
|
||||
}
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -162,10 +152,7 @@ export default function YachtsPage() {
|
|||
Вместимость:
|
||||
</div>
|
||||
<div>
|
||||
{
|
||||
yacht.capacity
|
||||
}{" "}
|
||||
человек
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[#333333] w-full flex justify-between">
|
||||
|
|
@ -183,6 +170,8 @@ export default function YachtsPage() {
|
|||
</div>
|
||||
<div className="pt-3 border-t border-[#DFDFDF]">
|
||||
<div className="flex items-center justify-between">
|
||||
{yacht.id && (
|
||||
<>
|
||||
<Link
|
||||
href={`/catalog/${yacht.id}`}
|
||||
className="text-sm text-[#2D908D] hover:underline"
|
||||
|
|
@ -196,6 +185,8 @@ export default function YachtsPage() {
|
|||
>
|
||||
Редактировать
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ interface AuthStore {
|
|||
setToken: (token: string, rememberMe?: boolean) => void;
|
||||
getToken: () => string | null;
|
||||
clearToken: () => void;
|
||||
setUserId: (userId: string | number, rememberMe?: boolean) => void;
|
||||
getUserId: () => string | null;
|
||||
clearUserId: () => void;
|
||||
}
|
||||
|
||||
const useAuthStore = create<AuthStore>((set) => ({
|
||||
const useAuthStore = create<AuthStore>(() => ({
|
||||
setToken: (token: string, rememberMe: boolean = false) => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (rememberMe) {
|
||||
localStorage.setItem("token", token);
|
||||
} else {
|
||||
|
|
@ -16,6 +20,7 @@ const useAuthStore = create<AuthStore>((set) => ({
|
|||
},
|
||||
|
||||
getToken: (): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const sessionToken = sessionStorage.getItem("token");
|
||||
if (sessionToken) {
|
||||
return sessionToken;
|
||||
|
|
@ -30,9 +35,41 @@ const useAuthStore = create<AuthStore>((set) => ({
|
|||
},
|
||||
|
||||
clearToken: () => {
|
||||
if (typeof window === "undefined") return;
|
||||
sessionStorage.removeItem("token");
|
||||
localStorage.removeItem("token");
|
||||
},
|
||||
|
||||
setUserId: (userId: string | number, rememberMe: boolean = false) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const userIdString = String(userId);
|
||||
if (rememberMe) {
|
||||
localStorage.setItem("userId", userIdString);
|
||||
} else {
|
||||
sessionStorage.setItem("userId", userIdString);
|
||||
}
|
||||
},
|
||||
|
||||
getUserId: (): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const sessionUserId = sessionStorage.getItem("userId");
|
||||
if (sessionUserId) {
|
||||
return sessionUserId;
|
||||
}
|
||||
|
||||
const localUserId = localStorage.getItem("userId");
|
||||
if (localUserId) {
|
||||
return localUserId;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
clearUserId: () => {
|
||||
if (typeof window === "undefined") return;
|
||||
sessionStorage.removeItem("userId");
|
||||
localStorage.removeItem("userId");
|
||||
},
|
||||
}));
|
||||
|
||||
export default useAuthStore;
|
||||
|
|
|
|||
Loading…
Reference in New Issue