first commit

This commit is contained in:
Samandar Turgunboyev
2025-10-18 17:14:59 +05:00
parent edf364b389
commit 036a36ce90
92 changed files with 14614 additions and 135 deletions

View File

@@ -0,0 +1,38 @@
"use client";
import StepOne from "@/pages/tours/ui/StepOne";
import StepTwo from "@/pages/tours/ui/StepTwo";
import { Hotel, Plane } from "lucide-react";
import { useMemo, useState } from "react";
import { useParams } from "react-router-dom";
const CreateEditTour = () => {
const { id } = useParams<{ id: string }>();
const isEditMode = useMemo(() => !!id, [id]);
const [step, setStep] = useState(1);
return (
<div className="p-8 w-full mx-auto bg-gray-900">
<h1 className="text-3xl font-bold mb-6 text-center">
{isEditMode ? "Turni Tahrirlash" : "Yangi Tur Qo'shish"}
</h1>
<div className="flex justify-between items-center mb-8">
<div
className={`flex-1 text-center py-2 rounded-l-lg ${step === 1 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground border"}`}
>
1. Tur ma'lumotlari <Plane className="w-5 h-5 inline ml-2" />
</div>
<div
className={`flex-1 text-center py-2 rounded-r-lg ${step === 2 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground border"}`}
>
2. Mehmonxona <Hotel className="w-5 h-5 inline ml-2" />
</div>
</div>
{step === 1 && <StepOne setStep={setStep} />}
{step === 2 && <StepTwo setStep={setStep} />}
</div>
);
};
export default CreateEditTour;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,287 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Dispatch, SetStateAction } from "react";
import { Controller, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import z from "zod";
const formSchema = z.object({
title: z.string().min(2, {
message: "Sarlavha kamida 2 ta belgidan iborat bolishi kerak.",
}),
rating: z.number(),
mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }),
hotelType: z
.string()
.min(1, { message: "Mehmonxona turi tanlanishi majburiy" }),
hotelFeatures: z
.array(z.string())
.min(1, { message: "Kamida 1 ta xususiyat tanlanishi kerak" }),
});
const StepTwo = ({
setStep,
}: {
setStep: Dispatch<SetStateAction<number>>;
}) => {
const navigator = useNavigate();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
rating: 3.0,
mealPlan: "",
hotelType: "",
hotelFeatures: [],
},
});
function onSubmit() {
navigator("tours");
}
const mealPlans = [
"Breakfast Only",
"Half Board",
"Full Board",
"All Inclusive",
];
const hotelTypes = ["Hotel", "Resort", "Guest House", "Apartment"];
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
{/* Mehmonxona nomi */}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<Label className="text-md">Mehmonxona nomi</Label>
<FormControl>
<Input
placeholder="Toshkent - Dubay"
{...field}
className="h-12 !text-md"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Mehmonxona rating */}
<FormField
control={form.control}
name="rating"
render={({ field }) => (
<FormItem>
<Label className="text-md">Mehmonxona raytingi</Label>
<FormControl>
<Input
type="text"
placeholder="3.0"
{...field}
className="h-12 !text-md"
onChange={(e) => {
const val = e.target.value;
if (/^\d*\.?\d*$/.test(val)) {
field.onChange(val);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Meal Plan */}
<FormField
control={form.control}
name="mealPlan"
render={() => (
<FormItem>
<Label className="text-md">Meal Plan</Label>
<FormControl>
<Controller
control={form.control}
name="mealPlan"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue placeholder="Taom rejasini tanlang" />
</SelectTrigger>
<SelectContent>
{mealPlans.map((plan) => (
<SelectItem key={plan} value={plan}>
{plan}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Hotel Type */}
<FormField
control={form.control}
name="hotelType"
render={() => (
<FormItem>
<Label className="text-md">Mehmonxona turi</Label>
<FormControl>
<Controller
control={form.control}
name="hotelType"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue placeholder="Mehmonxona turini tanlang" />
</SelectTrigger>
<SelectContent>
{hotelTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="hotelFeatures"
render={() => (
<FormItem>
<Label className="text-md">Mehmonxona qulayliklar</Label>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2">
{form.watch("amenities").map((item, idx) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Icon =
(LucideIcons as any)[item.icon_name] || XIcon;
return (
<Badge
key={idx}
variant="secondary"
className="px-3 py-1 text-sm flex items-center gap-2"
>
<Icon className="size-4" />
<span>{item.name}</span>
<button
type="button"
onClick={() => {
const current = form.getValues("amenities");
form.setValue(
"amenities",
current.filter((_, i: number) => i !== idx),
);
}}
className="ml-1 text-muted-foreground hover:text-destructive"
>
<XIcon className="size-4" />
</button>
</Badge>
);
})}
</div>
<div className="flex gap-3 items-center">
<IconSelect
setSelectedIcon={setSelectedIcon}
selectedIcon={selectedIcon}
/>
<Input
id="amenity_name"
placeholder="Qulaylik nomi (masalan: Wi-Fi)"
className="h-12 !text-md flex-1"
/>
<Button
type="button"
onClick={() => {
const nameInput = document.getElementById(
"amenity_name",
) as HTMLInputElement;
if (selectedIcon && nameInput.value) {
const current = form.getValues("amenities");
form.setValue("amenities", [
...current,
{
icon_name: selectedIcon,
name: nameInput.value,
},
]);
nameInput.value = "";
setSelectedIcon("");
}
}}
className="h-12"
>
Qoshish
</Button>
</div>
<FormMessage />
</div>
</FormItem>
)}
/> */}
</div>
<div className="flex justify-between">
<button
type="button"
onClick={() => setStep(1)}
className="mt-6 px-6 py-3 bg-gray-600 text-white rounded-md"
>
Ortga
</button>
<button
type="submit"
className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md"
>
Saqlash
</button>
</div>
</form>
</Form>
);
};
export default StepTwo;

View File

@@ -0,0 +1,110 @@
"use client";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { ImagePlus, XIcon } from "lucide-react";
import { useState } from "react";
interface TicketsImagesModelProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: any;
name: string;
label?: string;
}
export default function TicketsImagesModel({
form,
name,
label = "Rasmlar",
}: TicketsImagesModelProps) {
const [previews, setPreviews] = useState<string[]>([]);
return (
<FormField
control={form.control}
name={name}
render={() => (
<FormItem>
<Label className="text-md">{label}</Label>
<FormControl>
<div className="flex flex-col gap-3">
<Input
id="ticket-images"
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => {
const newFiles = e.target.files
? Array.from(e.target.files)
: [];
const existingFiles = form.getValues(name) || [];
const allFiles = [...existingFiles, ...newFiles];
form.setValue(name, allFiles);
const urls = allFiles.map((file) =>
URL.createObjectURL(file),
);
setPreviews(urls);
}}
/>
{/* Upload Zone */}
<label
htmlFor="ticket-images"
className="border-2 border-dashed border-gray-300 h-40 rounded-2xl flex flex-col justify-center items-center cursor-pointer hover:bg-muted/20 transition"
>
<ImagePlus className="size-8 text-muted-foreground mb-2" />
<p className="font-semibold text-white">Rasmlarni tanlang</p>
<p className="text-sm text-white">
Bir nechta rasm yuklashingiz mumkin
</p>
</label>
{/* Preview Images */}
{previews.length > 0 && (
<div className="flex flex-wrap gap-3">
{previews.map((src, i) => (
<div
key={i}
className="relative size-24 rounded-md overflow-hidden border"
>
<img
src={src}
alt={`preview-${i}`}
className="object-cover w-full h-full"
/>
<button
type="button"
onClick={() => {
const newFiles = form
.getValues(name)
.filter((_: File, idx: number) => idx !== i);
const newPreviews = previews.filter(
(_: string, idx: number) => idx !== i,
);
form.setValue(name, newFiles);
setPreviews(newPreviews);
}}
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white"
>
<XIcon className="size-4 text-destructive" />
</button>
</div>
))}
</div>
)}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@@ -0,0 +1,642 @@
"use client";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import {
ArrowLeft,
Calendar,
CheckCircle2,
Clock,
DollarSign,
Globe,
Heart,
Hotel,
MapPin,
Star,
Users,
Utensils,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
type TourDetail = {
id: number;
title: string;
price: number;
departure_date: string;
departure: string;
destination: string;
passenger_count: number;
languages: string;
rating: number;
hotel_info: string;
duration_days: number;
hotel_meals: string;
ticket_images: Array<{ image: string }>;
ticket_amenities: Array<{ name: string; icon_name: string }>;
ticket_included_services: Array<{
image: string;
title: string;
desc: string;
}>;
ticket_itinerary: Array<{
title: string;
duration: number;
ticket_itinerary_image: Array<{ image: string }>;
ticket_itinerary_destinations: Array<{ name: string }>;
}>;
ticket_hotel_meals: Array<{ image: string; name: string; desc: string }>;
travel_agency_id: string;
ticket_comments: Array<{
user: { id: number; username: string };
text: string;
rating: number;
}>;
tariff: Array<{ name: string }>;
is_liked: string;
};
export default function TourDetailPage() {
const params = useParams();
const router = useNavigate();
const [tour, setTour] = useState<TourDetail | null>(null);
useEffect(() => {
setTour({
id: Number(params.id),
title: "Dubai Hashamatli Sayohati",
price: 1500000,
departure_date: "2025-11-15",
departure: "Toshkent, O'zbekiston",
destination: "Dubai, BAA",
passenger_count: 30,
languages: "O'zbek, Rus, Ingliz",
rating: 4.8,
hotel_info: "5 yulduzli Atlantis The Palm mehmonxonasi",
duration_days: 7,
hotel_meals: "Nonushta va kechki ovqat kiritilgan",
ticket_images: [
{ image: "/dubai-burj-khalifa.png" },
{ image: "/dubai-palm-jumeirah.jpg" },
{ image: "/dubai-marina.jpg" },
{ image: "/dubai-desert-safari.png" },
],
ticket_amenities: [
{ name: "Wi-Fi", icon_name: "wifi" },
{ name: "Konditsioner", icon_name: "air-vent" },
{ name: "Basseyn", icon_name: "waves" },
{ name: "Fitnes zal", icon_name: "dumbbell" },
{ name: "Spa markaz", icon_name: "sparkles" },
{ name: "Restoran", icon_name: "utensils" },
],
ticket_included_services: [
{
image: "/airplane-ticket.jpg",
title: "Aviachiptalar",
desc: "Toshkent-Dubai-Toshkent yo'nalishi bo'yicha qatnov chiptalar",
},
{
image: "/comfortable-hotel-room.png",
title: "Mehmonxona",
desc: "5 yulduzli mehmonxonada 6 kecha turar joy",
},
{
image: "/diverse-tour-group.png",
title: "Gid xizmati",
desc: "Professional gid bilan barcha ekskursiyalar",
},
{
image: "/transfer-car.jpg",
title: "Transfer",
desc: "Aeroport-mehmonxona-aeroport transferi",
},
],
ticket_itinerary: [
{
title: "Dubayga kelish va mehmonxonaga joylashish",
duration: 1,
ticket_itinerary_image: [{ image: "/dubai-airport.jpg" }],
ticket_itinerary_destinations: [
{ name: "Dubai Xalqaro Aeroporti" },
{ name: "Atlantis The Palm" },
],
},
{
title: "Burj Khalifa va Dubai Mall sayohati",
duration: 1,
ticket_itinerary_image: [{ image: "/burj-khalifa-inside.jpg" }],
ticket_itinerary_destinations: [
{ name: "Burj Khalifa" },
{ name: "Dubai Mall" },
{ name: "Dubai Fountain" },
],
},
{
title: "Sahro safari va beduinlar lageri",
duration: 1,
ticket_itinerary_image: [{ image: "/dubai-desert-safari.png" }],
ticket_itinerary_destinations: [
{ name: "Dubai sahro" },
{ name: "Beduinlar lageri" },
],
},
],
ticket_hotel_meals: [
{
image: "/breakfast-buffet.png",
name: "Nonushta",
desc: "Xalqaro bufet nonushtasi har kuni ertalab",
},
{
image: "/dinner-restaurant.jpg",
name: "Kechki ovqat",
desc: "Mehmonxona restoranida 3 xil menyu tanlovli kechki ovqat",
},
],
travel_agency_id: "1",
ticket_comments: [
{
user: { id: 1, username: "Aziza Karimova" },
text: "Ajoyib sayohat bo'ldi! Barcha xizmatlar yuqori darajada. Gid juda professional va mehribon edi.",
rating: 5,
},
{
user: { id: 2, username: "Sardor Rahimov" },
text: "Mehmonxona va ovqatlar juda yaxshi. Faqat transfer biroz kechikdi, lekin umuman olganda juda yoqdi.",
rating: 4,
},
{
user: { id: 3, username: "Nilufar Toshmatova" },
text: "Hayotimning eng yaxshi sayohati! Barcha narsani juda yaxshi tashkil qilishgan. Rahmat!",
rating: 5,
},
],
tariff: [{ name: "standart" }],
is_liked: "true",
});
}, [params.id]);
if (!tour) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<p className="text-gray-400">Yuklanmoqda...</p>
</div>
);
}
const renderStars = (rating: number) => {
return Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${i < rating ? "fill-yellow-400 text-yellow-400" : "text-gray-600"}`}
/>
));
};
return (
<div className="min-h-screen bg-gray-900 w-full">
<div className="container mx-auto px-4 py-8 max-w-full">
<div className="flex items-center gap-4 mb-8">
<Button
variant="outline"
size="icon"
onClick={() => router(-1)}
className="rounded-full border-gray-700 bg-gray-800 hover:bg-gray-700"
>
<ArrowLeft className="w-5 h-5 text-gray-300" />
</Button>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-4xl font-bold text-white">{tour.title}</h1>
<Button
variant="ghost"
size="icon"
className="rounded-full hover:bg-gray-800"
>
<Heart
className={`w-6 h-6 ${tour.is_liked === "true" ? "fill-red-500 text-red-500" : "text-gray-400"}`}
/>
</Button>
</div>
<div className="flex items-center gap-4 text-gray-400">
<div className="flex items-center gap-1">
{renderStars(Math.floor(tour.rating))}
<span className="ml-2 font-semibold">{tour.rating}</span>
</div>
<span></span>
<span>{tour.ticket_comments.length} sharh</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{tour.ticket_images.map((img, idx) => (
<div
key={idx}
className="relative aspect-video rounded-lg overflow-hidden group"
>
<img
src={img.image || "/placeholder.svg"}
alt={`${tour.title} ${idx + 1}`}
className="w-full h-full object-cover transition-transform group-hover:scale-110"
/>
</div>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-2">
<DollarSign className="w-5 h-5 text-green-400" />
<p className="text-sm text-gray-400">Narxi</p>
</div>
<p className="text-2xl font-bold text-white">
{tour.price.toLocaleString()} so'm
</p>
</CardContent>
</Card>
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-2">
<Clock className="w-5 h-5 text-blue-400" />
<p className="text-sm text-gray-400">Davomiyligi</p>
</div>
<p className="text-2xl font-bold text-white">
{tour.duration_days} kun
</p>
</CardContent>
</Card>
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-2">
<Users className="w-5 h-5 text-purple-400" />
<p className="text-sm text-gray-400">Yo'lovchilar</p>
</div>
<p className="text-2xl font-bold text-white">
{tour.passenger_count} kishi
</p>
</CardContent>
</Card>
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-2">
<Calendar className="w-5 h-5 text-yellow-400" />
<p className="text-sm text-gray-400">Jo'nash sanasi</p>
</div>
<p className="text-xl font-bold text-white">
{new Date(tour.departure_date).toLocaleDateString("uz-UZ")}
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="grid w-full grid-cols-2 md:grid-cols-5 lg:w-auto bg-gray-800 border-gray-700">
<TabsTrigger
value="overview"
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
>
Umumiy
</TabsTrigger>
<TabsTrigger
value="itinerary"
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
>
Marshshrut
</TabsTrigger>
<TabsTrigger
value="services"
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
>
Xizmatlar
</TabsTrigger>
<TabsTrigger
value="hotel"
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
>
Mehmonxona
</TabsTrigger>
<TabsTrigger
value="reviews"
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
>
Sharhlar
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white">
Tur haqida ma'lumot
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-green-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Jo'nash joyi</p>
<p className="font-semibold text-white">
{tour.departure}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-blue-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Yo'nalish</p>
<p className="font-semibold text-white">
{tour.destination}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Globe className="w-5 h-5 text-purple-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Tillar</p>
<p className="font-semibold text-white">
{tour.languages}
</p>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-start gap-3">
<Hotel className="w-5 h-5 text-yellow-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Mehmonxona</p>
<p className="font-semibold text-white">
{tour.hotel_info}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Utensils className="w-5 h-5 text-green-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Ovqatlanish</p>
<p className="font-semibold text-white">
{tour.hotel_meals}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-blue-400 mt-1" />
<div>
<p className="text-sm text-gray-400">Tarif</p>
<p className="font-semibold text-white capitalize">
{tour.tariff[0]?.name}
</p>
</div>
</div>
</div>
</div>
<div className="pt-6 border-t border-gray-700">
<h3 className="text-lg font-semibold mb-4 text-white">
Qulayliklar
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{tour.ticket_amenities.map((amenity, idx) => (
<div
key={idx}
className="flex flex-col items-center gap-2 p-4 bg-gray-700/50 rounded-lg"
>
<CheckCircle2 className="w-6 h-6 text-green-400" />
<p className="text-sm text-center font-medium text-white">
{amenity.name}
</p>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="itinerary" className="space-y-6">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white">
Sayohat marshshruti
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{tour.ticket_itinerary.map((day, idx) => (
<div
key={idx}
className="border-l-4 border-green-400 pl-6 pb-6 last:pb-0"
>
<div className="flex items-center gap-3 mb-3">
<Badge
variant="outline"
className="text-base border-gray-600 text-gray-300"
>
{day.duration}-kun
</Badge>
<h3 className="text-xl font-semibold text-white">
{day.title}
</h3>
</div>
{day.ticket_itinerary_image.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{day.ticket_itinerary_image.map((img, imgIdx) => (
<div
key={imgIdx}
className="relative aspect-video rounded-lg overflow-hidden"
>
<img
src={img.image || "/placeholder.svg"}
alt={day.title}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
)}
<div className="flex flex-wrap gap-2">
{day.ticket_itinerary_destinations.map(
(dest, destIdx) => (
<Badge
key={destIdx}
variant="secondary"
className="text-sm bg-gray-700 text-gray-300"
>
<MapPin className="w-3 h-3 mr-1" />
{dest.name}
</Badge>
),
)}
</div>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="services" className="space-y-6">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white">
Narxga kiritilgan xizmatlar
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{tour.ticket_included_services.map((service, idx) => (
<div
key={idx}
className="border border-gray-700 rounded-lg overflow-hidden hover:shadow-xl transition-shadow bg-gray-800"
>
<div className="relative aspect-video">
<img
src={service.image || "/placeholder.svg"}
alt={service.title}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold mb-2 text-white">
{service.title}
</h3>
<p className="text-gray-400 text-sm">{service.desc}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="hotel" className="space-y-6">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white">
Mehmonxona va ovqatlanish
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="p-6 bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-3 mb-2">
<Hotel className="w-6 h-6 text-yellow-400" />
<h3 className="text-xl font-semibold text-white">
{tour.hotel_info}
</h3>
</div>
<p className="text-gray-400">{tour.hotel_meals}</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 text-white">
Ovqatlanish tafsilotlari
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{tour.ticket_hotel_meals.map((meal, idx) => (
<div
key={idx}
className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800"
>
<div className="relative aspect-video">
<img
src={meal.image || "/placeholder.svg"}
alt={meal.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h4 className="text-lg font-semibold mb-2 text-white">
{meal.name}
</h4>
<p className="text-gray-400 text-sm">{meal.desc}</p>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reviews" className="space-y-6">
<Card className="border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-2xl text-white">
Mijozlar sharhlari
</CardTitle>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{renderStars(Math.floor(tour.rating))}
</div>
<span className="text-2xl font-bold text-white">
{tour.rating}
</span>
<span className="text-gray-400">
({tour.ticket_comments.length} sharh)
</span>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{tour.ticket_comments.map((comment, idx) => (
<div
key={idx}
className="border-b border-gray-700 pb-6 last:border-0 last:pb-0"
>
<div className="flex items-start justify-between mb-3">
<div>
<p className="font-semibold text-lg text-white">
{comment.user.username}
</p>
<div className="flex items-center gap-1 mt-1">
{renderStars(comment.rating)}
</div>
</div>
</div>
<p className="text-gray-300 leading-relaxed">
{comment.text}
</p>
</div>
))}
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Card className="border-gray-700 shadow-lg mt-8 bg-gray-800">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-400 mb-1">Tur firmasi</p>
<p className="text-xl font-semibold text-white">
Firma ID: {tour.travel_agency_id}
</p>
</div>
<Button
variant="outline"
onClick={() => router(`/agencies/${tour.travel_agency_id}`)}
className="border-gray-700 bg-gray-800 hover:bg-gray-700 text-gray-300"
>
Firma sahifasiga o'tish
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,210 @@
"use client";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Edit, Plane, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
type Tour = {
id: number;
image?: string;
tickets: string;
min_price: string;
max_price: string;
top_duration: string;
top_destinations: string;
hotel_features_by_type: string;
hotel_types: string;
hotel_amenities: string;
};
const Tours = () => {
const [tours, setTours] = useState<Tour[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(3);
const [deleteId, setDeleteId] = useState<number | null>(null);
const navigate = useNavigate();
useEffect(() => {
const mockData: Tour[] = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
image: `/dubai-marina.jpg`,
tickets: `Bilet turi ${i + 1}`,
min_price: `${200 + i * 50}$`,
max_price: `${400 + i * 70}$`,
top_duration: `${3 + i} kun`,
top_destinations: `Shahar ${i + 1}`,
hotel_features_by_type: "Spa, Wi-Fi, Pool",
hotel_types: "5 yulduzli mehmonxona",
hotel_amenities: "Nonushta, Parking, Bar",
}));
const itemsPerPage = 6;
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
setTotalPages(Math.ceil(mockData.length / itemsPerPage));
setTours(mockData.slice(start, end));
}, [page]);
const confirmDelete = () => {
if (deleteId !== null) {
setTours((prev) => prev.filter((t) => t.id !== deleteId));
setDeleteId(null);
}
};
return (
<div className="min-h-screen bg-gray-900 text-foreground py-10 px-5 w-full">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-semibold">Turlar ro'yxati</h1>
<Button onClick={() => navigate("/tours/create")} variant="default">
<PlusCircle className="w-5 h-5 mr-2" /> Yangi tur qo'shish
</Button>
</div>
<div className="rounded-xl border overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead className="min-w-[150px]">Manzil</TableHead>
<TableHead className="min-w-[120px]">Davomiyligi</TableHead>
<TableHead className="min-w-[180px]">Mehmonxona</TableHead>
<TableHead className="min-w-[200px]">Narx Oralig'i</TableHead>
<TableHead className="min-w-[200px]">Imkoniyatlar</TableHead>
<TableHead className="min-w-[150px] text-center">
Amallar
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tours.map((tour, idx) => (
<TableRow key={tour.id}>
<TableCell className="font-medium text-center">
{(page - 1) * 6 + idx + 1}
</TableCell>
<TableCell>
<div className="flex items-center gap-2 font-semibold">
<Plane className="w-4 h-4 text-primary" />
{tour.top_destinations}
</div>
</TableCell>
<TableCell className="text-sm text-primary font-medium">
{tour.top_duration}
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{tour.hotel_types}</span>
<span className="text-xs text-muted-foreground">
{tour.tickets}
</span>
</div>
</TableCell>
<TableCell>
<span className="font-bold text-base text-green-600">
{tour.min_price} {tour.max_price}
</span>
</TableCell>
<TableCell className="text-sm">
{tour.hotel_amenities}
</TableCell>
<TableCell className="text-center">
<div className="flex gap-2 justify-center">
<Button
variant="outline"
size="icon"
onClick={() => navigate(`/tours/${tour.id}/edit`)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => setDeleteId(tour.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
<Button
variant="default"
size="sm"
onClick={() => navigate(`/tours/${tour.id}`)}
>
Batafsil
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Delete Confirmation Dialog - Faqat bitta */}
<Dialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
<DialogContent className="sm:max-w-[425px] bg-gray-900">
<DialogHeader>
<DialogTitle className="text-xl">
Turni o'chirishni tasdiqlang
</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-muted-foreground">
Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga
qaytarib bo'lmaydi.
</p>
</div>
<DialogFooter className="gap-4 flex">
<Button variant="outline" onClick={() => setDeleteId(null)}>
Bekor qilish
</Button>
<Button variant="destructive" onClick={confirmDelete}>
<Trash2 className="w-4 h-4 mr-2" />
O'chirish
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="flex justify-center mt-10 gap-3">
<Button
variant="outline"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
Oldingi
</Button>
<span className="text-sm flex items-center">
Sahifa {page} / {totalPages}
</span>
<Button
variant="outline"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
Keyingi
</Button>
</div>
</div>
);
};
export default Tours;

View File

@@ -0,0 +1,461 @@
import { Button } from "@/shared/ui/button";
import { Card, CardContent } from "@/shared/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import { Textarea } from "@/shared/ui/textarea";
import { Edit2, Plus, Search, Trash2 } from "lucide-react";
import React, { useState } from "react";
interface Badge {
id: number;
name: string;
color: string;
}
interface Tariff {
id: number;
name: string;
price: number;
}
interface Transport {
id: number;
name: string;
price: number;
}
interface MealPlan {
id: number;
name: string;
}
interface HotelType {
id: number;
name: string;
}
type TabId = "badges" | "tariffs" | "transports" | "mealPlans" | "hotelTypes";
type DataItem = Badge | Tariff | Transport | MealPlan | HotelType;
interface FormField {
name: string;
label: string;
type: "text" | "number" | "color" | "textarea" | "select";
required: boolean;
options?: string[];
min?: number;
max?: number;
}
interface Tab {
id: TabId;
label: string;
}
const ToursSetting: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabId>("badges");
const [searchTerm, setSearchTerm] = useState<string>("");
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [modalMode, setModalMode] = useState<"add" | "edit">("add");
const [currentItem, setCurrentItem] = useState<DataItem | null>(null);
const [badges, setBadges] = useState<Badge[]>([
{ id: 1, name: "Bestseller", color: "#FFD700" },
{ id: 2, name: "Yangi", color: "#4CAF50" },
]);
const [tariffs, setTariffs] = useState<Tariff[]>([
{ id: 1, name: "Standart", price: 500 },
{ id: 2, name: "Premium", price: 1000 },
]);
const [transports, setTransports] = useState<Transport[]>([
{ id: 1, name: "Avtobus", price: 200 },
{ id: 2, name: "Minivan", price: 500 },
]);
const [mealPlans, setMealPlans] = useState<MealPlan[]>([
{ id: 1, name: "BB (Bed & Breakfast)" },
{ id: 2, name: "HB (Half Board)" },
{ id: 3, name: "FB (Full Board)" },
]);
const [hotelTypes, setHotelTypes] = useState<HotelType[]>([
{ id: 1, name: "3 Yulduz" },
{ id: 2, name: "5 Yulduz" },
]);
const [formData, setFormData] = useState<Partial<DataItem>>({});
const getCurrentData = (): DataItem[] => {
switch (activeTab) {
case "badges":
return badges;
case "tariffs":
return tariffs;
case "transports":
return transports;
case "mealPlans":
return mealPlans;
case "hotelTypes":
return hotelTypes;
default:
return [];
}
};
const getSetterFunction = (): React.Dispatch<
React.SetStateAction<DataItem[]>
> => {
switch (activeTab) {
case "badges":
return setBadges as React.Dispatch<React.SetStateAction<DataItem[]>>;
case "tariffs":
return setTariffs as React.Dispatch<React.SetStateAction<DataItem[]>>;
case "transports":
return setTransports as React.Dispatch<
React.SetStateAction<DataItem[]>
>;
case "mealPlans":
return setMealPlans as React.Dispatch<React.SetStateAction<DataItem[]>>;
case "hotelTypes":
return setHotelTypes as React.Dispatch<
React.SetStateAction<DataItem[]>
>;
default:
return (() => {}) as React.Dispatch<React.SetStateAction<DataItem[]>>;
}
};
const filteredData = getCurrentData().filter((item) =>
Object.values(item).some((val) =>
val?.toString().toLowerCase().includes(searchTerm.toLowerCase()),
),
);
const getFormFields = (): FormField[] => {
switch (activeTab) {
case "badges":
return [
{ name: "name", label: "Nomi", type: "text", required: true },
{ name: "color", label: "Rang", type: "color", required: true },
];
case "tariffs":
return [
{ name: "name", label: "Tarif nomi", type: "text", required: true },
{ name: "price", label: "Narx", type: "number", required: true },
];
case "transports":
return [
{
name: "name",
label: "Transport nomi",
type: "text",
required: true,
},
{ name: "capacity", label: "Sig'im", type: "number", required: true },
];
case "mealPlans":
return [{ name: "name", label: "Nomi", type: "text", required: true }];
case "hotelTypes":
return [
{ name: "name", label: "Tur nomi", type: "text", required: true },
];
default:
return [];
}
};
const openModal = (
mode: "add" | "edit",
item: DataItem | null = null,
): void => {
setModalMode(mode);
setCurrentItem(item);
if (mode === "edit" && item) {
setFormData(item);
} else {
setFormData({});
}
setIsModalOpen(true);
};
const closeModal = (): void => {
setIsModalOpen(false);
setFormData({});
setCurrentItem(null);
};
const handleSubmit = (): void => {
const setter = getSetterFunction();
if (modalMode === "add") {
const newId = Math.max(...getCurrentData().map((i) => i.id), 0) + 1;
setter([...getCurrentData(), { ...formData, id: newId } as DataItem]);
} else {
setter(
getCurrentData().map((item) =>
item.id === currentItem?.id ? { ...item, ...formData } : item,
),
);
}
closeModal();
};
const handleDelete = (id: number): void => {
if (window.confirm("Rostdan ham o'chirmoqchimisiz?")) {
const setter = getSetterFunction();
setter(getCurrentData().filter((item) => item.id !== id));
}
};
const tabs: Tab[] = [
{ id: "badges", label: "Belgilar" },
{ id: "tariffs", label: "Tariflar" },
{ id: "transports", label: "Transportlar" },
{ id: "mealPlans", label: "Ovqatlanish" },
{ id: "hotelTypes", label: "Otel turlari" },
];
const getFieldValue = (fieldName: string): string | number => {
return (formData as Record<string, string | number>)[fieldName] || "";
};
return (
<div className="min-h-screen bg-gray-900 p-6 w-full">
<div className="max-w-7xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">Tur Sozlamalari</h1>
<Tabs
value={activeTab}
onValueChange={(v) => {
setActiveTab(v as TabId);
setSearchTerm("");
}}
>
<TabsList className="grid w-full grid-cols-5">
{tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<Card className="bg-gray-900">
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center">
<div className="relative w-full sm:w-96">
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground"
size={20}
/>
<Input
type="text"
placeholder="Qidirish..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Button
onClick={() => openModal("add")}
className="w-full sm:w-auto cursor-pointer"
>
<Plus size={20} className="mr-2" />
Yangi qo'shish
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900">
<div className="overflow-x-auto">
<div className="min-w-full">
<div className="border-b">
<div className="flex">
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
ID
</div>
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
Nomi
</div>
{activeTab === "badges" && (
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-48">
Rang
</div>
)}
{(activeTab === "tariffs" || activeTab === "transports") && (
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
Narx
</div>
)}
<div className="px-6 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
Amallar
</div>
</div>
</div>
<div className="divide-y">
{filteredData.length === 0 ? (
<div className="px-6 py-8 text-center text-muted-foreground">
Ma'lumot topilmadi
</div>
) : (
filteredData.map((item) => (
<div
key={item.id}
className="flex items-center hover:bg-accent/50 transition-colors"
>
<div className="px-6 py-4 w-20">{item.id}</div>
<div className="px-6 py-4 font-medium flex-1">
{item.name}
</div>
{activeTab === "badges" && (
<div className="px-6 py-4 w-48">
<div className="flex items-center gap-2">
<div
className="w-6 h-6 rounded border"
style={{ backgroundColor: (item as Badge).color }}
/>
<span>{(item as Badge).color}</span>
</div>
</div>
)}
{(activeTab === "tariffs" ||
activeTab === "transports") && (
<div className="px-6 py-4 w-32">
{(item as Tariff).price}
</div>
)}
<div className="px-6 py-4 w-32">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => openModal("edit", item)}
>
<Edit2 size={18} />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(item.id)}
>
<Trash2 size={18} className="text-destructive" />
</Button>
</div>
</div>
</div>
))
)}
</div>
</div>
</div>
</Card>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="sm:max-w-[500px] bg-gray-900">
<DialogHeader>
<DialogTitle>
{modalMode === "add" ? "Yangi qo'shish" : "Tahrirlash"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{getFormFields().map((field) => (
<div key={field.name} className="space-y-2">
<Label htmlFor={field.name}>
{field.label}
{field.required && (
<span className="text-destructive">*</span>
)}
</Label>
{field.type === "textarea" ? (
<Textarea
id={field.name}
value={getFieldValue(field.name) as string}
onChange={(e) =>
setFormData({
...formData,
[field.name]: e.target.value,
})
}
required={field.required}
rows={3}
/>
) : field.type === "select" ? (
<Select
value={getFieldValue(field.name) as string}
onValueChange={(value) =>
setFormData({ ...formData, [field.name]: value })
}
required={field.required}
>
<SelectTrigger>
<SelectValue placeholder="Tanlang" />
</SelectTrigger>
<SelectContent>
{field.options?.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id={field.name}
type={field.type}
value={getFieldValue(field.name)}
onChange={(e) =>
setFormData({
...formData,
[field.name]:
field.type === "number"
? Number(e.target.value)
: e.target.value,
})
}
required={field.required}
min={field.min}
max={field.max}
/>
)}
</div>
))}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={closeModal}
className="flex-1"
>
Bekor qilish
</Button>
<Button type="button" onClick={handleSubmit} className="flex-1">
Saqlash
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
);
};
export default ToursSetting;