From 81d293434cb205cc9a182ace4b6d5f650ba77b66 Mon Sep 17 00:00:00 2001 From: Sergey Bolshakov Date: Thu, 11 Dec 2025 04:13:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=91=D1=80=D1=81=D1=82=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20'=D0=9A?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=20=D0=AF=D1=85=D1=82'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 98 +++++++ package.json | 2 + public/images/icons/boat-year-max.svg | 15 + public/images/icons/boat-year-min.svg | 15 + public/images/icons/length-max.svg | 15 + public/images/icons/length-min.svg | 15 + public/images/icons/price-max.svg | 15 + public/images/icons/price-min.svg | 16 ++ src/app/catalog/components/CatalogSidebar.tsx | 263 ++++++++++++++++++ src/app/catalog/page.tsx | 263 ++++++++++++++++++ src/app/components/Hero.tsx | 12 +- src/app/components/YachtGrid.tsx | 15 +- src/components/form/guest-picker.tsx | 42 ++- src/components/ui/checkbox.tsx | 15 +- src/components/ui/date-picker.tsx | 10 +- src/components/ui/icon.tsx | 20 +- src/components/ui/radio-group.tsx | 45 +++ src/components/ui/select.tsx | 15 +- src/components/ui/slider.tsx | 38 +++ 19 files changed, 899 insertions(+), 30 deletions(-) create mode 100644 public/images/icons/boat-year-max.svg create mode 100644 public/images/icons/boat-year-min.svg create mode 100644 public/images/icons/length-max.svg create mode 100644 public/images/icons/length-min.svg create mode 100644 public/images/icons/price-max.svg create mode 100644 public/images/icons/price-min.svg create mode 100644 src/app/catalog/components/CatalogSidebar.tsx create mode 100644 src/app/catalog/page.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/slider.tsx diff --git a/package-lock.json b/package-lock.json index 0fd9e3e..e901afb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,10 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -3234,6 +3236,69 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", @@ -3300,6 +3365,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", diff --git a/package.json b/package.json index e37b553..f51005a 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/public/images/icons/boat-year-max.svg b/public/images/icons/boat-year-max.svg new file mode 100644 index 0000000..0ad4351 --- /dev/null +++ b/public/images/icons/boat-year-max.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/icons/boat-year-min.svg b/public/images/icons/boat-year-min.svg new file mode 100644 index 0000000..484ecb8 --- /dev/null +++ b/public/images/icons/boat-year-min.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/icons/length-max.svg b/public/images/icons/length-max.svg new file mode 100644 index 0000000..19ea4dc --- /dev/null +++ b/public/images/icons/length-max.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/icons/length-min.svg b/public/images/icons/length-min.svg new file mode 100644 index 0000000..f820508 --- /dev/null +++ b/public/images/icons/length-min.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/icons/price-max.svg b/public/images/icons/price-max.svg new file mode 100644 index 0000000..a466a20 --- /dev/null +++ b/public/images/icons/price-max.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/icons/price-min.svg b/public/images/icons/price-min.svg new file mode 100644 index 0000000..6b6dd3e --- /dev/null +++ b/public/images/icons/price-min.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/app/catalog/components/CatalogSidebar.tsx b/src/app/catalog/components/CatalogSidebar.tsx new file mode 100644 index 0000000..ddd1240 --- /dev/null +++ b/src/app/catalog/components/CatalogSidebar.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { useState } from "react"; +import { Slider } from "@/components/ui/slider"; +import { Checkbox } from "@/components/ui/checkbox"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { DatePicker } from "@/components/ui/date-picker"; +import { GuestPicker } from "@/components/form/guest-picker"; +import Icon from "@/components/ui/icon"; + +export default function CatalogSidebar() { + const [lengthRange, setLengthRange] = useState([7, 50]); + const [priceRange, setPriceRange] = useState([3000, 200000]); + const [yearRange, setYearRange] = useState([1991, 2025]); + const [adults, setAdults] = useState(0); + const [children, setChildren] = useState(0); + const [paymentType, setPaymentType] = useState("all"); + const [quickBooking, setQuickBooking] = useState(false); + const [hasToilet, setHasToilet] = useState(false); + const [vesselType, setVesselType] = useState(""); + + const formatPrice = (value: number) => { + return new Intl.NumberFormat("ru-RU").format(value) + " Р"; + }; + + const handleReset = () => { + setLengthRange([7, 50]); + setPriceRange([3000, 200000]); + setYearRange([1991, 2025]); + setAdults(0); + setChildren(0); + setPaymentType("all"); + setQuickBooking(false); + setHasToilet(false); + setVesselType(""); + }; + + const activeFiltersCount = [ + lengthRange[0] !== 7 || lengthRange[1] !== 50, + priceRange[0] !== 3000 || priceRange[1] !== 200000, + yearRange[0] !== 1991 || yearRange[1] !== 2025, + adults !== 0 || children !== 0, + paymentType !== "all", + quickBooking, + hasToilet, + vesselType !== "", + ].filter(Boolean).length; + + return ( + + ); +} diff --git a/src/app/catalog/page.tsx b/src/app/catalog/page.tsx new file mode 100644 index 0000000..84667ac --- /dev/null +++ b/src/app/catalog/page.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import Image from "next/image"; +import Icon from "@/components/ui/icon"; +import CatalogSidebar from "@/app/catalog/components/CatalogSidebar"; +import Link from "next/link"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const yachts = [ + { + name: "Яхта", + length: "12 метров", + price: "от 12 500 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht1.jpg", + quickBooking: true, + }, + { + name: "Яхта", + length: "12 метров", + price: "от 13 200 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht2.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "8 метров", + price: "от 7 000 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht3.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "13 метров", + price: "от 12 000 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht4.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "13 метров", + price: "от 30 000 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht5.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "10 метров", + price: "от 8 500 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht6.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "8 метров", + price: "от 6 000 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht1.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "10 метров", + price: "от 6 960 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht2.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "10 метров", + price: "от 6 600 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht3.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "13 метров", + price: "от 18 000 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht4.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "10 метров", + price: "от 7 200 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht5.jpg", + badge: "По запросу", + }, + { + name: "Яхта", + length: "11 метров", + price: "от 7 200 ₽ / час", + feet: "7 Футов", + img: "/images/yachts/yacht6.jpg", + badge: "По запросу", + }, +]; + +export default function CatalogPage() { + return ( +
+
+ {/* Breadcrumbs */} +
+ + + Аренда яхты + + + > + Каталог яхт +
+ +
+ {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+ {/* Header */} +
+
+

+ Аренда яхт +

+

+ Доступно яхт:{" "} + + {yachts.length} + +

+
+
+
Сортировка:
+ +
+
+ + {/* Yacht Grid */} +
+ {yachts.map((yacht, idx) => ( +
+ + +
+ {/* Quick Booking Badge */} + {yacht.quickBooking && ( +
+
+ Быстрая бронь +
+
+ )} + {yacht.name} + {/* Badge Overlay */} + {yacht.badge && ( +
+
+ + + {yacht.badge} + +
+
+ )} +
+
+ +
+ {/* Левая колонка - название и длина */} +
+

+ {yacht.name} +

+
+ + + {yacht.length} + +
+
+ + {/* Правая колонка - цена и футы */} +
+

+ {yacht.price} +

+
+ + + {yacht.feet} + +
+
+
+
+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/src/app/components/Hero.tsx b/src/app/components/Hero.tsx index 7197ce9..9e80b46 100644 --- a/src/app/components/Hero.tsx +++ b/src/app/components/Hero.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import Image from "next/image"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -8,6 +9,8 @@ import Icon from "@/components/ui/icon"; import { GuestPicker } from "@/components/form/guest-picker"; export default function Hero() { + const [adults, setAdults] = useState(0); + const [children, setChildren] = useState(0); return (
- + { + setAdults(adults); + setChildren(children); + }} + /> {/* Кнопка поиска */} diff --git a/src/app/components/YachtGrid.tsx b/src/app/components/YachtGrid.tsx index bc2b86f..5e69cbf 100644 --- a/src/app/components/YachtGrid.tsx +++ b/src/app/components/YachtGrid.tsx @@ -2,6 +2,7 @@ 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 = [ @@ -174,12 +175,14 @@ export default function YachtGrid() { {/* Call to Action Button */}
- + + +
diff --git a/src/components/form/guest-picker.tsx b/src/components/form/guest-picker.tsx index dfbde6a..4c47aac 100644 --- a/src/components/form/guest-picker.tsx +++ b/src/components/form/guest-picker.tsx @@ -12,29 +12,47 @@ import { Counter } from "../ui/counter"; import { ChevronUp, ChevronDown } from "lucide-react"; interface GuestPickerProps { + adults?: number; + childrenCount?: number; + onChange?: (adults: number, children: number) => void; onApply?: (adults: number, children: number) => void; className?: string; showIcon?: boolean; + variant?: "default" | "small"; + placeholder?: string; } -export const GuestPicker: React.FC = ({ onApply, showIcon = true }) => { - const [adults, setAdults] = useState(0); - const [children, setChildren] = useState(0); +export const GuestPicker: React.FC = ({ + adults = 0, + childrenCount = 0, + onChange, + showIcon = true, + variant = "default", + placeholder = "Сколько гостей?" +}) => { const [isOpen, setIsOpen] = useState(false); + const handleAdultsChange = (value: number) => { + onChange?.(value, childrenCount); + }; + + const handleChildrenChange = (value: number) => { + onChange?.(adults, value); + }; + const handleApply = () => { - onApply?.(adults, children); + onChange?.(adults, childrenCount); setIsOpen(false); }; const getDisplayText = () => { - const total = adults + children; - if (total === 0) return "Сколько гостей?"; - if (children === 0) + const total = adults + childrenCount; + if (total === 0) return placeholder; + if (childrenCount === 0) return `${adults} ${adults === 1 ? "взрослый" : "взрослых"}`; return `${adults} ${ adults === 1 ? "взрослый" : "взрослых" - }, ${children} ${children === 1 ? "ребенок" : "детей"}`; + }, ${childrenCount} ${childrenCount === 1 ? "ребенок" : "детей"}`; }; return ( @@ -46,7 +64,7 @@ export const GuestPicker: React.FC = ({ onApply, showIcon = tr