Интеграция с бэком на главной странице

This commit is contained in:
Sergey Bolshakov 2025-12-14 20:41:54 +03:00
parent 85c23f136e
commit 7181718b0d
9 changed files with 477 additions and 201 deletions

View File

@ -1,6 +1,16 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "http",
hostname: "89.169.188.2",
pathname: '/**'
},
],
unoptimized: false,
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,

18
src/api/types.ts Normal file
View File

@ -0,0 +1,18 @@
interface CatalogItemDto {
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: CatalogItemDto;
restYachts: CatalogItemDto[];
}

View File

@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
@ -15,6 +16,10 @@ interface CatalogSidebarProps {
}
export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const [lengthRange, setLengthRange] = useState([7, 50]);
const [priceRange, setPriceRange] = useState([3000, 200000]);
const [yearRange, setYearRange] = useState([1991, 2025]);
@ -24,6 +29,138 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
const [quickBooking, setQuickBooking] = useState(false);
const [hasToilet, setHasToilet] = useState(false);
const [vesselType, setVesselType] = useState("");
const [date, setDate] = useState<Date | null>(null);
const [departureTime, setDepartureTime] = useState("12:00");
const [arrivalTime, setArrivalTime] = useState("13:00");
// Загрузка фильтров из searchParams при монтировании и изменении URL
useEffect(() => {
const lengthMin = searchParams.get("lengthMin");
const lengthMax = searchParams.get("lengthMax");
if (lengthMin && lengthMax) {
setLengthRange([parseInt(lengthMin), parseInt(lengthMax)]);
} else {
setLengthRange([7, 50]);
}
const priceMin = searchParams.get("priceMin");
const priceMax = searchParams.get("priceMax");
if (priceMin && priceMax) {
setPriceRange([parseInt(priceMin), parseInt(priceMax)]);
} else {
setPriceRange([3000, 200000]);
}
const yearMin = searchParams.get("yearMin");
const yearMax = searchParams.get("yearMax");
if (yearMin && yearMax) {
setYearRange([parseInt(yearMin), parseInt(yearMax)]);
} else {
setYearRange([1991, 2025]);
}
const adultsParam = searchParams.get("adults");
setAdults(adultsParam ? parseInt(adultsParam) : 0);
const childrenParam = searchParams.get("children");
setChildren(childrenParam ? parseInt(childrenParam) : 0);
const paymentTypeParam = searchParams.get("paymentType");
setPaymentType(paymentTypeParam || "all");
const quickBookingParam = searchParams.get("quickBooking");
setQuickBooking(quickBookingParam === "true");
const hasToiletParam = searchParams.get("hasToilet");
setHasToilet(hasToiletParam === "true");
const vesselTypeParam = searchParams.get("vesselType");
setVesselType(vesselTypeParam || "");
const dateParam = searchParams.get("date");
if (dateParam) {
const parsedDate = new Date(dateParam);
if (!isNaN(parsedDate.getTime())) {
setDate(parsedDate);
} else {
setDate(null);
}
} else {
setDate(null);
}
const departureTimeParam = searchParams.get("departureTime");
setDepartureTime(departureTimeParam || "12:00");
const arrivalTimeParam = searchParams.get("arrivalTime");
setArrivalTime(arrivalTimeParam || "13:00");
}, [searchParams]);
// Функция для сохранения фильтров в searchParams
const handleApplyFilters = () => {
const params = new URLSearchParams();
// Сохраняем только нестандартные значения
if (lengthRange[0] !== 7 || lengthRange[1] !== 50) {
params.set("lengthMin", lengthRange[0].toString());
params.set("lengthMax", lengthRange[1].toString());
}
if (priceRange[0] !== 3000 || priceRange[1] !== 200000) {
params.set("priceMin", priceRange[0].toString());
params.set("priceMax", priceRange[1].toString());
}
if (yearRange[0] !== 1991 || yearRange[1] !== 2025) {
params.set("yearMin", yearRange[0].toString());
params.set("yearMax", yearRange[1].toString());
}
if (adults > 0) {
params.set("adults", adults.toString());
}
if (children > 0) {
params.set("children", children.toString());
}
if (paymentType !== "all") {
params.set("paymentType", paymentType);
}
if (quickBooking) {
params.set("quickBooking", "true");
}
if (hasToilet) {
params.set("hasToilet", "true");
}
if (vesselType) {
params.set("vesselType", vesselType);
}
if (date) {
params.set("date", date.toISOString());
}
if (departureTime !== "12:00") {
params.set("departureTime", departureTime);
}
if (arrivalTime !== "13:00") {
params.set("arrivalTime", arrivalTime);
}
// Обновляем URL без прокрутки страницы
const newUrl = params.toString()
? `${pathname}?${params.toString()}`
: pathname;
router.replace(newUrl, { scroll: false });
// Вызываем callback, если он есть
onApply?.();
};
const formatPrice = (value: number) => {
return new Intl.NumberFormat("ru-RU").format(value) + " Р";
@ -39,6 +176,11 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
setQuickBooking(false);
setHasToilet(false);
setVesselType("");
setDate(null);
setDepartureTime("12:00");
setArrivalTime("13:00");
// Очищаем URL параметры без прокрутки страницы
router.replace(pathname, { scroll: false });
};
const activeFiltersCount = [
@ -50,6 +192,9 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
quickBooking,
hasToilet,
vesselType !== "",
date !== null,
departureTime !== "12:00",
arrivalTime !== "13:00",
].filter(Boolean).length;
return (
@ -111,7 +256,16 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
{/* Дата */}
<div>
<DatePicker variant="small" placeholder="Дата" />
<DatePicker
variant="small"
placeholder="Дата"
value={date}
departureTime={departureTime}
arrivalTime={arrivalTime}
onDateChange={(newDate) => setDate(newDate || null)}
onDepartureTimeChange={setDepartureTime}
onArrivalTimeChange={setArrivalTime}
/>
</div>
{/* Гостей */}
@ -259,7 +413,7 @@ export default function CatalogSidebar({ onApply }: CatalogSidebarProps) {
{/* Кнопка Применить */}
<Button
onClick={onApply}
onClick={handleApplyFilters}
className="w-full bg-[#008299] hover:bg-[#006d7f] text-white font-bold h-12 mt-2"
>
Применить

View File

@ -13,32 +13,10 @@ import Image from "next/image";
import Icon from "@/components/ui/icon";
import { useState } from "react";
import { GuestDatePicker } from "@/components/form/guest-date-picker";
import { formatMinCost, formatWidth, getImageUrl } from "@/lib/utils";
const yacht = {
name: "Яхта",
length: "12 метров",
price: "от 18 000 ₽",
perTime: "/ час",
feet: "7 Футов",
mainImage: "/images/featured-yacht/featured1.png",
thumbnails: [
"/images/featured-yacht/featured1.png",
"/images/featured-yacht/featured2.png",
"/images/featured-yacht/featured3.png",
"/images/featured-yacht/featured4.png",
"/images/featured-yacht/featured5.png",
"/images/featured-yacht/featured6.png",
"/images/featured-yacht/featured7.png",
"/images/featured-yacht/featured8.png",
"/images/featured-yacht/featured9.png",
"/images/featured-yacht/featured10.png",
],
isPromoted: true,
totalPrice: "0 ₽",
};
export default function FeaturedYacht() {
const [selectedImage, setSelectedImage] = useState(yacht.mainImage);
export default function FeaturedYacht({ yacht }: { yacht: CatalogItemDto }) {
const [selectedImage, setSelectedImage] = useState(yacht.mainImageUrl);
const handleThumbnailClick = (imageSrc: string) => {
setSelectedImage(imageSrc);
@ -75,7 +53,7 @@ export default function FeaturedYacht() {
<div className="flex items-center gap-2 text-gray-600">
<Icon size={16} name="width" />
<span className="text-lg">
{yacht.length}
{formatWidth(yacht.length)}
</span>
</div>
</div>
@ -83,11 +61,12 @@ export default function FeaturedYacht() {
{/* Main yacht image */}
<div className="relative mb-6">
<Image
src={selectedImage}
src={getImageUrl(selectedImage)}
alt={yacht.name}
width={600}
height={400}
className="w-full h-80 object-cover rounded-[24px]"
unoptimized
/>
</div>
@ -102,30 +81,29 @@ export default function FeaturedYacht() {
className="w-full"
>
<CarouselContent className="-ml-2 md:-ml-4">
{yacht.thumbnails.map((thumb, idx) => (
{yacht.galleryUrls.map((thumb, idx) => (
<CarouselItem
key={idx}
className="pl-2 md:pl-4 basis-auto"
>
<div className="relative">
<Image
src={thumb}
alt={`${
yacht.name
} view ${idx + 1}`}
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 ===
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"
}`}
? "border-[#008299]"
: "border-gray-200 hover:border-gray-400"
}`}
onClick={() =>
handleThumbnailClick(
thumb
)
}
unoptimized
/>
</div>
</CarouselItem>
@ -137,7 +115,7 @@ export default function FeaturedYacht() {
</div>
{/* Promoted badge */}
{yacht.isPromoted && (
{yacht.isFeatured && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Icon
className="min-w-[21px] min-h-[21px]"
@ -176,10 +154,10 @@ export default function FeaturedYacht() {
<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">
{yacht.price}
<p className="text-3xl font-bold whitespace-nowrap">
{formatMinCost(yacht.minCost)}
<span className="text-sm font-normal text-gray-500">
{yacht.perTime}
/ час
</span>
</p>
</div>
@ -202,7 +180,7 @@ export default function FeaturedYacht() {
<span className="font-normal">
Итого:
</span>
<span>{yacht.totalPrice}</span>
<span>0 </span>
</div>
</div>
</div>

View File

@ -1,63 +1,29 @@
"use client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import Icon from "@/components/ui/icon";
import Link from "next/link";
import FeaturedYacht from "./FeaturedYacht";
const yachts = [
{
name: "Яхта",
length: "12 метров",
price: "от 12 500 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht1.jpg",
bestOfferText: "🔥 Лучшее предложение",
colorPrice: true,
},
{
name: "Яхта",
length: "14 метров",
price: "от 26 400 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht2.jpg",
bestOfferText: "🍷 Идеальна для заката с бокалом вина",
},
{
name: "Яхта",
length: "22 метра",
price: "от 48 000 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht3.jpg",
bestOfferText: "⌛ Часто бронируется — успей",
},
{
name: "Яхта",
length: "13.6 метров",
price: "от 17 400 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht4.jpg",
badge: "По запросу",
},
{
name: "Яхта",
length: "13 метров",
price: "от 14 400 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht5.jpg",
badge: "По запросу",
},
{
name: "Яхта",
length: "12 метров",
price: "от 12 480 ₽ / час",
feet: "7 Футов",
img: "/images/yachts/yacht6.jpg",
badge: "По запросу",
},
];
import useApiClient from "@/hooks/useApiClient";
import { useEffect, useState } from "react";
import { formatMinCost, formatSpeed, formatWidth, getImageUrl } from "@/lib/utils";
export default function YachtGrid() {
const client = useApiClient();
const [featuredYacht, setFeaturedYacht] = useState<CatalogItemDto | null>(null);
const [yachtCatalog, setYachtCatalog] = useState<CatalogItemDto[] | null>(null);
useEffect(() => {
(async () => {
const response = await client.get<MainPageCatalogResponseDto>("/catalog/main-page/");
setFeaturedYacht(response.data.featuredYacht);
setYachtCatalog(response.data.restYachts);
})();
}, [])
return (
<section className="text-white">
<div className="container max-w-6xl mx-auto px-4 mt-6 md:mt-12">
@ -79,103 +45,107 @@ export default function YachtGrid() {
</div>
{/* Featured Yacht Block */}
<FeaturedYacht />
{featuredYacht && (
<FeaturedYacht yacht={featuredYacht} />
)}
{/* Yacht Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{yachts.map((yacht, idx) => (
<Link
key={idx}
href={`/catalog/${idx + 1}`}
className="block"
>
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
<CardHeader className="p-0 relative">
<div className="relative">
{/* Best Offer Badge - над карточкой */}
{yacht.bestOfferText && (
<div className="w-full flex justify-center">
<div
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
style={{
backgroundImage:
"url('/images/best-yacht-bg.jpg')",
}}
>
<span>
{yacht.bestOfferText}
</span>
</div>
</div>
)}
<Image
src={yacht.img}
alt={yacht.name}
width={400}
height={250}
className="w-full h-48 object-cover"
/>
{/* Badge Overlay */}
{yacht.badge && (
<>
<div className="absolute top-3 left-3">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<Icon
size={16}
name="restart"
/>
{yachtCatalog && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{yachtCatalog.map((yacht, idx) => (
<Link
key={idx}
href={`/catalog/${idx + 1}`}
className="block"
>
<Card className="overflow-hidden bg-white text-gray-900 cursor-pointer transition-all duration-200 hover:shadow-lg">
<CardHeader className="p-0 relative">
<div className="relative">
{/* Best Offer Badge - над карточкой */}
{yacht.topText && (
<div className="w-full flex justify-center">
<div
className="flex w-full items-center justify-center text-white px-4 py-2 text-sm font-medium bg-cover bg-center bg-no-repeat"
style={{
backgroundImage:
"url('/images/best-yacht-bg.jpg')",
}}
>
<span>
{yacht.badge}
{yacht.topText}
</span>
</div>
</div>
</>
)}
</div>
</CardHeader>
<CardContent className="p-4">
<div className="flex justify-between gap-4">
{/* Левая колонка - название и длина */}
<div className="space-y-2">
<h3 className="font-bold text-l">
{yacht.name}
</h3>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="width" />
<span>{yacht.length}</span>
</div>
)}
<Image
src={getImageUrl(yacht.mainImageUrl)}
alt={yacht.name}
width={400}
height={250}
className="w-full h-48 object-cover"
/>
{/* Badge Overlay */}
{yacht.hasQuickRent && !yacht.topText && (
<>
<div className="absolute top-3 left-3">
<div className="flex items-center justify-center bg-black/40 text-white px-3 py-1 rounded-lg text-sm flex items-center gap-1">
<Icon
size={16}
name="restart"
/>
<span>
По запросу
</span>
</div>
</div>
</>
)}
</div>
</CardHeader>
<CardContent className="p-4">
<div className="flex justify-between gap-4">
{/* Левая колонка - название и длина */}
<div className="space-y-2">
<h3 className="font-bold text-l">
{yacht.name}
</h3>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="width" />
<span>{formatWidth(yacht.length)}</span>
</div>
</div>
{/* Правая колонка - цена и футы */}
<div className="space-y-2">
<div className="w-fit">
{yacht.colorPrice ? (
<p
style={{
background:
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
}}
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px]"
>
{yacht.price}
</p>
) : (
<p className="w-fit text-l">
{yacht.price}
</p>
)}
</div>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="anchor" />
<span>{yacht.feet}</span>
{/* Правая колонка - цена и футы */}
<div className="flex flex-col justify-between">
<div className="w-fit">
{yacht.isBestOffer ? (
<p
style={{
background:
"linear-gradient(90deg, #008299 0%, #7E8FFF 100%)",
}}
className="text-l font-bold text-white pl-2 pr-3 rounded-t-[5px] rounded-bl-[10px] rounded-br-[30px] whitespace-nowrap"
>
{formatMinCost(yacht.minCost)} / час
</p>
) : (
<p className="w-fit text-l whitespace-nowrap">
{formatMinCost(yacht.minCost)} / час
</p>
)}
</div>
<div className="flex items-center gap-1 text-sm">
<Icon size={16} name="anchor" />
<span>{formatSpeed(yacht.speed)}</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
)}
{/* Call to Action Button */}
<div className="text-center">

View File

@ -5,11 +5,88 @@ import Image from "next/image";
import Link from "next/link";
import { User, ArrowUpRight, Map, ArrowLeft, Heart } from "lucide-react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
export default function ConfirmPage() {
const [promocode, setPromocode] = useState("");
const router = useRouter();
const searchParams = useSearchParams();
// Извлекаем параметры из URL
const yachtId = searchParams.get("yachtId");
const guestCount = searchParams.get("guestCount");
const departureDate = searchParams.get("departureDate");
const departureTime = searchParams.get("departureTime");
const arrivalDate = searchParams.get("arrivalDate");
const arrivalTime = searchParams.get("arrivalTime");
// Функция для форматирования даты (краткий формат)
const formatDate = (dateString: string | null) => {
if (!dateString) return null;
try {
const date = new Date(dateString);
const months = [
"янв", "фев", "мар", "апр", "май", "июн",
"июл", "авг", "сен", "окт", "ноя", "дек"
];
const day = date.getDate();
const month = months[date.getMonth()];
return `${day} ${month}`;
} catch {
return null;
}
};
// Функция для форматирования даты (полный формат для десктопа)
const formatDateFull = (dateString: string | null) => {
if (!dateString) return null;
try {
const date = new Date(dateString);
const months = [
"января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря"
];
const day = date.getDate();
const month = months[date.getMonth()];
return `${day} ${month}`;
} catch {
return null;
}
};
// Функция для форматирования времени
const formatTime = (timeString: string | null) => {
if (!timeString) return null;
// Предполагаем формат HH:mm или HH:mm:ss
return timeString.split(":").slice(0, 2).join(":");
};
// Форматируем данные для отображения
const departureDateFormatted = formatDate(departureDate);
const departureTimeFormatted = formatTime(departureTime);
const arrivalDateFormatted = formatDate(arrivalDate);
const arrivalTimeFormatted = formatTime(arrivalTime);
// Полный формат для десктопной версии
const departureDateFormattedFull = formatDateFull(departureDate);
const arrivalDateFormattedFull = formatDateFull(arrivalDate);
// Формируем строки для отображения
const departureDisplay = departureDateFormatted && departureTimeFormatted
? `${departureDateFormatted} ${departureTimeFormatted}`
: "Не выбрано";
const arrivalDisplay = arrivalDateFormatted && arrivalTimeFormatted
? `${arrivalDateFormatted} ${arrivalTimeFormatted}`
: "Не выбрано";
const datesDisplay = departureDateFormattedFull && departureTimeFormatted && arrivalDateFormattedFull && arrivalTimeFormatted
? `${departureDateFormattedFull} в ${departureTimeFormatted}${arrivalDateFormattedFull} в ${arrivalTimeFormatted}`
: "Не выбрано";
const guestsDisplay = guestCount
? guestCount === "1" ? "1 гость" : `${guestCount} гостей`
: "Не выбрано";
return (
<main className="bg-[#f4f4f4] grow">
@ -36,8 +113,8 @@ export default function ConfirmPage() {
Яхта Сеньорита
</h2>
<div className="flex justify-center gap-10 text-xs text-[#666666]">
<span>09 авг.</span>
<span>Гостей: 1</span>
<span>{departureDateFormatted || "Не выбрано"}</span>
<span>Гостей: {guestCount || "Не выбрано"}</span>
</div>
</div>
@ -69,7 +146,7 @@ export default function ConfirmPage() {
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">
9 Авг 00:00
{departureDisplay}
</div>
</div>
</div>
@ -79,7 +156,7 @@ export default function ConfirmPage() {
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF]">
<div className="text-[#333333]">
9 Авг 02:00
{arrivalDisplay}
</div>
</div>
</div>
@ -99,7 +176,7 @@ export default function ConfirmPage() {
</label>
<div className="bg-white rounded-full px-8 py-4 border border-[#DFDFDF] flex items-center justify-between">
<span className="text-[#333333]">
1 гость
{guestsDisplay}
</span>
</div>
</div>
@ -316,8 +393,7 @@ export default function ConfirmPage() {
Даты
</div>
<div className="text-base text-[#999999]">
9 августа в 00:00 9 августа в
02:00
{datesDisplay}
</div>
</div>
@ -328,7 +404,7 @@ export default function ConfirmPage() {
Гости
</div>
<div className="text-base text-[#999999]">
1 гость
{guestsDisplay}
</div>
</div>
</div>

View File

@ -18,14 +18,59 @@ interface DatePickerProps {
showIcon?: boolean;
variant?: "default" | "small";
placeholder?: string;
value?: Date | null;
departureTime?: string;
arrivalTime?: string;
onDateChange?: (date: Date | undefined) => void;
onDepartureTimeChange?: (time: string) => void;
onArrivalTimeChange?: (time: string) => void;
}
export function DatePicker({ showIcon = true, variant = "default", placeholder = "Выберите дату и время" }: DatePickerProps) {
const [date, setDate] = React.useState<Date>();
const [departureTime, setDepartureTime] = React.useState("12:00");
const [arrivalTime, setArrivalTime] = React.useState("13:00");
export function DatePicker({
showIcon = true,
variant = "default",
placeholder = "Выберите дату и время",
value,
departureTime: externalDepartureTime,
arrivalTime: externalArrivalTime,
onDateChange,
onDepartureTimeChange,
onArrivalTimeChange,
}: DatePickerProps) {
const [internalDate, setInternalDate] = React.useState<Date>();
const [internalDepartureTime, setInternalDepartureTime] = React.useState("12:00");
const [internalArrivalTime, setInternalArrivalTime] = React.useState("13:00");
const [open, setOpen] = React.useState(false);
// Используем внешние значения, если они предоставлены, иначе внутренние
const date = value !== undefined ? (value || undefined) : internalDate;
const departureTime = externalDepartureTime !== undefined ? externalDepartureTime : internalDepartureTime;
const arrivalTime = externalArrivalTime !== undefined ? externalArrivalTime : internalArrivalTime;
const handleDateChange = (newDate: Date | undefined) => {
if (onDateChange) {
onDateChange(newDate);
} else {
setInternalDate(newDate);
}
};
const handleDepartureTimeChange = (time: string) => {
if (onDepartureTimeChange) {
onDepartureTimeChange(time);
} else {
setInternalDepartureTime(time);
}
};
const handleArrivalTimeChange = (time: string) => {
if (onArrivalTimeChange) {
onArrivalTimeChange(time);
} else {
setInternalArrivalTime(time);
}
};
const handleApply = () => {
// Закрываем popover после применения
setOpen(false);
@ -71,7 +116,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
<Calendar
mode="single"
selected={date}
onSelect={setDate}
onSelect={handleDateChange}
className="mb-4 "
locale={ru}
disabled={(date) =>
@ -112,7 +157,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
type="time"
value={departureTime}
onChange={(e) =>
setDepartureTime(e.target.value)
handleDepartureTimeChange(e.target.value)
}
className="w-full focus:outline-none focus:border-transparent"
/>
@ -129,7 +174,7 @@ export function DatePicker({ showIcon = true, variant = "default", placeholder =
type="time"
value={arrivalTime}
onChange={(e) =>
setArrivalTime(e.target.value)
handleArrivalTimeChange(e.target.value)
}
className="w-full focus:outline-none focus:border-transparent"
/>

View File

@ -7,7 +7,7 @@ const useApiClient = () => {
const authPopup = useAuthPopup();
const apiClient = axios.create({
baseURL: "/api",
baseURL: "http://192.168.1.5:4000/",
headers: {
"Content-Type": "application/json",
},

View File

@ -4,3 +4,28 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const API_BASE_URL = "http://89.169.188.2";
export const getImageUrl = (relativePath: string): string => {
if (!relativePath) return "";
// Если путь уже абсолютный, возвращаем как есть
if (relativePath.startsWith("http://") || relativePath.startsWith("https://")) {
return relativePath;
}
// Убираем начальный слеш, если есть, и формируем абсолютный URL
const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
return `${API_BASE_URL}/${cleanPath}`;
};
export const formatWidth = (width: number): string => {
return width + " метров";
};
export const formatSpeed = (speed: number): string => {
return speed + " футов";
};
export const formatMinCost = (minCost: number): string => {
return "от " + minCost + " ₽";
};