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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
src/api/types.ts
127
src/api/types.ts
|
|
@ -1,77 +1,86 @@
|
|||
interface CatalogItemShortDto {
|
||||
id?: number;
|
||||
name: string;
|
||||
length: number;
|
||||
speed: number;
|
||||
minCost: number;
|
||||
mainImageUrl: string;
|
||||
galleryUrls: string[];
|
||||
hasQuickRent: boolean;
|
||||
isFeatured: boolean;
|
||||
topText?: string;
|
||||
isBestOffer?: boolean;
|
||||
export interface CatalogItemShortDto {
|
||||
id?: number;
|
||||
name: string;
|
||||
length: number;
|
||||
speed: number;
|
||||
minCost: number;
|
||||
mainImageUrl: string;
|
||||
galleryUrls: string[];
|
||||
hasQuickRent: boolean;
|
||||
isFeatured: boolean;
|
||||
topText?: string;
|
||||
isBestOffer?: boolean;
|
||||
}
|
||||
|
||||
interface MainPageCatalogResponseDto {
|
||||
featuredYacht: CatalogItemShortDto;
|
||||
restYachts: CatalogItemShortDto[];
|
||||
export interface MainPageCatalogResponseDto {
|
||||
featuredYacht: CatalogItemShortDto;
|
||||
restYachts: CatalogItemShortDto[];
|
||||
}
|
||||
|
||||
interface CatalogFilteredResponseDto {
|
||||
items: CatalogItemShortDto[];
|
||||
total: number;
|
||||
export interface CatalogFilteredResponseDto {
|
||||
items: CatalogItemShortDto[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Reservation {
|
||||
id: number;
|
||||
yachtId: number;
|
||||
reservatorId: number;
|
||||
startUtc: number;
|
||||
endUtc: number;
|
||||
id: number;
|
||||
yachtId: number;
|
||||
reservatorId: number;
|
||||
startUtc: number;
|
||||
endUtc: number;
|
||||
}
|
||||
|
||||
interface Review {
|
||||
id: number;
|
||||
reviewerId: number;
|
||||
yachtId: number;
|
||||
starsCount: number;
|
||||
description: string;
|
||||
id: number;
|
||||
reviewerId: number;
|
||||
yachtId: number;
|
||||
starsCount: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
userId?: number;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
yachts?: Yacht[];
|
||||
companyName?: string;
|
||||
inn?: number;
|
||||
ogrn?: number;
|
||||
export interface User {
|
||||
userId?: number;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
yachts?: Yacht[];
|
||||
companyName?: string;
|
||||
inn?: number;
|
||||
ogrn?: number;
|
||||
}
|
||||
|
||||
interface CatalogItemLongDto extends CatalogItemShortDto {
|
||||
year: number;
|
||||
comfortCapacity: number;
|
||||
maxCapacity: number;
|
||||
width: number;
|
||||
cabinsCount: number;
|
||||
matherial: string;
|
||||
power: number;
|
||||
description: string;
|
||||
owner: User;
|
||||
reviews: Review[];
|
||||
reservations: Reservation[];
|
||||
export interface CatalogItemLongDto extends CatalogItemShortDto {
|
||||
year: number;
|
||||
comfortCapacity: number;
|
||||
maxCapacity: number;
|
||||
width: number;
|
||||
cabinsCount: number;
|
||||
matherial: string;
|
||||
power: number;
|
||||
description: string;
|
||||
owner: User;
|
||||
reviews: Review[];
|
||||
reservations: Reservation[];
|
||||
}
|
||||
|
||||
interface Yacht {
|
||||
yachtId: number;
|
||||
name: string;
|
||||
model: string;
|
||||
year: number;
|
||||
length: number;
|
||||
userId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
yachtId: number;
|
||||
name: string;
|
||||
model: string;
|
||||
year: number;
|
||||
length: number;
|
||||
userId: number;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -3,247 +3,270 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@/components/ui/carousel";
|
||||
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,
|
||||
yacht,
|
||||
}: {
|
||||
yacht: CatalogItemShortDto;
|
||||
yacht: CatalogItemShortDto;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
||||
const [bookingData, setBookingData] = useState<GuestDatePickerValue>({
|
||||
date: undefined,
|
||||
departureTime: "12:00",
|
||||
arrivalTime: "13:00",
|
||||
adults: 1,
|
||||
children: 0,
|
||||
});
|
||||
|
||||
const handleThumbnailClick = (imageSrc: string) => {
|
||||
setSelectedImage(imageSrc);
|
||||
};
|
||||
|
||||
// Расчет итоговой стоимости
|
||||
const getTotalPrice = () => {
|
||||
if (!bookingData.date || !bookingData.departureTime || !bookingData.arrivalTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Форматируем дату в ISO строку для calculateTotalPrice
|
||||
const dateString = format(bookingData.date, "yyyy-MM-dd");
|
||||
|
||||
const { totalPrice } = calculateTotalPrice(
|
||||
dateString,
|
||||
bookingData.departureTime,
|
||||
dateString, // Используем ту же дату для arrival
|
||||
bookingData.arrivalTime,
|
||||
yacht.minCost
|
||||
);
|
||||
|
||||
return totalPrice;
|
||||
};
|
||||
|
||||
// Обработчик нажатия на кнопку "Забронировать"
|
||||
const handleBookClick = () => {
|
||||
if (!bookingData.date || !yacht.id) {
|
||||
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(),
|
||||
const router = useRouter();
|
||||
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
|
||||
const [bookingData, setBookingData] = useState<GuestDatePickerValue>({
|
||||
date: undefined,
|
||||
departureTime: "12:00",
|
||||
arrivalTime: "13:00",
|
||||
adults: 1,
|
||||
children: 0,
|
||||
});
|
||||
|
||||
// Переходим на страницу подтверждения
|
||||
router.push(`/confirm?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<Card className="overflow-hidden bg-white text-gray-900">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col lg:flex-row gap-11 px-6 py-10">
|
||||
{/* Left side - Yacht details and images */}
|
||||
<div className="flex-1">
|
||||
{/* Promoted banner - Mobile only */}
|
||||
<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)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium relative z-10">
|
||||
Заметнее других — бронируют быстрее
|
||||
</span>
|
||||
</div>
|
||||
const handleThumbnailClick = (imageSrc: string) => {
|
||||
setSelectedImage(imageSrc);
|
||||
};
|
||||
|
||||
{/* Header with yacht name and length */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
// Расчет итоговой стоимости
|
||||
const getTotalPrice = () => {
|
||||
if (
|
||||
!bookingData.date ||
|
||||
!bookingData.departureTime ||
|
||||
!bookingData.arrivalTime
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
{/* Main yacht image */}
|
||||
<div className="relative mb-6">
|
||||
<Image
|
||||
src={getImageUrl(selectedImage)}
|
||||
alt={yacht.name}
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-80 object-cover rounded-[24px]"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
// Форматируем дату в ISO строку для calculateTotalPrice
|
||||
const dateString = format(bookingData.date, "yyyy-MM-dd");
|
||||
|
||||
{/* Thumbnail images carousel */}
|
||||
<div className="relative mb-6">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: false,
|
||||
slidesToScroll: 2,
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent className="-ml-2 md:-ml-4">
|
||||
{yacht.galleryUrls.map((thumb, idx) => (
|
||||
<CarouselItem
|
||||
key={idx}
|
||||
className="pl-2 md:pl-4 basis-auto"
|
||||
>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={getImageUrl(thumb)}
|
||||
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
|
||||
? "border-[#008299]"
|
||||
: "border-gray-200 hover:border-gray-400"
|
||||
}`}
|
||||
onClick={() => handleThumbnailClick(thumb)}
|
||||
unoptimized
|
||||
/>
|
||||
const { totalPrice } = calculateTotalPrice(
|
||||
dateString,
|
||||
bookingData.departureTime,
|
||||
dateString, // Используем ту же дату для arrival
|
||||
bookingData.arrivalTime,
|
||||
yacht.minCost
|
||||
);
|
||||
|
||||
return totalPrice;
|
||||
};
|
||||
|
||||
// Обработчик нажатия на кнопку "Забронировать"
|
||||
const handleBookClick = () => {
|
||||
if (!bookingData.date || !yacht.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Формируем URL с параметрами
|
||||
const params = new URLSearchParams({
|
||||
yachtId: yacht.id.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(),
|
||||
});
|
||||
|
||||
// Переходим на страницу подтверждения
|
||||
router.push(`/confirm?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<Card className="overflow-hidden bg-white text-gray-900">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col lg:flex-row gap-11 px-6 py-10">
|
||||
{/* Left side - Yacht details and images */}
|
||||
<div className="flex-1">
|
||||
{/* Promoted banner - Mobile only */}
|
||||
<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)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium relative z-10">
|
||||
Заметнее других — бронируют быстрее
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Header with yacht name and length */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main yacht image */}
|
||||
<div className="relative mb-6">
|
||||
<Image
|
||||
src={getImageUrl(selectedImage)}
|
||||
alt={yacht.name}
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-80 object-cover rounded-[24px]"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail images carousel */}
|
||||
<div className="relative mb-6">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: false,
|
||||
slidesToScroll: 2,
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent className="-ml-2 md:-ml-4">
|
||||
{yacht.galleryUrls.map((thumb, idx) => (
|
||||
<CarouselItem
|
||||
key={idx}
|
||||
className="pl-2 md:pl-4 basis-auto"
|
||||
>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={getImageUrl(thumb)}
|
||||
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
|
||||
? "border-[#008299]"
|
||||
: "border-gray-200 hover:border-gray-400"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleThumbnailClick(
|
||||
thumb
|
||||
)
|
||||
}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
|
||||
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
{/* Promoted badge */}
|
||||
{yacht.isFeatured && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Icon
|
||||
className="min-w-[21px] min-h-[21px]"
|
||||
size={21}
|
||||
name="ad"
|
||||
/>
|
||||
<span>
|
||||
Это объявление продвигается.{" "}
|
||||
<span className="underline cursor-pointer">
|
||||
Хотите так же?
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
|
||||
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 bg-white hover:bg-gray-50 shadow-lg z-10" />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
{/* Promoted badge */}
|
||||
{yacht.isFeatured && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Icon
|
||||
className="min-w-[21px] min-h-[21px]"
|
||||
size={21}
|
||||
name="ad"
|
||||
/>
|
||||
<span>
|
||||
Это объявление продвигается.{" "}
|
||||
<span className="underline cursor-pointer">
|
||||
Хотите так же?
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right side - Booking form */}
|
||||
<div className="min-w-[296px] flex-0 flex flex-col justify-between">
|
||||
<div>
|
||||
{/* Promoted banner - Desktop only */}
|
||||
<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)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium relative z-10">
|
||||
Заметнее других — бронируют быстрее
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right side - Booking form */}
|
||||
<div className="min-w-[296px] flex-0 flex flex-col justify-between">
|
||||
<div>
|
||||
{/* Promoted banner - Desktop only */}
|
||||
<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)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium relative z-10">
|
||||
Заметнее других — бронируют быстрее
|
||||
</span>
|
||||
</div>
|
||||
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
|
||||
{/* Price */}
|
||||
<div className="mb-6">
|
||||
<p className="text-3xl font-bold whitespace-nowrap">
|
||||
{formatMinCost(yacht.minCost)}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
/ час
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-[16px] p-6 pb-8 border-gray-200 pt-6">
|
||||
{/* Price */}
|
||||
<div className="mb-6">
|
||||
<p className="text-3xl font-bold whitespace-nowrap">
|
||||
{formatMinCost(yacht.minCost)}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
/ час
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{/* Booking form */}
|
||||
<div className="mb-8">
|
||||
<GuestDatePicker
|
||||
value={bookingData}
|
||||
onChange={setBookingData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Booking form */}
|
||||
<div className="mb-8">
|
||||
<GuestDatePicker
|
||||
value={bookingData}
|
||||
onChange={setBookingData}
|
||||
/>
|
||||
</div>
|
||||
{/* Book button */}
|
||||
<Button
|
||||
variant="gradient"
|
||||
className="font-bold text-white h-[64px] w-full px-8"
|
||||
onClick={handleBookClick}
|
||||
disabled={
|
||||
!bookingData.date || !yacht.id
|
||||
}
|
||||
>
|
||||
Забронировать
|
||||
</Button>
|
||||
|
||||
{/* Book button */}
|
||||
<Button
|
||||
variant="gradient"
|
||||
className="font-bold text-white h-[64px] w-full px-8"
|
||||
onClick={handleBookClick}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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,37 +75,41 @@ 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"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
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"
|
||||
? "border-[#333333] bg-white text-[#333333] font-bold"
|
||||
: "border-transparent text-[#999999] hover:text-[#333333]"
|
||||
}`}
|
||||
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]"
|
||||
}`}
|
||||
>
|
||||
Активные
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("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]"
|
||||
}`}
|
||||
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]"
|
||||
}`}
|
||||
>
|
||||
Подтвержденные
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("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]"
|
||||
}`}
|
||||
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]"
|
||||
}`}
|
||||
>
|
||||
Архив
|
||||
</button>
|
||||
|
|
@ -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>
|
||||
)}
|
||||
<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,19 +170,23 @@ export default function YachtsPage() {
|
|||
</div>
|
||||
<div className="pt-3 border-t border-[#DFDFDF]">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href={`/catalog/${yacht.id}`}
|
||||
className="text-sm text-[#2D908D] hover:underline"
|
||||
>
|
||||
Посмотреть
|
||||
объявление
|
||||
</Link>
|
||||
<Link
|
||||
href={`/profile/yachts/${yacht.id}/edit`}
|
||||
className="text-sm text-[#2D908D] hover:underline"
|
||||
>
|
||||
Редактировать
|
||||
</Link>
|
||||
{yacht.id && (
|
||||
<>
|
||||
<Link
|
||||
href={`/catalog/${yacht.id}`}
|
||||
className="text-sm text-[#2D908D] hover:underline"
|
||||
>
|
||||
Посмотреть
|
||||
объявление
|
||||
</Link>
|
||||
<Link
|
||||
href={`/profile/yachts/${yacht.id}/edit`}
|
||||
className="text-sm text-[#2D908D] hover:underline"
|
||||
>
|
||||
Редактировать
|
||||
</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