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,339 @@
import {
Building2,
ChevronLeft,
ChevronRight,
Eye,
Package,
TrendingUp,
UserIcon,
} from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
type Agency = {
id: number;
name: string;
owner: string;
status: "faol" | "nofaol";
profitPercent: number;
totalTours: number;
soldTours: number;
totalProfit: number;
};
export default function TourAgenciesPage() {
const [agencies, setAgencies] = useState<Agency[]>([
{
id: 1,
name: "Silk Road Travel",
owner: "Ali Karimov",
status: "faol",
profitPercent: 15,
totalTours: 12,
soldTours: 56,
totalProfit: 8900000,
},
{
id: 2,
name: "UzTour Plus",
owner: "Madina Qodirova",
status: "nofaol",
profitPercent: 10,
totalTours: 5,
soldTours: 0,
totalProfit: 0,
},
{
id: 3,
name: "Orient Express",
owner: "Sardor Rahimov",
status: "faol",
profitPercent: 12,
totalTours: 8,
soldTours: 34,
totalProfit: 5600000,
},
{
id: 4,
name: "Golden Voyage",
owner: "Dilnoza Azimova",
status: "faol",
profitPercent: 18,
totalTours: 15,
soldTours: 89,
totalProfit: 12400000,
},
{
id: 5,
name: "SkyLine Tours",
owner: "Rustam Qobilov",
status: "nofaol",
profitPercent: 8,
totalTours: 4,
soldTours: 0,
totalProfit: 0,
},
{
id: 6,
name: "Desert Adventures",
owner: "Kamola Saidova",
status: "faol",
profitPercent: 20,
totalTours: 11,
soldTours: 42,
totalProfit: 6700000,
},
]);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 4;
const navigate = useNavigate();
const handleStatusChange = (id: number, newStatus: "faol" | "nofaol") => {
setAgencies((prev) =>
prev.map((a) => (a.id === id ? { ...a, status: newStatus } : a)),
);
};
const totalPages = Math.ceil(agencies.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const paginatedAgencies = agencies.slice(
startIndex,
startIndex + itemsPerPage,
);
const activeCount = agencies.filter((a) => a.status === "faol").length;
const totalTours = agencies.reduce((sum, a) => sum + a.totalTours, 0);
const totalRevenue = agencies.reduce((sum, a) => sum + a.totalProfit, 0);
return (
<div className="min-h-screen w-full bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
<div className="container mx-auto px-4 py-12 max-w-[90%]">
{/* Header */}
<div className="mb-12">
<div className="flex items-center gap-3 mb-3">
<div className="p-3 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl shadow-lg shadow-cyan-500/20">
<Building2 className="w-8 h-8 text-white" />
</div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 via-cyan-300 to-blue-400 bg-clip-text text-transparent">
Tur firmalari
</h1>
</div>
<p className="text-slate-400 text-lg ml-14">
Firmalarni karta ko'rinishida boshqaring va statistikani kuzating
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-12">
<StatCard
title="Jami firmalar"
value={agencies.length.toString()}
icon={<Package className="w-6 h-6" />}
gradient="from-blue-600 to-blue-400"
shadowColor="blue"
/>
<StatCard
title="Faol firmalar"
value={activeCount.toString()}
icon={<TrendingUp className="w-6 h-6" />}
gradient="from-green-600 to-emerald-400"
shadowColor="green"
/>
<StatCard
title="Jami turlar"
value={totalTours.toString()}
icon={<Package className="w-6 h-6" />}
gradient="from-amber-600 to-yellow-400"
shadowColor="amber"
/>
<StatCard
title="Umumiy daromad"
value={`${(totalRevenue / 1_000_000).toFixed(1)}M`}
icon={<TrendingUp className="w-6 h-6" />}
gradient="from-purple-600 to-pink-400"
shadowColor="purple"
/>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-10">
{paginatedAgencies.map((agency) => (
<div
key={agency.id}
className="group relative hover:scale-105 transition-transform duration-300"
>
<div
className={`absolute inset-0 bg-gradient-to-r ${
agency.status === "faol"
? "from-blue-600/20 to-cyan-600/20"
: "from-slate-600/20 to-slate-500/20"
} rounded-2xl blur-xl group-hover:blur-2xl transition-all opacity-0 group-hover:opacity-100`}
/>
<div
className={`relative bg-gradient-to-br ${
agency.status === "faol"
? "from-slate-700 to-slate-800"
: "from-slate-800 to-slate-900"
} border border-slate-600/50 rounded-2xl p-6 shadow-2xl hover:shadow-2xl transition-all backdrop-blur-sm hover:border-slate-500/70`}
>
{/* Header */}
<div className="flex justify-between items-start mb-4">
<div>
<h2 className="text-2xl font-bold text-white mb-1">
{agency.name}
</h2>
<div className="flex gap-2 items-center">
<UserIcon className="text-slate-400 size-5" />
<p className="text-slate-400">{agency.owner}</p>
</div>
</div>
<div
className={`px-3 py-1 rounded-full text-sm font-semibold whitespace-nowrap ${
agency.status === "faol"
? "bg-green-500/20 text-green-300 border border-green-500/50"
: "bg-red-500/20 text-red-300 border border-red-500/50"
}`}
>
{agency.status === "faol" ? "Faol" : "No-faol"}
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3 mb-5">
<div className="bg-slate-600/30 rounded-lg p-3 border border-slate-500/30 hover:border-slate-500/50 transition-all">
<p className="text-slate-400 text-xs uppercase tracking-wider mb-1">
Komissiya
</p>
<p className="text-2xl font-bold text-blue-300">
{agency.profitPercent}%
</p>
</div>
<div className="bg-slate-600/30 rounded-lg p-3 border border-slate-500/30 hover:border-slate-500/50 transition-all">
<p className="text-slate-400 text-xs uppercase tracking-wider mb-1">
Jami tur
</p>
<p className="text-2xl font-bold text-cyan-300">
{agency.totalTours}
</p>
</div>
<div className="bg-slate-600/30 rounded-lg p-3 border border-slate-500/30 hover:border-slate-500/50 transition-all">
<p className="text-slate-400 text-xs uppercase tracking-wider mb-1">
Sotilgan
</p>
<p className="text-2xl font-bold text-yellow-300">
{agency.soldTours}
</p>
</div>
<div className="bg-slate-600/30 rounded-lg p-3 border border-slate-500/30 hover:border-slate-500/50 transition-all">
<p className="text-slate-400 text-xs uppercase tracking-wider mb-1">
Daromad
</p>
<p className="text-lg font-bold text-green-300">
{(agency.totalProfit / 1_000_000).toFixed(1)}M
</p>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-4 border-t border-slate-600/50">
<select
value={agency.status}
onChange={(e) =>
handleStatusChange(
agency.id,
e.target.value as "faol" | "nofaol",
)
}
className="flex-1 bg-slate-700/50 border border-slate-600 rounded-lg px-3 py-2 text-slate-300 text-sm hover:bg-slate-600/50 transition-all cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="faol" className="bg-slate-800">
Faol
</option>
<option value="nofaol" className="bg-slate-800">
No-faol
</option>
</select>
<button
onClick={() => navigate(`/agencies/${agency.id}`)}
className="flex-1 cursor-pointer bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 text-white rounded-lg px-4 py-2 font-medium transition-all flex items-center justify-center gap-2 shadow-lg hover:shadow-cyan-500/50"
>
<Eye className="w-4 h-4" />
Ko'rish
</button>
</div>
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex justify-end gap-2">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(totalPages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
currentPage === i + 1
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
}`}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((p) => Math.min(p + 1, totalPages))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
}
function StatCard({
title,
value,
icon,
gradient,
}: {
title: string;
value: string;
icon: React.ReactNode;
gradient: string;
shadowColor: string;
}) {
return (
<div className="group relative hover:scale-105 transition-transform duration-300">
<div
className={`absolute inset-0 bg-gradient-to-r ${gradient} rounded-xl blur-lg opacity-20 group-hover:opacity-40 transition-all`}
/>
<div
className={`relative bg-gradient-to-br ${gradient} bg-opacity-10 border border-white/10 backdrop-blur-sm rounded-xl p-6 shadow-xl hover:shadow-2xl transition-all hover:border-white/20`}
>
<div className="flex justify-between items-start mb-3">
<p className="text-slate-300 text-sm font-medium">{title}</p>
<div
className={`bg-gradient-to-br ${gradient} p-2 rounded-lg text-white shadow-lg`}
>
{icon}
</div>
</div>
<p className="text-3xl font-bold text-white">{value}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,240 @@
"use client";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import {
ArrowLeft,
ChevronRight,
DollarSign,
Package,
Percent,
TrendingUp,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
type Tour = {
id: number;
name: string;
description: string;
sold: number;
profit: number;
status: "faol" | "nofaol";
};
type Agency = {
id: number;
name: string;
owner: string;
status: "faol" | "nofaol";
profitPercent: number;
totalTours: number;
soldTours: number;
totalProfit: number;
tours: Tour[];
};
export default function AgencyDetailPage() {
const params = useParams();
const router = useNavigate();
const [agency, setAgency] = useState<Agency | null>(null);
useEffect(() => {
// Mock data - replace with actual API call
setAgency({
id: Number(params.id),
name: "Silk Road Travel",
owner: "Ali Karimov",
status: "faol",
profitPercent: 15,
totalTours: 12,
soldTours: 56,
totalProfit: 8900000,
tours: [
{
id: 1,
name: "Dubai Tour",
description: "7 kunlik hashamatli sayohat",
sold: 23,
profit: 3450000,
status: "faol",
},
{
id: 2,
name: "Bali Adventure",
description: "10 kunlik ekzotik sayohat",
sold: 33,
profit: 5450000,
status: "faol",
},
{
id: 3,
name: "Istanbul Express",
description: "5 kunlik madaniy sayohat",
sold: 0,
profit: 0,
status: "nofaol",
},
],
});
}, [params.id]);
if (!agency) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<p className="text-gray-400">Yuklanmoqda...</p>
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 w-full">
<div className="container mx-auto px-4 py-8 max-w-full">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Button
variant="outline"
size="icon"
onClick={() => router("/")}
className="rounded-full border-gray-700 bg-gray-800 hover:bg-gray-700"
>
<ArrowLeft className="w-5 h-5 text-gray-300" />
</Button>
<div>
<h1 className="text-4xl font-bold text-white">{agency.name}</h1>
<p className="text-gray-400 mt-1">Egasi: {agency.owner}</p>
</div>
</div>
<Badge
variant={agency.status === "faol" ? "default" : "secondary"}
className="text-base px-4 py-2"
>
{agency.status === "faol" ? "Faol" : "No-faol"}
</Badge>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Total Tours */}
<Card className="border border-gray-700 shadow-lg hover:shadow-xl transition-shadow bg-gray-800">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-400">
Jami turlar
</CardTitle>
<Package className="w-5 h-5 text-blue-400" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-white">
{agency.totalTours}
</div>
<p className="text-xs text-gray-400 mt-1">
Qo'shilgan turlar soni
</p>
</CardContent>
</Card>
{/* Sold Tours */}
<Card className="border border-gray-700 shadow-lg hover:shadow-xl transition-shadow bg-gray-800">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-400">
Sotilgan turlar
</CardTitle>
<TrendingUp className="w-5 h-5 text-green-400" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-white">
{agency.soldTours}
</div>
<p className="text-xs text-gray-400 mt-1">Jami sotilgan turlar</p>
</CardContent>
</Card>
{/* Profit Percent */}
<Card className="border border-gray-700 shadow-lg hover:shadow-xl transition-shadow bg-gray-800">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-400">
Ulush foizi
</CardTitle>
<Percent className="w-5 h-5 text-purple-400" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-white">
{agency.profitPercent}%
</div>
<p className="text-xs text-gray-400 mt-1">Har bir sotuvdan</p>
</CardContent>
</Card>
{/* Total Profit */}
<Card className="border border-gray-700 shadow-lg hover:shadow-xl transition-shadow bg-gray-800">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-400">
Umumiy daromad
</CardTitle>
<DollarSign className="w-5 h-5 text-yellow-400" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-white">
{(agency.totalProfit / 1000).toLocaleString()}
</div>
<p className="text-xs text-gray-400 mt-1">so'm daromad</p>
</CardContent>
</Card>
</div>
{/* Tours List */}
<Card className="border border-gray-700 shadow-lg bg-gray-800">
<CardHeader>
<CardTitle className="text-2xl text-white">
Qo'shilgan turlar
</CardTitle>
<p className="text-gray-400">
Firma tomonidan qo'shilgan barcha turlar ro'yxati
</p>
</CardHeader>
<CardContent>
<div className="space-y-3">
{agency.tours.map((tour) => (
<Link key={tour.id} to={`/tours/${tour.id}`} className="block">
<div className="p-5 border border-gray-700 rounded-xl hover:bg-gray-700 transition-all cursor-pointer group bg-gray-800">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white group-hover:text-blue-400 transition-colors">
{tour.name}
</h3>
</div>
<p className="text-sm text-gray-400 mb-3">
{tour.description}
</p>
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<Package className="w-4 h-4 text-gray-400" />
<span className="text-gray-400">Sotilgan:</span>
<span className="font-semibold text-white">
{tour.sold} ta
</span>
</div>
<div className="flex items-center gap-2">
<DollarSign className="w-4 h-4 text-gray-400" />
<span className="text-gray-400">Daromad:</span>
<span className="font-semibold text-yellow-400">
{(tour.profit / 1000).toLocaleString()} so'm
</span>
</div>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-blue-400 transition-colors" />
</div>
</div>
</Link>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,160 @@
import { Eye } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
type Booking = {
id: number;
userName: string;
tourName: string;
agentName: string;
destination: string;
totalAmount: number;
paidAmount: number;
status: "Paid" | "Partial" | "Pending";
};
const initialBookings: Booking[] = [
{
id: 1,
userName: "Alijon Saidov",
tourName: "Ichan Qala - Xiva",
agentName: "Xiva Tours",
destination: "Xiva",
totalAmount: 1200,
paidAmount: 1200,
status: "Paid",
},
{
id: 2,
userName: "Shahnoza Karimova",
tourName: "Samarqandning Qadimiy Go'zalligi",
agentName: "Samarqand Travel",
destination: "Samarqand",
totalAmount: 1500,
paidAmount: 800,
status: "Partial",
},
{
id: 3,
userName: "Javlon Tursunov",
tourName: "Tog'li Chimyon Sayohati",
agentName: "Toshkent Explorer",
destination: "Toshkent V.",
totalAmount: 1000,
paidAmount: 0,
status: "Pending",
},
];
const getStatusColor = (status: string) => {
switch (status) {
case "Paid":
return "bg-gray-600 text-green-500 border-gray-500";
case "Partial":
return "bg-gray-600 text-yellow-500 border-gray-600";
case "Pending":
return "bg-gray-600 text-red-500 border-gray-600";
default:
return "bg-gray-600 text-gray-100 border-gray-500";
}
};
const BookingsPanel = () => {
const [bookings, setBookings] = useState<Booking[]>(initialBookings);
const handleStatusChange = (id: number, newStatus: Booking["status"]) => {
setBookings((prev) =>
prev.map((b) => (b.id === id ? { ...b, status: newStatus } : b)),
);
};
return (
<div className="min-h-screen bg-gray-900 p-4 sm:p-8 font-[Inter] text-gray-100 w-full">
<div className="max-w-[90%] mx-auto">
<h1 className="text-3xl sm:text-4xl font-extrabold text-blue-400 mb-6">
Bronlar Paneli
</h1>
<div className="bg-gray-800 shadow-2xl rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-700">
<thead className="bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
Tour (Agent)
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
Destination
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
Total / Paid
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-300 uppercase tracking-wider">
Ko'rish
</th>
</tr>
</thead>
<tbody className="bg-gray-800 divide-y divide-gray-700">
{bookings.map((booking) => (
<tr
key={booking.id}
className="hover:bg-gray-700 transition duration-150 ease-in-out"
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">
{booking.userName}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
<span className="font-semibold">{booking.tourName}</span>{" "}
<span className="text-gray-400">
({booking.agentName})
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
{booking.destination}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
${booking.paidAmount} / ${booking.totalAmount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<select
className={`px-3 py-1 text-xs font-semibold rounded-full border ${getStatusColor(
booking.status,
)}`}
value={booking.status}
onChange={(e) =>
handleStatusChange(
booking.id,
e.target.value as Booking["status"],
)
}
>
<option value="Paid">Paid</option>
<option value="Partial">Partial</option>
<option value="Pending">Pending</option>
</select>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<Link to={`/bookings/${booking.id}`}>
<button className="bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-1 transition-colors">
<Eye className="w-4 h-4" /> Details
</button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export default BookingsPanel;

View File

@@ -0,0 +1,360 @@
import formatPhone from "@/shared/lib/formatPhone";
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 { Edit, Phone, Plus, Trash2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
const roles = ["Operator", "Bugalter", "Manager"] as const;
type Employee = {
id: number;
firstname: string;
lastname: string;
phone: string;
role: "Operator" | "Bugalter" | "Manager";
};
const initialEmployees: Employee[] = [
{
id: 1,
firstname: "Alisher",
lastname: "Karimov",
phone: "+998901234567",
role: "Operator",
},
{
id: 2,
firstname: "Nigora",
lastname: "Rahimova",
phone: "+998912345678",
role: "Bugalter",
},
{
id: 3,
firstname: "Nigora",
lastname: "Rahimova",
phone: "+998912345678",
role: "Manager",
},
];
const employeeSchema = z.object({
firstname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"),
lastname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"),
phone: z.string().min(9, "Telefon raqam noto'g'ri"),
role: z.enum(roles),
});
type EmployeeFormValues = z.infer<typeof employeeSchema>;
const EmployeesManagement = () => {
const form = useForm<EmployeeFormValues>({
resolver: zodResolver(employeeSchema),
defaultValues: {
firstname: "",
lastname: "",
phone: "+998",
role: "Bugalter",
},
});
const [employees, setEmployees] = useState<Employee[]>(initialEmployees);
const [currentPage, setCurrentPage] = useState(1);
const [showModal, setShowModal] = useState(false);
const [modalMode, setModalMode] = useState("add");
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(
null,
);
useEffect(() => {
if (selectedEmployee) {
form.setValue("firstname", selectedEmployee.firstname);
form.setValue("lastname", selectedEmployee.lastname);
form.setValue("phone", selectedEmployee.phone);
form.setValue("role", selectedEmployee.role);
}
}, [selectedEmployee, form]);
const itemsPerPage = 6;
const totalPages = Math.ceil(employees.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const currentEmployees = employees.slice(
startIndex,
startIndex + itemsPerPage,
);
const handleAdd = () => {
setModalMode("add");
setShowModal(true);
};
const handleEdit = (employee: Employee) => {
setSelectedEmployee(employee);
setShowModal(true);
setModalMode("edit");
};
const handleDelete = (id: number) => {
if (window.confirm("Ushbu xodimni o'chirishni xohlaysizmi?")) {
setEmployees(employees.filter((emp) => emp.id !== id));
}
};
const onSubmit = (values: EmployeeFormValues) => {
if (modalMode === "add") {
const newEmp: Employee = { ...values, id: Date.now() };
setEmployees([...employees, newEmp]);
} else if (selectedEmployee) {
setEmployees(
employees.map((emp) =>
emp.id === selectedEmployee.id ? { ...emp, ...values } : emp,
),
);
}
setShowModal(false);
};
return (
<div className="min-h-screen w-full bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 text-gray-100 p-6">
<div className="w-[90%] mx-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Xodimlar
</h1>
<p className="text-gray-400 mt-2">
Jami {employees.length} ta xodim
</p>
</div>
<button
onClick={handleAdd}
className="flex cursor-pointer items-center gap-2 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white px-6 py-3 rounded-lg font-semibold transition-all shadow-lg hover:shadow-xl transform hover:scale-105"
>
<Plus size={20} />
Xodim qo'shish
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{currentEmployees.map((employee) => (
<div
key={employee.id}
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700 hover:border-blue-500/50 transition-all shadow-lg hover:shadow-2xl transform hover:-translate-y-1"
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-4">
<div>
<h3 className="text-xl font-bold text-white">
{employee.firstname} {employee.lastname}
</h3>
<p className="text-blue-400 text-sm">{employee.role}</p>
</div>
</div>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center gap-2 text-gray-300">
<Phone size={16} className="text-gray-500" />
<span className="text-sm">{formatPhone(employee.phone)}</span>
</div>
</div>
<div className="flex gap-2 pt-4 border-t border-gray-700">
<button
onClick={() => handleEdit(employee)}
className="flex-1 cursor-pointer flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-all font-medium"
>
<Edit size={16} />
Tahrirlash
</button>
<button
onClick={() => handleDelete(employee.id)}
className="flex-1 cursor-pointer flex items-center justify-center gap-2 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-all font-medium"
>
<Trash2 size={16} />
O'chirish
</button>
</div>
</div>
))}
</div>
<div className="flex justify-end items-center gap-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800/50 disabled:cursor-not-allowed text-white rounded-lg transition-all border border-gray-700"
>
Oldingi
</button>
{[...Array(totalPages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg transition-all border ${
currentPage === i + 1
? "bg-gradient-to-r from-blue-500 to-purple-600 text-white border-transparent"
: "bg-gray-800 hover:bg-gray-700 text-white border-gray-700"
}`}
>
{i + 1}
</button>
))}
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800/50 disabled:cursor-not-allowed text-white rounded-lg transition-all border border-gray-700"
>
Keyingi
</button>
</div>
</div>
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-gray-800 rounded-2xl p-8 max-w-md w-full border border-gray-700 shadow-2xl">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-white">
{modalMode === "add" ? "Xodim qo'shish" : "Xodimni tahrirlash"}
</h2>
<button
onClick={() => setShowModal(false)}
className="text-gray-400 hover:text-white transition-colors"
>
<X size={24} />
</button>
</div>
<div className="space-y-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="firstname"
render={({ field }) => (
<FormItem>
<Label>First Name</Label>
<FormControl>
<Input
placeholder="First Name"
{...field}
className="h-12"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastname"
render={({ field }) => (
<FormItem>
<Label>Last Name</Label>
<FormControl>
<Input
placeholder="Last Name"
{...field}
className="h-12"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<Label>Phone</Label>
<FormControl>
<Input
placeholder="+998 90 123 45 67"
{...field}
value={formatPhone(field.value)}
className="h-12"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="w-full">
<Label>Role</Label>
<Select
value={field.value}
onValueChange={(val) => field.onChange(val)}
>
<SelectTrigger className="w-full !h-12 cursor-pointer">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent className="cursor-pointer">
{roles.map((r) => (
<SelectItem
key={r}
value={r}
className="cursor-pointer"
>
{r}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-3 pt-4">
<button
type="submit"
className="flex-1 cursor-pointer bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white px-6 py-3 rounded-lg font-semibold transition-all shadow-lg"
>
{modalMode === "add" ? "Qo'shish" : "Saqlash"}
</button>
<button
onClick={() => {
setShowModal(false);
form.reset();
}}
className="flex-1 bg-gray-700 cursor-pointer hover:bg-gray-600 text-white px-6 py-3 rounded-lg font-semibold transition-all"
>
Bekor qilish
</button>
</div>
</form>
</Form>
</div>
</div>
</div>
)}
</div>
);
};
export default EmployeesManagement;

346
src/pages/faq/ui/Faq.tsx Normal file
View File

@@ -0,0 +1,346 @@
"use client";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
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,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
type FaqType = {
id: number;
category: string;
question: string;
answer: string;
};
const categories = [
{ value: "umumiy", label: "Umumiy" },
{ value: "tolov", label: "Tolov" },
{ value: "hujjatlar", label: "Hujjatlar" },
{ value: "sugurta", label: "Sugurta" },
];
const initialFaqs: FaqType[] = [
{
id: 1,
category: "umumiy",
question: "Sayohatni bron qilish uchun qanday tolov usullari mavjud?",
answer:
"Biz kredit karta, Payme, Click, va naqd tolovni qabul qilamiz. Tolovlar xavfsiz va ishonchli tizim orqali amalga oshiriladi.",
},
{
id: 2,
category: "umumiy",
question: "Sayohatni bekor qilsam, pul qaytariladimi?",
answer:
"Ha, ammo bu bron qilingan turdagi sayohatga bogliq. Bazi sayohatlar uchun 24 soat oldin bekor qilsangiz, toliq qaytariladi.",
},
{
id: 3,
category: "hujjatlar",
question: "Pasport muddati tugasa sayohat qilish mumkinmi?",
answer:
"Yoq, pasport muddati kamida 6 oy amal qilishi kerak. Aks holda, mamlakatga kirish rad etiladi.",
},
{
id: 4,
category: "sugurta",
question: "Sayohat davomida sugurta kerakmi?",
answer:
"Ha, biz barcha mijozlarga sayohat sugurtasini tavsiya qilamiz. Bu favqulodda holatlarda yordam beradi.",
},
{
id: 5,
category: "tolov",
question: "Tolovni bosqichma-bosqich amalga oshirish mumkinmi?",
answer: "Ha, ayrim yonalishlar uchun bosqichli tolov mavjud.",
},
];
const faqForm = z.object({
categories: z.string().min(1, { message: "Majburiy maydon" }),
title: z.string().min(1, { message: "Majburiy maydon" }),
answer: z.string().min(1, { message: "Majburiy maydon" }),
});
const Faq = () => {
const [faqs, setFaqs] = useState<FaqType[]>(initialFaqs);
const [activeTab, setActiveTab] = useState("umumiy");
const [openModal, setOpenModal] = useState(false);
const [editFaq, setEditFaq] = useState<FaqType | null>(null);
const [deleteId, setDeleteId] = useState<number | null>(null);
const filteredFaqs = faqs.filter((faq) => faq.category === activeTab);
const form = useForm<z.infer<typeof faqForm>>({
resolver: zodResolver(faqForm),
defaultValues: {
answer: "",
categories: "",
title: "",
},
});
function onSubmit(value: z.infer<typeof faqForm>) {
console.log(value);
}
const handleEdit = (faq: FaqType) => {
setEditFaq(faq);
setOpenModal(true);
form.setValue("answer", faq.answer);
form.setValue("title", faq.question);
form.setValue("categories", faq.category);
};
const handleDelete = () => {
if (deleteId) {
setFaqs((prev) => prev.filter((faq) => faq.id !== deleteId));
setDeleteId(null);
}
};
useEffect(() => {
if (!openModal) {
form.reset();
setEditFaq(null);
}
}, [openModal, form]);
return (
<div className="p-6 space-y-6 w-full">
{/* Header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">FAQ (Savol va javoblar)</h1>
<Button
className="gap-2"
onClick={() => {
setEditFaq(null);
setOpenModal(true);
}}
>
<PlusCircle className="w-4 h-4" /> Yangi qoshish
</Button>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-2">
{categories.map((cat) => (
<TabsTrigger key={cat.value} value={cat.value}>
{cat.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value={activeTab} className="mt-4">
{filteredFaqs.length > 0 ? (
<div className="border rounded-md overflow-hidden shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead>Savol</TableHead>
<TableHead>Javob</TableHead>
<TableHead className="w-[120px] text-center">
Amallar
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredFaqs.map((faq, index) => (
<TableRow key={faq.id}>
<TableCell className="text-center font-medium">
{index + 1}
</TableCell>
<TableCell className="font-medium">
{faq.question}
</TableCell>
<TableCell className="text-foreground">
{faq.answer.length > 80
? faq.answer.slice(0, 80) + "..."
: faq.answer}
</TableCell>
<TableCell className="flex justify-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => handleEdit(faq)}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => setDeleteId(faq.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<p className="text-gray-500 text-sm mt-4">
Bu bolimda savollar yoq.
</p>
)}
</TabsContent>
</Tabs>
<Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="sm:max-w-[500px] bg-gray-900">
<DialogHeader>
<DialogTitle>
{editFaq ? "FAQni tahrirlash" : "Yangi FAQ qoshish"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="categories"
render={({ field }) => (
<FormItem>
<Label className="text-md">Kategoriya</Label>
<FormControl>
<Select
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger className="w-full !h-12 border-gray-700 text-white">
<SelectValue placeholder="Kategoriya tanlang" />
</SelectTrigger>
<SelectContent className="border-gray-700 text-white">
<SelectGroup>
<SelectLabel>Kategoriyalar</SelectLabel>
{categories.map((cat) => (
<SelectItem key={cat.value} value={cat.value}>
{cat.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<Label className="text-md">Savol</Label>
<FormControl>
<Input
placeholder="Savol"
{...field}
className="h-12 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="answer"
render={({ field }) => (
<FormItem>
<Label className="text-md">Javob</Label>
<FormControl>
<Textarea
placeholder="Javob"
{...field}
className="min-h-48 max-h-56 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-between">
<Button
type="button"
onClick={() => {
setOpenModal(false);
form.reset();
}}
className="bg-gray-600 px-5 py-5 hover:bg-gray-700 text-white mt-4 cursor-pointer"
>
Bekor qilish
</Button>
<Button
type="submit"
className="bg-blue-600 px-5 py-5 hover:bg-blue-700 text-white mt-4 cursor-pointer"
>
{/* {isEditMode ? "Yangilikni saqlash" : "Keyingisi"} */}
Qo'shish
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
<Dialog open={!!deleteId} onOpenChange={() => setDeleteId(null)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Haqiqatan ham ochirmoqchimisiz?</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteId(null)}>
Bekor qilish
</Button>
<Button variant="destructive" onClick={handleDelete}>
Ochirish
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default Faq;

View File

@@ -0,0 +1,232 @@
"use client";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
type FaqCategoryType = {
id: number;
name: string;
faqCount: number;
};
// fakeData: kategoriya + savol soni
const initialCategories: FaqCategoryType[] = [
{ id: 1, name: "umumiy", faqCount: 8 },
{ id: 2, name: "tolov", faqCount: 5 },
{ id: 3, name: "hujjatlar", faqCount: 6 },
{ id: 4, name: "sugurta", faqCount: 3 },
];
const categoryFormSchema = z.object({
name: z.string().min(1, { message: "Kategoriya nomi majburiy" }),
});
const FaqCategory = () => {
const [categories, setCategories] =
useState<FaqCategoryType[]>(initialCategories);
const [openModal, setOpenModal] = useState(false);
const [editCategory, setEditCategory] = useState<FaqCategoryType | null>(
null,
);
const [deleteId, setDeleteId] = useState<number | null>(null);
const form = useForm<z.infer<typeof categoryFormSchema>>({
resolver: zodResolver(categoryFormSchema),
defaultValues: { name: "" },
});
const onSubmit = (values: z.infer<typeof categoryFormSchema>) => {
if (editCategory) {
setCategories((prev) =>
prev.map((cat) =>
cat.id === editCategory.id ? { ...cat, name: values.name } : cat,
),
);
} else {
const newCategory = {
id: Date.now(),
name: values.name,
faqCount: 0, // yangi kategoriya bolsa 0 ta savol
};
setCategories((prev) => [...prev, newCategory]);
}
setOpenModal(false);
};
const handleEdit = (cat: FaqCategoryType) => {
setEditCategory(cat);
form.setValue("name", cat.name);
setOpenModal(true);
};
const handleDelete = () => {
if (deleteId) {
setCategories((prev) => prev.filter((cat) => cat.id !== deleteId));
setDeleteId(null);
}
};
useEffect(() => {
if (!openModal) {
form.reset();
setEditCategory(null);
}
}, [openModal, form]);
return (
<div className="p-6 space-y-6 w-full">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">FAQ Kategoriyalar</h1>
<Button
className="gap-2"
onClick={() => {
setEditCategory(null);
setOpenModal(true);
}}
>
<PlusCircle className="w-4 h-4" /> Yangi kategoriya
</Button>
</div>
{/* Jadval */}
<div className="border rounded-md overflow-hidden shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead>Kategoriya nomi</TableHead>
<TableHead className="text-center">Savollar soni</TableHead>
<TableHead className="w-[120px] text-center">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.map((cat, index) => (
<TableRow key={cat.id}>
<TableCell className="text-center font-medium">
{index + 1}
</TableCell>
<TableCell className="capitalize font-medium">
{cat.name}
</TableCell>
<TableCell className="text-center">{cat.faqCount}</TableCell>
<TableCell className="flex justify-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => handleEdit(cat)}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => setDeleteId(cat.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Modal */}
<Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="sm:max-w-[400px] bg-gray-900">
<DialogHeader>
<DialogTitle>
{editCategory ? "Kategoriyani tahrirlash" : "Yangi kategoriya"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<Label className="text-md">Kategoriya nomi</Label>
<FormControl>
<Input
placeholder="Masalan: umumiy"
{...field}
className="h-12 bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="flex justify-end gap-3">
<Button
type="button"
onClick={() => setOpenModal(false)}
className="bg-gray-600 hover:bg-gray-700 text-white"
>
Bekor qilish
</Button>
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{editCategory ? "Saqlash" : "Qoshish"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Ochirish tasdigi */}
<Dialog open={!!deleteId} onOpenChange={() => setDeleteId(null)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Haqiqatan ham ochirmoqchimisiz?</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteId(null)}>
Bekor qilish
</Button>
<Button variant="destructive" onClick={handleDelete}>
Ochirish
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default FaqCategory;

View File

@@ -0,0 +1,431 @@
"use client";
import {
CreditCard,
DollarSign,
Eye,
Hotel,
MapPin,
Plane,
TrendingUp,
Users,
} from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
type Purchase = {
id: number;
userName: string;
userPhone: string;
tourName: string;
tourId: number;
agencyName: string;
agencyId: number;
destination: string;
travelDate: string;
amount: number;
paymentStatus: "paid" | "pending" | "cancelled" | "refunded";
purchaseDate: string;
};
const mockPurchases: Purchase[] = [
{
id: 1,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-10",
amount: 1500000,
paymentStatus: "paid",
purchaseDate: "2025-10-10",
},
{
id: 2,
userName: "Sardor Rahimov",
userPhone: "+998 91 234 56 78",
tourName: "Bali Adventure Package",
tourId: 2,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Bali, Indonesia",
travelDate: "2025-11-15",
amount: 1800000,
paymentStatus: "paid",
purchaseDate: "2025-10-12",
},
{
id: 3,
userName: "Nilufar Toshmatova",
userPhone: "+998 93 345 67 89",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-20",
amount: 1500000,
paymentStatus: "pending",
purchaseDate: "2025-10-14",
},
{
id: 4,
userName: "Jamshid Alimov",
userPhone: "+998 94 456 78 90",
tourName: "Istanbul Express Tour",
tourId: 3,
agencyName: "Orient Express",
agencyId: 3,
destination: "Istanbul, Turkey",
travelDate: "2025-11-05",
amount: 1200000,
paymentStatus: "cancelled",
purchaseDate: "2025-10-08",
},
{
id: 5,
userName: "Madina Yusupova",
userPhone: "+998 97 567 89 01",
tourName: "Paris Romantic Getaway",
tourId: 4,
agencyName: "Euro Travels",
agencyId: 2,
destination: "Paris, France",
travelDate: "2025-12-01",
amount: 2200000,
paymentStatus: "paid",
purchaseDate: "2025-10-16",
},
];
export default function FinancePage() {
const [tab, setTab] = useState<"bookings" | "agencies">("bookings");
const [filterStatus, setFilterStatus] = useState<
"all" | "paid" | "pending" | "cancelled" | "refunded"
>("all");
const getStatusBadge = (status: Purchase["paymentStatus"]) => {
const base =
"px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2";
switch (status) {
case "paid":
return (
<span
className={`${base} bg-green-900 text-green-400 border border-green-700`}
>
<div className="w-2 h-2 rounded-full bg-green-400"></div>
Paid
</span>
);
case "pending":
return (
<span
className={`${base} bg-yellow-900 text-yellow-400 border border-yellow-700`}
>
<div className="w-2 h-2 rounded-full bg-yellow-400"></div>
Pending
</span>
);
case "cancelled":
return (
<span
className={`${base} bg-red-900 text-red-400 border border-red-700`}
>
<div className="w-2 h-2 rounded-full bg-red-400"></div>
Cancelled
</span>
);
case "refunded":
return (
<span
className={`${base} bg-blue-900 text-blue-400 border border-blue-700`}
>
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
Refunded
</span>
);
}
};
const filteredPurchases =
filterStatus === "all"
? mockPurchases
: mockPurchases.filter((p) => p.paymentStatus === filterStatus);
const totalRevenue = filteredPurchases
.filter((p) => p.paymentStatus === "paid")
.reduce((sum, p) => sum + p.amount, 0);
const pendingRevenue = filteredPurchases
.filter((p) => p.paymentStatus === "pending")
.reduce((sum, p) => sum + p.amount, 0);
const agencies = Array.from(
new Set(mockPurchases.map((p) => p.agencyId)),
).map((id) => {
const agencyPurchases = mockPurchases.filter((p) => p.agencyId === id);
return {
id,
name: agencyPurchases[0].agencyName,
totalPaid: agencyPurchases
.filter((p) => p.paymentStatus === "paid")
.reduce((sum, p) => sum + p.amount, 0),
pending: agencyPurchases
.filter((p) => p.paymentStatus === "pending")
.reduce((sum, p) => sum + p.amount, 0),
purchaseCount: agencyPurchases.length,
destinations: Array.from(
new Set(agencyPurchases.map((p) => p.destination)),
),
};
});
return (
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
<div className="w-[90%] mx-auto py-6">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold">Travel Finance Dashboard</h1>
<p className="text-gray-400 mt-2">
Manage bookings, payments, and agency finances
</p>
</div>
<div className="bg-gray-800 rounded-lg p-3 flex items-center">
<Plane className="text-blue-400 mr-2" size={20} />
<span className="font-medium text-gray-100">Travel Pro</span>
</div>
</div>
{/* Tab Toggle */}
<div className="flex gap-2 mb-8 bg-gray-800 rounded-xl p-1 w-fit">
<button
className={`px-6 py-3 rounded-lg flex items-center gap-2 transition-all ${
tab === "bookings"
? "bg-blue-600 text-white shadow-md"
: "text-gray-400 hover:bg-gray-700"
}`}
onClick={() => setTab("bookings")}
>
<CreditCard size={18} />
Bookings & Payments
</button>
<button
className={`px-6 py-3 rounded-lg flex items-center gap-2 transition-all ${
tab === "agencies"
? "bg-blue-600 text-white shadow-md"
: "text-gray-400 hover:bg-gray-700"
}`}
onClick={() => setTab("agencies")}
>
<Users size={18} />
Agency Reports
</button>
</div>
{tab === "bookings" && (
<>
{/* Filter */}
<div className="flex gap-2 mb-6 flex-wrap">
{["all", "paid", "pending", "cancelled", "refunded"].map((s) => (
<button
key={s}
className={`px-4 py-2 rounded-lg transition-all ${
filterStatus === s
? "bg-blue-600 text-white shadow-md"
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
}`}
onClick={() =>
setFilterStatus(
s as
| "all"
| "paid"
| "pending"
| "cancelled"
| "refunded",
)
}
>
{s === "all"
? "All Bookings"
: s === "paid"
? "Paid"
: s === "pending"
? "Pending"
: s === "cancelled"
? "Cancelled"
: "Refunded"}
</button>
))}
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Total Revenue</p>
<DollarSign className="text-green-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-green-400 mt-3">
${(totalRevenue / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">
From completed bookings
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Pending Payments</p>
<TrendingUp className="text-yellow-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-yellow-400 mt-3">
${(pendingRevenue / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">
Awaiting confirmation
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">
Confirmed Bookings
</p>
<CreditCard className="text-blue-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-blue-400 mt-3">
{
filteredPurchases.filter((p) => p.paymentStatus === "paid")
.length
}
</p>
<p className="text-sm text-gray-500 mt-1">Paid and confirmed</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Pending Bookings</p>
<Hotel className="text-purple-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-purple-400 mt-3">
{
filteredPurchases.filter(
(p) => p.paymentStatus === "pending",
).length
}
</p>
<p className="text-sm text-gray-500 mt-1">Awaiting payment</p>
</div>
</div>
{/* Booking Cards */}
<h2 className="text-xl font-bold mb-4">Recent Bookings</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPurchases.map((p) => (
<div
key={p.id}
className="bg-gray-800 p-5 rounded-xl shadow hover:shadow-md transition-all flex flex-col justify-between"
>
<div>
<div className="flex justify-between items-start mb-3">
<h2 className="text-lg font-bold text-gray-100">
{p.userName}
</h2>
</div>
<p className="text-gray-400 text-sm">{p.userPhone}</p>
<p className="mt-3 font-semibold text-gray-200">
{p.tourName}
</p>
<div className="flex items-center gap-1 mt-1 text-gray-400">
<MapPin size={14} />
<p className="text-sm">{p.destination}</p>
</div>
<div className="flex justify-between mt-3">
<div>
<p className="text-gray-500 text-sm">Travel Date</p>
<p className="text-gray-100 font-medium">
{p.travelDate}
</p>
</div>
<div className="text-right">
<p className="text-gray-500 text-sm">Amount</p>
<p className="text-green-400 font-bold">
${(p.amount / 1000000).toFixed(1)}M
</p>
</div>
</div>
</div>
<div className="mt-4 flex justify-between items-center">
{getStatusBadge(p.paymentStatus)}
<Link to={`/bookings/${p.id}`}>
<button className="bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-1 transition-colors">
<Eye className="w-4 h-4" /> Details
</button>
</Link>
</div>
</div>
))}
</div>
</>
)}
{tab === "agencies" && (
<>
<h2 className="text-xl font-bold mb-6">Partner Agencies</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{agencies.map((a) => (
<div
key={a.id}
className="bg-gray-800 p-6 rounded-xl shadow hover:shadow-md transition-all"
>
<h2 className="text-xl font-bold mb-3 flex items-center gap-2 text-gray-100">
<Users className="text-blue-400" size={20} />
{a.name}
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="bg-green-900 p-3 rounded-lg">
<p className="text-gray-400 text-sm">Paid</p>
<p className="text-green-400 font-bold text-lg">
${(a.totalPaid / 1000000).toFixed(1)}M
</p>
</div>
<div className="bg-yellow-900 p-3 rounded-lg">
<p className="text-gray-400 text-sm">Pending</p>
<p className="text-yellow-400 font-bold text-lg">
${(a.pending / 1000000).toFixed(1)}M
</p>
</div>
</div>
<div className="mb-4 text-gray-400">
<p className="text-sm mb-1">
Bookings:{" "}
<span className="font-medium text-gray-100">
{a.purchaseCount}
</span>
</p>
<p className="text-sm">
Destinations:{" "}
<span className="font-medium text-gray-100">
{a.destinations.length}
</span>
</p>
</div>
<div className="flex gap-2">
<Link
to={`/travel/booking/${a.id}`}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-1 transition-colors flex-1 justify-center"
>
<Eye className="w-4 h-4" /> View Details
</Link>
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,467 @@
"use client";
import {
ArrowLeft,
Calendar,
DollarSign,
Download,
Eye,
Hotel,
MapPin,
Plane,
Share2,
Star,
TrendingUp,
Users,
} from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
type TourPurchase = {
id: number;
userName: string;
userPhone: string;
tourName: string;
tourId: number;
agencyName: string;
agencyId: number;
destination: string;
travelDate: string;
amount: number;
paymentStatus: "paid" | "pending" | "cancelled" | "refunded";
purchaseDate: string;
rating: number;
review: string;
};
const mockTourData = {
id: 1,
name: "Dubai Luxury Tour",
destination: "Dubai, UAE",
duration: "7 days",
price: 1500000,
totalBookings: 45,
totalRevenue: 67500000,
averageRating: 4.8,
agency: "Silk Road Travel",
description:
"Experience the ultimate luxury in Dubai with 5-star accommodations, private tours, and exclusive experiences.",
inclusions: [
"5-star hotel accommodation",
"Private city tours",
"Desert safari experience",
"Burj Khalifa tickets",
"Airport transfers",
],
};
const mockTourPurchases: TourPurchase[] = [
{
id: 1,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-10",
amount: 1500000,
paymentStatus: "paid",
purchaseDate: "2025-10-10",
rating: 5,
review:
"Amazing experience! The hotel was luxurious and the tours were well organized.",
},
{
id: 2,
userName: "Sardor Rahimov",
userPhone: "+998 91 234 56 78",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-15",
amount: 1500000,
paymentStatus: "paid",
purchaseDate: "2025-10-12",
rating: 4,
review:
"Great tour overall. The desert safari was the highlight of our trip.",
},
{
id: 3,
userName: "Nilufar Toshmatova",
userPhone: "+998 93 345 67 89",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-20",
amount: 1500000,
paymentStatus: "pending",
purchaseDate: "2025-10-14",
rating: 0,
review: "",
},
];
export default function FinanceDetailTour() {
const [activeTab, setActiveTab] = useState<
"overview" | "bookings" | "reviews"
>("overview");
const getStatusBadge = (status: TourPurchase["paymentStatus"]) => {
const base =
"px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2";
switch (status) {
case "paid":
return (
<span
className={`${base} bg-green-900 text-green-400 border border-green-700`}
>
<div className="w-2 h-2 rounded-full bg-green-400"></div>
Paid
</span>
);
case "pending":
return (
<span
className={`${base} bg-yellow-900 text-yellow-400 border border-yellow-700`}
>
<div className="w-2 h-2 rounded-full bg-yellow-400"></div>
Pending
</span>
);
case "cancelled":
return (
<span
className={`${base} bg-red-900 text-red-400 border border-red-700`}
>
<div className="w-2 h-2 rounded-full bg-red-400"></div>
Cancelled
</span>
);
case "refunded":
return (
<span
className={`${base} bg-blue-900 text-blue-400 border border-blue-700`}
>
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
Refunded
</span>
);
}
};
const renderStars = (rating: number) => {
return (
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-4 h-4 ${
star <= rating
? "text-yellow-400 fill-yellow-400"
: "text-gray-600"
}`}
/>
))}
</div>
);
};
const paidBookings = mockTourPurchases.filter(
(p) => p.paymentStatus === "paid",
);
const totalRevenue = paidBookings.reduce((sum, p) => sum + p.amount, 0);
const pendingRevenue = mockTourPurchases
.filter((p) => p.paymentStatus === "pending")
.reduce((sum, p) => sum + p.amount, 0);
return (
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
<div className="w-[90%] mx-auto py-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Link
to="/finance"
className="bg-gray-800 p-2 rounded-lg hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</Link>
<div>
<h1 className="text-3xl font-bold">Tour Financial Details</h1>
<p className="text-gray-400 mt-1">
Financial performance for {mockTourData.name}
</p>
</div>
</div>
<div className="flex gap-3">
<button className="bg-gray-800 px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2">
<Download className="w-4 h-4" />
Export Report
</button>
<button className="bg-blue-600 px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2">
<Share2 className="w-4 h-4" />
Share
</button>
</div>
</div>
{/* Tour Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Total Revenue</p>
<DollarSign className="text-green-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-green-400 mt-3">
${(totalRevenue / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">
From completed bookings
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Pending Revenue</p>
<TrendingUp className="text-yellow-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-yellow-400 mt-3">
${(pendingRevenue / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">Awaiting payment</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Total Bookings</p>
<Users className="text-blue-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-blue-400 mt-3">
{mockTourPurchases.length}
</p>
<p className="text-sm text-gray-500 mt-1">All bookings</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Average Rating</p>
<Star className="text-yellow-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-yellow-400 mt-3">
{mockTourData.averageRating}/5
</p>
<p className="text-sm text-gray-500 mt-1">Customer satisfaction</p>
</div>
</div>
{/* Main Content */}
<div className="bg-gray-800 rounded-xl shadow">
{/* Tabs */}
<div className="flex border-b border-gray-700">
<button
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
activeTab === "overview"
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400 hover:text-gray-300"
}`}
onClick={() => setActiveTab("overview")}
>
<Eye className="w-4 h-4" />
Tour Overview
</button>
<button
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
activeTab === "bookings"
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400 hover:text-gray-300"
}`}
onClick={() => setActiveTab("bookings")}
>
<Users className="w-4 h-4" />
Bookings ({mockTourPurchases.length})
</button>
<button
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
activeTab === "reviews"
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400 hover:text-gray-300"
}`}
onClick={() => setActiveTab("reviews")}
>
<Star className="w-4 h-4" />
Reviews
</button>
</div>
<div className="p-6">
{activeTab === "overview" && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Tour Information */}
<div className="lg:col-span-2">
<h3 className="text-lg font-bold mb-4">Tour Information</h3>
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Plane className="w-5 h-5 text-blue-400" />
<div>
<p className="text-sm text-gray-400">Tour Name</p>
<p className="text-gray-100 font-medium">
{mockTourData.name}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<MapPin className="w-5 h-5 text-green-400" />
<div>
<p className="text-sm text-gray-400">Destination</p>
<p className="text-gray-100">
{mockTourData.destination}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Calendar className="w-5 h-5 text-purple-400" />
<div>
<p className="text-sm text-gray-400">Duration</p>
<p className="text-gray-100">{mockTourData.duration}</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Hotel className="w-5 h-5 text-yellow-400" />
<div>
<p className="text-sm text-gray-400">Agency</p>
<p className="text-gray-100">{mockTourData.agency}</p>
</div>
</div>
<div className="p-3 bg-gray-700 rounded-lg">
<p className="text-sm text-gray-400 mb-2">Description</p>
<p className="text-gray-100">
{mockTourData.description}
</p>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-bold mb-4">Tour Inclusions</h3>
<div className="mt-6 p-4 bg-gray-700 rounded-lg">
<p className="text-sm text-gray-400 mb-2">Base Price</p>
<p className="text-2xl font-bold text-green-400">
${(mockTourData.price / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-400 mt-1">per person</p>
</div>
</div>
</div>
)}
{activeTab === "bookings" && (
<div className="space-y-6">
<h2 className="text-xl font-bold mb-4">Recent Bookings</h2>
{mockTourPurchases.map((purchase) => (
<div
key={purchase.id}
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-bold text-gray-100">
{purchase.userName}
</h3>
<p className="text-gray-400 text-sm">
{purchase.userPhone}
</p>
</div>
{getStatusBadge(purchase.paymentStatus)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<p className="text-sm text-gray-400">Travel Date</p>
<p className="text-gray-100 font-medium">
{purchase.travelDate}
</p>
</div>
<div>
<p className="text-sm text-gray-400">Booking Date</p>
<p className="text-gray-100">{purchase.purchaseDate}</p>
</div>
<div>
<p className="text-sm text-gray-400">Amount</p>
<p className="text-green-400 font-bold">
${(purchase.amount / 1000000).toFixed(1)}M
</p>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-600">
<div className="text-sm text-gray-400">
Agency: {purchase.agencyName}
</div>
<Link
to={`/bookings/${purchase.id}`}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
View Details
</Link>
</div>
</div>
))}
</div>
)}
{activeTab === "reviews" && (
<div className="space-y-6">
<h2 className="text-xl font-bold mb-4">Customer Reviews</h2>
{mockTourPurchases
.filter((purchase) => purchase.rating > 0)
.map((purchase) => (
<div
key={purchase.id}
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-bold text-gray-100">
{purchase.userName}
</h3>
<p className="text-gray-400 text-sm">
{purchase.travelDate}
</p>
</div>
<div className="flex items-center gap-2">
{renderStars(purchase.rating)}
<span className="text-gray-300">
{purchase.rating}.0
</span>
</div>
</div>
<p className="text-gray-100 mb-4">{purchase.review}</p>
<div className="flex justify-between items-center pt-4 border-t border-gray-600">
<div className="text-sm text-gray-400">
Booked on {purchase.purchaseDate}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,452 @@
"use client";
import {
ArrowLeft,
Calendar,
CreditCard,
DollarSign,
Download,
Mail,
MapPin,
Phone,
Share2,
TrendingUp,
User,
} from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
type UserPurchase = {
id: number;
userName: string;
userPhone: string;
userEmail: string;
tourName: string;
tourId: number;
agencyName: string;
agencyId: number;
destination: string;
travelDate: string;
returnDate: string;
amount: number;
paymentStatus: "paid" | "pending" | "cancelled" | "refunded";
paymentMethod: "credit_card" | "paypal" | "bank_transfer" | "crypto";
purchaseDate: string;
travelers: number;
bookingReference: string;
};
const mockUserData = {
id: 1,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
userEmail: "aziza.karimova@example.com",
joinDate: "2024-01-15",
totalSpent: 4500000,
totalBookings: 3,
memberLevel: "Gold",
};
const mockUserPurchases: UserPurchase[] = [
{
id: 1,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
userEmail: "aziza.karimova@example.com",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-10",
returnDate: "2025-11-17",
amount: 1500000,
paymentStatus: "paid",
paymentMethod: "credit_card",
purchaseDate: "2025-10-10",
travelers: 2,
bookingReference: "TRV-DXB-001",
},
{
id: 2,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
userEmail: "aziza.karimova@example.com",
tourName: "Paris Romantic Getaway",
tourId: 4,
agencyName: "Euro Travels",
agencyId: 2,
destination: "Paris, France",
travelDate: "2025-12-01",
returnDate: "2025-12-08",
amount: 2200000,
paymentStatus: "paid",
paymentMethod: "paypal",
purchaseDate: "2025-10-16",
travelers: 2,
bookingReference: "TRV-PAR-002",
},
{
id: 3,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
userEmail: "aziza.karimova@example.com",
tourName: "Bali Adventure Package",
tourId: 2,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Bali, Indonesia",
travelDate: "2025-11-15",
returnDate: "2025-11-22",
amount: 1800000,
paymentStatus: "pending",
paymentMethod: "bank_transfer",
purchaseDate: "2025-10-12",
travelers: 1,
bookingReference: "TRV-BAL-003",
},
];
export default function FinanceDetailUser() {
const [activeTab, setActiveTab] = useState<"bookings" | "details">(
"bookings",
);
const getStatusBadge = (status: UserPurchase["paymentStatus"]) => {
const base =
"px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2";
switch (status) {
case "paid":
return (
<span
className={`${base} bg-green-900 text-green-400 border border-green-700`}
>
<div className="w-2 h-2 rounded-full bg-green-400"></div>
Paid
</span>
);
case "pending":
return (
<span
className={`${base} bg-yellow-900 text-yellow-400 border border-yellow-700`}
>
<div className="w-2 h-2 rounded-full bg-yellow-400"></div>
Pending
</span>
);
case "cancelled":
return (
<span
className={`${base} bg-red-900 text-red-400 border border-red-700`}
>
<div className="w-2 h-2 rounded-full bg-red-400"></div>
Cancelled
</span>
);
case "refunded":
return (
<span
className={`${base} bg-blue-900 text-blue-400 border border-blue-700`}
>
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
Refunded
</span>
);
}
};
const getPaymentMethod = (method: UserPurchase["paymentMethod"]) => {
switch (method) {
case "credit_card":
return "Credit Card";
case "paypal":
return "PayPal";
case "bank_transfer":
return "Bank Transfer";
case "crypto":
return "Cryptocurrency";
}
};
const totalSpent = mockUserPurchases
.filter((p) => p.paymentStatus === "paid")
.reduce((sum, p) => sum + p.amount, 0);
const pendingAmount = mockUserPurchases
.filter((p) => p.paymentStatus === "pending")
.reduce((sum, p) => sum + p.amount, 0);
return (
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
<div className="w-[90%] mx-auto py-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Link
to="/finance"
className="bg-gray-800 p-2 rounded-lg hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</Link>
<div>
<h1 className="text-3xl font-bold">User Financial Details</h1>
<p className="text-gray-400 mt-1">
Detailed financial overview for {mockUserData.userName}
</p>
</div>
</div>
<div className="flex gap-3">
<button className="bg-gray-800 px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2">
<Download className="w-4 h-4" />
Export
</button>
<button className="bg-blue-600 px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2">
<Share2 className="w-4 h-4" />
Share Report
</button>
</div>
</div>
{/* User Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Total Spent</p>
<DollarSign className="text-green-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-green-400 mt-3">
${(totalSpent / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">All completed bookings</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Pending Payments</p>
<TrendingUp className="text-yellow-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-yellow-400 mt-3">
${(pendingAmount / 1000000).toFixed(1)}M
</p>
<p className="text-sm text-gray-500 mt-1">Awaiting confirmation</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Total Bookings</p>
<CreditCard className="text-blue-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-blue-400 mt-3">
{mockUserData.totalBookings}
</p>
<p className="text-sm text-gray-500 mt-1">All time bookings</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">Member Level</p>
<User className="text-purple-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-purple-400 mt-3">
{mockUserData.memberLevel}
</p>
<p className="text-sm text-gray-500 mt-1">Loyalty status</p>
</div>
</div>
{/* Main Content */}
<div className="bg-gray-800 rounded-xl shadow">
{/* Tabs */}
<div className="flex border-b border-gray-700">
<button
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
activeTab === "bookings"
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400 hover:text-gray-300"
}`}
onClick={() => setActiveTab("bookings")}
>
<CreditCard className="w-4 h-4" />
Booking History
</button>
<button
className={`px-6 py-4 font-medium flex items-center gap-2 transition-colors ${
activeTab === "details"
? "text-blue-400 border-b-2 border-blue-400"
: "text-gray-400 hover:text-gray-300"
}`}
onClick={() => setActiveTab("details")}
>
<User className="w-4 h-4" />
User Details
</button>
</div>
<div className="p-6">
{activeTab === "bookings" && (
<div className="space-y-6">
<h2 className="text-xl font-bold mb-4">Booking History</h2>
{mockUserPurchases.map((purchase) => (
<div
key={purchase.id}
className="bg-gray-700 p-6 rounded-lg hover:bg-gray-600 transition-colors"
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-bold text-gray-100">
{purchase.tourName}
</h3>
<p className="text-gray-400 text-sm">
Booking Ref: {purchase.bookingReference}
</p>
</div>
{getStatusBadge(purchase.paymentStatus)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-gray-400" />
<div>
<p className="text-sm text-gray-400">Destination</p>
<p className="text-gray-100">
{purchase.destination}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
<div>
<p className="text-sm text-gray-400">Travel Dates</p>
<p className="text-gray-100">
{purchase.travelDate} - {purchase.returnDate}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<div>
<p className="text-sm text-gray-400">Travelers</p>
<p className="text-gray-100">
{purchase.travelers} person(s)
</p>
</div>
</div>
<div className="flex items-center gap-2">
<DollarSign className="w-4 h-4 text-gray-400" />
<div>
<p className="text-sm text-gray-400">Amount</p>
<p className="text-green-400 font-bold">
${(purchase.amount / 1000000).toFixed(1)}M
</p>
</div>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-600">
<div className="text-sm text-gray-400">
Booked on {purchase.purchaseDate} {" "}
{getPaymentMethod(purchase.paymentMethod)}
</div>
</div>
</div>
))}
</div>
)}
{activeTab === "details" && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Personal Information */}
<div>
<h3 className="text-lg font-bold mb-4">
Personal Information
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<User className="w-5 h-5 text-blue-400" />
<div>
<p className="text-sm text-gray-400">Full Name</p>
<p className="text-gray-100">{mockUserData.userName}</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Phone className="w-5 h-5 text-green-400" />
<div>
<p className="text-sm text-gray-400">Phone Number</p>
<p className="text-gray-100">
{mockUserData.userPhone}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Mail className="w-5 h-5 text-yellow-400" />
<div>
<p className="text-sm text-gray-400">Email Address</p>
<p className="text-gray-100">
{mockUserData.userEmail}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg">
<Calendar className="w-5 h-5 text-purple-400" />
<div>
<p className="text-sm text-gray-400">Member Since</p>
<p className="text-gray-100">{mockUserData.joinDate}</p>
</div>
</div>
</div>
</div>
{/* Travel Preferences */}
<div>
<h3 className="text-lg font-bold mb-4">Travel Statistics</h3>
<div className="space-y-4">
<div className="p-4 bg-gray-700 rounded-lg">
<p className="text-sm text-gray-400 mb-2">
Favorite Destination
</p>
<p className="text-gray-100 font-medium">Dubai, UAE</p>
<p className="text-sm text-gray-400 mt-1">2 bookings</p>
</div>
<div className="p-4 bg-gray-700 rounded-lg">
<p className="text-sm text-gray-400 mb-2">
Preferred Agency
</p>
<p className="text-gray-100 font-medium">
Silk Road Travel
</p>
<p className="text-sm text-gray-400 mt-1">
2 out of 3 bookings
</p>
</div>
<div className="p-4 bg-gray-700 rounded-lg">
<p className="text-sm text-gray-400 mb-2">
Average Booking Value
</p>
<p className="text-green-400 font-bold">
$
{(
totalSpent /
mockUserData.totalBookings /
1000000
).toFixed(1)}
M
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}

32
src/pages/news/lib/api.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { NewsType } from "@/pages/news/lib/type";
const STORAGE_KEY = "news_data";
export const getAllNews = (): NewsType[] => {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
};
export const addNews = (news: Omit<NewsType, "id" | "createdAt">) => {
const all = getAllNews();
const newNews: NewsType = {
id: "1",
createdAt: new Date().toISOString(),
...news,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify([newNews, ...all]));
};
export const updateNews = (id: string, updated: Partial<NewsType>) => {
const all = getAllNews().map((n) => (n.id === id ? { ...n, ...updated } : n));
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
};
export const deleteNews = (id: string) => {
const filtered = getAllNews().filter((n) => n.id !== id);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
};
export const getNewsById = (id: string) => {
return getAllNews().find((n) => n.id === id);
};

View File

@@ -0,0 +1,54 @@
import type { NewsAll } from "./type";
export const fakeNewsData: NewsAll[] = [
{
id: 1,
short_title: "Yangi sayohat yonalishlari ochildi",
slug: "yangi-sayohat-yonalishlari",
image: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e",
category: { id: 1, name: "Turlar" },
short_text:
"Bu yozda yangi xalqaro yonalishlar ochilmoqda — Turkiya, Dubay, Malayziya va yana koplab manzillar.",
created: "2025-10-15T08:45:00Z",
},
{
id: 2,
short_title: "Tur firmalar uchun yangi litsenziya tizimi",
slug: "litsenziya-tizimi",
image: "https://images.unsplash.com/photo-1488646953014-85cb44e25828",
category: { id: 2, name: "Yangiliklar" },
short_text:
"Turizm agentliklari uchun raqamli litsenziya olish tizimi ishga tushirildi. Endi barcha jarayon onlayn boladi.",
created: "2025-09-22T12:30:00Z",
},
{
id: 3,
short_title: "Sayohat bozorida narxlar pasaymoqda",
slug: "narxlar-pasaymoqda",
image: "https://images.unsplash.com/photo-1473625247510-8ceb1760943f",
category: { id: 3, name: "Blog" },
short_text:
"Songgi haftalarda xalqaro aviabiletlar narxi 15% gacha arzonlashgani kuzatildi. Mutaxassislar bunga tahlil beradi.",
created: "2025-10-10T09:15:00Z",
},
{
id: 4,
short_title: "Yangi mehmonxonalar tarmogi ish boshladi",
slug: "yangi-mehmonxonalar",
image: "https://images.unsplash.com/photo-1566073771259-6a8506099945",
category: { id: 4, name: "Yangiliklar" },
short_text:
"Ozbekistonda 5 ta yangi premium mehmonxona ochildi. Bu turizm industriyasi uchun muhim qadam.",
created: "2025-10-05T15:10:00Z",
},
{
id: 5,
short_title: "Sayyohlar uchun foydali maslahatlar",
slug: "sayyohlar-maslahatlar",
image: "https://images.unsplash.com/photo-1526778548025-fa2f459cd5c1",
category: { id: 5, name: "Blog" },
short_text:
"Chet elga chiqayotganlar uchun xavfsizlik, valyuta va mobil aloqa haqida 10 ta foydali maslahat.",
created: "2025-09-30T10:00:00Z",
},
];

View File

@@ -0,0 +1,21 @@
import z from "zod";
export const newsForm = z.object({
title: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
desc: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
category: z.string().min(1, {
message: "Majburiy maydon",
}),
banner: z.string().min(1, { message: "Banner rasmi majburiy" }),
});
export const newsPostForm = z.object({
desc: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
banner: z.string().min(1, { message: "Banner rasmi majburiy" }),
});

View File

@@ -0,0 +1,33 @@
// types.ts
export interface NewsCategory {
id: string;
name: string;
}
export interface NewsTag {
id: string;
name: string;
}
export interface NewsType {
id: string;
title: string;
content: string;
image?: string;
category?: NewsCategory;
tags?: NewsTag[];
createdAt?: string;
}
export interface NewsAll {
id: number;
short_title: string;
slug: string;
image: string;
category: {
id: number;
name: string;
};
short_text: string;
created: string;
}

View File

@@ -0,0 +1,37 @@
"use client";
import StepOne from "@/pages/news/ui/StepOne";
import StepTwo from "@/pages/news/ui/StepTwo";
import { useMemo, useState } from "react";
import { useParams } from "react-router-dom";
const AddNews = () => {
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 text-white rounded-2xl shadow-lg">
<h1 className="text-3xl font-bold mb-6">
{isEditMode ? "Yangilikni tahrirlash" : "Yangi yangilik qoshish"}
</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. Yangilik sarlavhasi
</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. Yangilik ma'lumotlari
</div>
</div>
{step === 1 && <StepOne isEditMode={isEditMode} setStep={setStep} />}
{step === 2 && <StepTwo isEditMode={isEditMode} setStep={setStep} />}
</div>
);
};
export default AddNews;

210
src/pages/news/ui/News.tsx Normal file
View File

@@ -0,0 +1,210 @@
"use client";
import { fakeNewsData } from "@/pages/news/lib/data";
import type { NewsAll } from "@/pages/news/lib/type";
import formatDate from "@/shared/lib/formatDate";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import clsx from "clsx";
import { Calendar, Edit, FolderOpen, PlusCircle, Trash2 } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const News = () => {
const [newsList, setNewsList] = useState<NewsAll[]>(fakeNewsData);
const loading = false;
const error = null;
const [deleteId, setDeleteId] = useState<number | null>(null);
const navigate = useNavigate();
const confirmDelete = () => {
if (deleteId !== null) {
setNewsList((prev) => prev.filter((t) => t.id !== deleteId));
setDeleteId(null);
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
<p className="text-lg">Yuklanmoqda...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
<div className="text-center">
<p className="text-xl text-red-400">{error}</p>
<Button className="mt-4">Qayta urinish</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 w-full text-white p-6">
{/* Header */}
<div className="flex justify-between items-center mb-8 w-[90%] mx-auto">
<div>
<h1 className="text-4xl font-bold mb-2">Yangiliklar</h1>
<p className="text-gray-400">
Jami {newsList.length} ta yangilik mavjud
</p>
</div>
<Button
onClick={() => navigate("/news/add")}
className="flex items-center gap-2 cursor-pointer bg-blue-600 hover:bg-blue-700 text-white"
>
<PlusCircle size={18} />
Yangilik qo'shish
</Button>
</div>
{/* News Grid */}
<div
className={clsx(
"gap-6 w-[90%] mx-auto",
newsList.length === 0
? "flex justify-center items-center min-h-[60vh]"
: "grid md:grid-cols-2 lg:grid-cols-3",
)}
>
{newsList.length === 0 ? (
<div className="text-center py-12">
<div className="mb-6">
<div className="w-24 h-24 bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-4">
<FolderOpen size={48} className="text-gray-600" />
</div>
</div>
<p className="text-2xl text-gray-400 mb-2 font-semibold">
Hozircha yangilik yo'q
</p>
<p className="text-gray-500 mb-6">
Birinchi yangilikni qo'shib boshlang
</p>
<Button
onClick={() => navigate("/news/add")}
className="flex items-center gap-2 mx-auto bg-blue-600 hover:bg-blue-700 text-white"
>
<PlusCircle size={18} /> Yangilik qo'shish
</Button>
</div>
) : (
newsList.map((item) => (
<Card
key={item.id}
className="overflow-hidden bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group"
>
{/* Image */}
<div className="relative h-48 overflow-hidden">
<img
src={item.image}
alt={item.short_title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
onError={(e) => {
e.currentTarget.src =
"https://images.unsplash.com/photo-1507525428034-b723cf961d3e";
}}
/>
{/* Category Badge */}
{item.category && (
<Badge className="absolute top-3 left-3 bg-blue-600 hover:bg-blue-700 text-white border-0">
<FolderOpen size={12} className="mr-1" />
{item.category.name}
</Badge>
)}
</div>
{/* Content */}
<div className="p-4 space-y-3">
{/* Title */}
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
{item.short_title}
</h2>
{/* Short Text */}
<p className="text-sm text-gray-400 line-clamp-3 leading-relaxed">
{item.short_text}
</p>
{/* Date */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar size={14} />
<span>{formatDate.format(item.created, "DD.MM.YYYY")}</span>
</div>
{/* Slug */}
<div className="pt-2 border-t border-neutral-800">
<code className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded">
/{item.slug}
</code>
</div>
{/* Actions */}
<div className="flex justify-end gap-2 pt-3">
<Button
onClick={() => navigate(`/news/add`)}
size="sm"
variant="outline"
className="hover:bg-neutral-700 hover:text-blue-400"
>
<Edit size={16} className="mr-1" />
Tahrirlash
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => setDeleteId(item.id)}
className="hover:bg-red-700"
>
<Trash2 size={16} className="mr-1" />
O'chirish
</Button>
</div>
</div>
</Card>
))
)}
</div>
<Dialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
<DialogContent className="sm:max-w-[425px] bg-gray-900">
<DialogHeader>
<DialogTitle className="text-xl">
Yangilikni 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>
);
};
export default News;

View File

@@ -0,0 +1,232 @@
"use client";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
type NewsCategoryType = {
id: number;
name: string;
count: number;
};
const fakeCategories: NewsCategoryType[] = [
{
id: 1,
name: "Blog",
count: 12,
},
{
id: 2,
name: "News",
count: 8,
},
{
id: 3,
name: "Tours",
count: 5,
},
];
const NewsCategory = () => {
const newsForm = z.object({
title: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
});
const [categories, setCategories] =
useState<NewsCategoryType[]>(fakeCategories);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editItem, setEditItem] = useState<NewsCategoryType | null>(null);
const form = useForm<z.infer<typeof newsForm>>({
resolver: zodResolver(newsForm),
defaultValues: {
title: "",
},
});
useEffect(() => {
if (editItem) {
form.setValue("title", editItem.name);
}
}, [editItem, form]);
const openDialog = () => {
setIsDialogOpen(true);
};
function onSubmit() {
setIsDialogOpen(false);
}
const handleDelete = (id: number) => {
setCategories((prev) => prev.filter((c) => c.id !== id));
};
return (
<div className="min-h-screen bg-gray-900 w-full text-gray-100 p-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-semibold tracking-tight">
News Categories
</h1>
<Button
onClick={() => {
openDialog();
form.reset();
}}
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white"
>
<PlusCircle className="w-4 h-4 mr-2" /> Yangi qoshish
</Button>
</div>
<div className="rounded-lg border border-gray-800 overflow-hidden bg-gray-900">
<Table>
<TableHeader className="bg-gray-800/50">
<TableRow>
<TableHead className="text-gray-300 w-[60px]">#</TableHead>
<TableHead className="text-gray-300">Kategoriya nomi</TableHead>
<TableHead className="text-gray-300 text-center">
Yangiliklar soni
</TableHead>
<TableHead className="text-gray-300 text-right">
Harakatlar
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.length > 0 ? (
categories.map((cat, index) => (
<TableRow
key={cat.id}
className="border-b border-gray-800 hover:bg-gray-800/40 transition-colors"
>
<TableCell>{index + 1}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div>
<p className="font-medium">{cat.name}</p>
</div>
</div>
</TableCell>
<TableCell className="text-center">
<Badge variant="secondary" className="bg-gray-700">
{cat.count} ta
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
className="border-gray-700 text-gray-200 hover:bg-gray-800"
onClick={() => {
openDialog();
setEditItem(cat);
}}
>
<Edit className="w-4 h-4 mr-1" /> Edit
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(cat.id)}
>
<Trash2 className="w-4 h-4 mr-1" /> Ochirish
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={4}
className="text-center py-8 text-gray-400"
>
Hech qanday kategoriya topilmadi
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="bg-gray-900 border border-gray-700 text-gray-100">
<DialogHeader>
<DialogTitle>
{editItem ? "Kategoriya tahrirlash" : "Yangi kategoriya qoshish"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-gray-900"
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<Label className="text-md">Yangilik nomi</Label>
<FormControl>
<Input
placeholder="Masalan: Yangi turistik joylar ochildi"
{...field}
className="h-12 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
className="bg-blue-600 px-5 py-5 hover:bg-blue-700 text-white mt-4 cursor-pointer"
>
Saqlash
</Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
</div>
);
};
export default NewsCategory;

View File

@@ -0,0 +1,190 @@
import { newsForm } from "@/pages/news/lib/form";
import { Button } from "@/shared/ui/button";
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,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { XIcon } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import type z from "zod";
const StepOne = ({
setStep,
isEditMode,
}: {
setStep: Dispatch<SetStateAction<number>>;
isEditMode: boolean;
}) => {
const categories = [
{ name: "Blog", id: "1" },
{ name: "Tours", id: "2" },
{ name: "News", id: "3" },
];
const form = useForm<z.infer<typeof newsForm>>({
resolver: zodResolver(newsForm),
defaultValues: {
title: "",
category: "",
banner: "",
desc: "",
},
});
function onSubmit() {
setStep(2);
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-gray-900"
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<Label className="text-md">Yangilik nomi</Label>
<FormControl>
<Input
placeholder="Masalan: Yangi turistik joylar ochildi"
{...field}
className="h-12 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="desc"
render={({ field }) => (
<FormItem>
<Label className="text-md">Yangilik haqida</Label>
<FormControl>
<Textarea
placeholder="Yangilik haqida"
{...field}
className="min-h-48 max-h-56 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<Label className="text-md">Kategoriya</Label>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger className="w-full !h-12 bg-gray-800 border-gray-700 text-white">
<SelectValue placeholder="Kategoriya tanlang" />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-700 text-white">
<SelectGroup>
<SelectLabel>Kategoriyalar</SelectLabel>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id}>
{cat.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="banner"
render={() => (
<FormItem>
<Label className="text-md">Banner rasmi</Label>
<FormControl>
<div className="flex flex-col gap-3 w-full">
<Input
type="file"
id="license-files"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
form.setValue("banner", url);
}
}}
className="hidden"
/>
<label
htmlFor="license-files"
className="w-full border-2 border-dashed h-40 border-[#D3D3D3] flex flex-col items-center gap-2 justify-center py-4 rounded-2xl cursor-pointer"
>
<p className="font-semibold text-xl text-[#FFFF]">
Drag or select files
</p>
<p className="text-[#FFFF] text-sm">
Drop files here or click to browse
</p>
</label>
{form.watch("banner") && (
<div className="relative size-24 rounded-md overflow-hidden border">
<img
src={form.watch("banner")}
alt={`Nanner`}
className="object-cover w-full h-full"
/>
<button
type="button"
onClick={() => form.setValue("banner", "")}
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>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="w-full flex justify-end">
<Button
type="submit"
className="bg-blue-600 px-5 py-5 hover:bg-blue-700 text-white mt-4 cursor-pointer"
>
{isEditMode ? "Yangilikni saqlash" : "Keyingisi"}
</Button>
</div>
</form>
</Form>
);
};
export default StepOne;

View File

@@ -0,0 +1,195 @@
"use client";
import { Button } from "@/shared/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusCircle, Trash2, XIcon } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import z from "zod";
const newsItemSchema = z.object({
desc: z.string().min(2, {
message: "Yangilik matni kamida 2 belgidan iborat bolishi kerak",
}),
banner: z.string().min(1, { message: "Banner rasmi majburiy" }),
});
const newsListSchema = z.object({
items: z
.array(newsItemSchema)
.min(1, { message: "Kamida 1 ta yangilik kerak" }),
});
type NewsFormType = z.infer<typeof newsListSchema>;
const StepTwo = ({
setStep,
isEditMode,
}: {
setStep: Dispatch<SetStateAction<number>>;
isEditMode: boolean;
}) => {
const form = useForm<NewsFormType>({
resolver: zodResolver(newsListSchema),
defaultValues: {
items: [{ desc: "", banner: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "items",
});
const navigator = useNavigate();
function onSubmit() {
navigator("/news");
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 bg-gray-900 p-6 rounded-2xl text-white"
>
<h2 className="text-2xl font-semibold">Yangiliklar royxati</h2>
{fields.map((field, index) => (
<div
key={field.id}
className="relative border border-gray-700 bg-gray-800 rounded-xl p-4 space-y-4"
>
{/* O'chirish tugmasi */}
{fields.length > 1 && (
<button
type="button"
onClick={() => remove(index)}
className="absolute top-3 right-3 text-red-400 hover:text-red-500"
>
<Trash2 className="size-5" />
</button>
)}
{/* DESC FIELD */}
<FormField
control={form.control}
name={`items.${index}.desc`}
render={({ field }) => (
<FormItem>
<Label className="text-md">Yangilik haqida</Label>
<FormControl>
<Textarea
placeholder="Yangilik haqida"
{...field}
className="min-h-48 max-h-56 !text-md bg-gray-800 border-gray-700 text-white"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* BANNER FIELD */}
<FormField
control={form.control}
name={`items.${index}.banner`}
render={() => (
<FormItem>
<Label className="text-md">Banner rasmi</Label>
<FormControl>
<div className="flex flex-col gap-3 w-full">
<Input
type="file"
id={`file-${index}`}
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
form.setValue(`items.${index}.banner`, url);
}
}}
className="hidden"
/>
<label
htmlFor={`file-${index}`}
className="w-full border-2 border-dashed h-40 border-gray-600 hover:border-gray-500 transition-all flex flex-col items-center gap-2 justify-center py-4 rounded-2xl cursor-pointer"
>
<p className="font-semibold text-xl text-white">
Drag or select files
</p>
<p className="text-gray-300 text-sm">
Drop files here or click to browse
</p>
</label>
{form.watch(`items.${index}.banner`) && (
<div className="relative size-24 rounded-md overflow-hidden border border-gray-700">
<img
src={form.watch(`items.${index}.banner`)}
alt="Banner preview"
className="object-cover w-full h-full"
/>
<button
type="button"
onClick={() =>
form.setValue(`items.${index}.banner`, "")
}
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>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
))}
<div className="flex justify-end">
<Button
type="button"
onClick={() => append({ desc: "", banner: "" })}
className="flex items-center px-6 py-5 text-lg gap-2 bg-gray-600 hover:bg-gray-700 text-white cursor-pointer"
>
<PlusCircle className="size-5" />
Qoshish
</Button>
</div>
{/* Navigatsiya tugmalari */}
<div className="w-full flex justify-between pt-4">
<Button
type="button"
onClick={() => setStep(1)}
className="bg-gray-600 hover:bg-gray-700 text-white"
>
Orqaga
</Button>
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{isEditMode ? "Yangiliklarni saqlash" : "Saqlash"}
</Button>
</div>
</form>
</Form>
);
};
export default StepTwo;

269
src/pages/seo/ui/Seo.tsx Normal file
View File

@@ -0,0 +1,269 @@
"use client";
import {
AlertCircle,
CheckCircle,
FileText,
Image as ImageIcon,
TrendingUp,
} from "lucide-react";
import { useState, type ChangeEvent } from "react";
type SeoData = {
title: string;
description: string;
keywords: string;
ogTitle: string;
ogDescription: string;
ogImage: string;
};
export default function Seo() {
const [formData, setFormData] = useState<SeoData>({
title: "",
description: "",
keywords: "",
ogTitle: "",
ogDescription: "",
ogImage: "",
});
const [savedSeo, setSavedSeo] = useState<SeoData | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result as string;
setImagePreview(result);
setFormData((prev) => ({
...prev,
ogImage: result,
}));
};
reader.readAsDataURL(file);
}
};
const handleSave = () => {
setSavedSeo(formData);
setFormData({
description: "",
keywords: "",
ogDescription: "",
ogImage: "",
ogTitle: "",
title: "",
});
};
const getTitleLength = () => formData.title.length;
const getDescriptionLength = () => formData.description.length;
const isValidTitle = getTitleLength() > 30 && getTitleLength() <= 60;
const isValidDescription =
getDescriptionLength() > 120 && getDescriptionLength() <= 160;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 p-8 w-full">
<div className="max-w-[90%] mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<TrendingUp className="w-8 h-8 text-blue-400" />
<h1 className="text-4xl font-bold text-white">SEO Manager</h1>
</div>
<p className="text-slate-400">
Saytingizni qidiruv tizimida yaxshi pozitsiyaga keltiring
</p>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 gap-8">
<div className="bg-slate-800 rounded-lg p-6 space-y-6">
{/* Title */}
<div>
<label className="block text-sm font-semibold text-white mb-2">
<FileText className="inline w-4 h-4 mr-1" /> Page Title
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Sahifa sarlavhasi (3060 belgi)"
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex items-center justify-between mt-2">
<span className="text-sm text-slate-400">
{getTitleLength()} / 60
</span>
{isValidTitle && (
<CheckCircle className="w-5 h-5 text-green-400" />
)}
{getTitleLength() > 0 && !isValidTitle && (
<AlertCircle className="w-5 h-5 text-yellow-400" />
)}
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-semibold text-white mb-2">
Meta Description
</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Sahifa tavsifi (120160 belgi)"
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
<div className="flex items-center justify-between mt-2">
<span className="text-sm text-slate-400">
{getDescriptionLength()} / 160
</span>
{isValidDescription && (
<CheckCircle className="w-5 h-5 text-green-400" />
)}
{getDescriptionLength() > 0 && !isValidDescription && (
<AlertCircle className="w-5 h-5 text-yellow-400" />
)}
</div>
</div>
{/* Keywords */}
<div>
<label className="block text-sm font-semibold text-white mb-2">
Keywords
</label>
<input
type="text"
name="keywords"
value={formData.keywords}
onChange={handleChange}
placeholder="Kalit so'zlar (vergul bilan ajratilgan)"
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-slate-400 mt-1">
Masalan: Python, Web Development, Coding
</p>
</div>
{/* OG Tags */}
<div className="border-t border-slate-700 pt-6">
<h3 className="text-sm font-semibold text-white mb-4">
Open Graph (Ijtimoiy Tarmoqlar)
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm text-slate-300 mb-2">
OG Title
</label>
<input
type="text"
name="ogTitle"
value={formData.ogTitle}
onChange={handleChange}
placeholder="Ijtimoiy tarmoqdagi sarlavha"
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm text-slate-300 mb-2">
OG Description
</label>
<textarea
name="ogDescription"
value={formData.ogDescription}
onChange={handleChange}
placeholder="Ijtimoiy tarmoqdagi tavsif"
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
</div>
<div>
<label className="block text-sm text-slate-300 mb-2">
<ImageIcon className="inline w-4 h-4 mr-1" /> OG Image
</label>
<div className="space-y-3">
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 file:bg-blue-600 file:text-white file:px-3 file:py-1 file:rounded file:border-0 file:cursor-pointer"
/>
{imagePreview && (
<div className="relative">
<img
src={imagePreview}
alt="Preview"
className="w-full h-40 object-cover rounded-lg"
/>
<button
type="button"
onClick={() => {
setImagePreview(null);
setFormData((prev) => ({
...prev,
ogImage: "",
}));
}}
className="mt-2 text-xs bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
>
Ochirish
</button>
</div>
)}
</div>
</div>
</div>
</div>
<button
onClick={handleSave}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors"
>
Saqlash
</button>
</div>
</div>
{/* Saved SEO Data (Preview) */}
{savedSeo && (
<div className="mt-8 bg-slate-700 rounded-lg p-6 text-slate-200">
<h3 className="text-lg font-semibold mb-2">
Saqlangan SEO Malumotlari
</h3>
<pre className="bg-slate-800 p-4 rounded text-xs overflow-auto">
{JSON.stringify(
{
...savedSeo,
ogImage: savedSeo.ogImage
? savedSeo.ogImage.substring(0, 100) + "..."
: "",
},
null,
2,
)}
</pre>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,350 @@
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import { Checkbox } from "@/shared/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Edit2, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import ReactQuill from "react-quill-new";
import "react-quill-new/dist/quill.snow.css";
type Offer = {
id: string;
title: string;
audience: "Foydalanuvchi qollanmasi" | "Maxfiylik siyosati";
content: string;
active: boolean;
createdAt: string;
};
const FAKE_DATA: Offer[] = [
{
id: "of-1",
title: "Ommaviy oferta - Standart shartlar",
audience: "Foydalanuvchi qollanmasi",
content:
"Bu hujjat kompaniya va xizmatlardan foydalanish bo'yicha umumiy shartlarni o'z ichiga oladi.",
active: true,
createdAt: new Date().toISOString(),
},
{
id: "of-2",
title: "Foydalanuvchi qollanmasi uchun oferta",
audience: "Foydalanuvchi qollanmasi",
content: "Foydalanuvchi qollanmasi uchun maxsus shartlar va kafolatlar.",
active: false,
createdAt: new Date(Date.now() - 86400000).toISOString(),
},
];
const STORAGE_KEY = "ommaviy_oferta_v1";
export default function PolicyCrud() {
const [items, setItems] = useState<Offer[]>([]);
const [query, setQuery] = useState("");
const [editing, setEditing] = useState<Offer | null>(null);
const [form, setForm] = useState<Partial<Offer>>({
title: "",
audience: "Foydalanuvchi qollanmasi",
content: "",
active: true,
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as Offer[];
setItems(parsed);
} catch {
setItems(FAKE_DATA);
}
} else {
setItems(FAKE_DATA);
}
}, []);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}, [items]);
function resetForm() {
setForm({
title: "",
audience: "Foydalanuvchi qollanmasi",
content: "",
active: true,
});
setErrors({});
setEditing(null);
}
function validate(f: Partial<Offer>) {
const e: Record<string, string> = {};
if (!f.title || f.title.trim().length < 3)
e.title = "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak";
if (!f.content || f.content.trim().length < 10)
e.content = "Kontent kamida 10 ta belgidan iborat bo'lishi kerak";
return e;
}
function handleCreateOrUpdate() {
const validation = validate(form);
if (Object.keys(validation).length) {
setErrors(validation);
return;
}
if (editing) {
setItems((prev) =>
prev.map((it) =>
it.id === editing.id ? { ...it, ...(form as Offer) } : it,
),
);
resetForm();
} else {
const newItem: Offer = {
id: `of-${Date.now()}`,
title: (form.title || "Untitled").trim(),
audience: (form.audience as Offer["audience"]) || "Barcha",
content: (form.content || "").trim(),
active: form.active ?? true,
createdAt: new Date().toISOString(),
};
setItems((prev) => [newItem, ...prev]);
resetForm();
}
}
function startEdit(item: Offer) {
setEditing(item);
setForm({ ...item });
setErrors({});
window.scrollTo({ top: 0, behavior: "smooth" });
}
function removeItem(id: string) {
setItems((prev) => prev.filter((p) => p.id !== id));
}
function toggleActive(id: string) {
setItems((prev) =>
prev.map((p) => (p.id === id ? { ...p, active: !p.active } : p)),
);
}
const filtered = items.filter((it) => {
const q = query.trim().toLowerCase();
if (!q) return true;
return (
it.title.toLowerCase().includes(q) ||
it.content.toLowerCase().includes(q) ||
it.audience.toLowerCase().includes(q)
);
});
return (
<div className="min-h-screen w-full p-6 bg-gray-900">
<div className="max-w-[90%] mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Ommaviy oferta</h1>
</div>
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>
{editing ? "Tahrirish" : "Yangi oferta yaratish"}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Sarlavha</label>
<Input
value={form.title || ""}
onChange={(e) =>
setForm((s) => ({ ...s, title: e.target.value }))
}
placeholder="Ommaviy oferta sarlavhasi"
className="mt-1"
/>
{errors.title && (
<p className="text-destructive text-sm mt-1">{errors.title}</p>
)}
</div>
<div className="h-full w-[100%]">
<label className="text-sm font-medium">Kontent</label>
<div className="mt-1">
<ReactQuill
value={form.content || ""}
onChange={(value) =>
setForm((s) => ({ ...s, content: value }))
}
className="bg-gray-900 h-48"
placeholder="Oferta matnini kiriting..."
/>
</div>
{errors.content && (
<p className="text-destructive text-sm mt-1">
{errors.content}
</p>
)}
</div>
<div className="grid grid-cols-1 gap-4 mt-24">
<div>
<label className="text-sm font-medium">Kimlar uchun</label>
<Select
value={form.audience || "Barcha"}
onValueChange={(value) =>
setForm((s) => ({
...s,
audience: value as Offer["audience"],
}))
}
>
<SelectTrigger className="mt-1 w-full !h-12">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Foydalanuvchi qollanmasi">
Foydalanuvchi qollanmasi
</SelectItem>
<SelectItem value="Maxfiylik siyosati">
Maxfiylik siyosati
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={!!form.active}
onCheckedChange={(checked) =>
setForm((s) => ({ ...s, active: checked ? true : false }))
}
/>
<span>Faol</span>
</label>
</div>
</div>
<div className="flex gap-2 pt-4">
<Button
onClick={handleCreateOrUpdate}
className="bg-blue-600 hover:bg-blue-700"
>
{editing ? "Saqlash" : "Yaratish"}
</Button>
<Button variant="outline" onClick={resetForm}>
Bekor qilish
</Button>
</div>
</CardContent>
</Card>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Qidirish sarlavha, kontent yoki auditoriya bo'yicha..."
className="max-w-sm"
/>
<div className="flex gap-4 text-sm text-muted-foreground">
<span>Natija: {filtered.length}</span>
<span>Barcha: {items.length}</span>
</div>
</div>
<div className="space-y-3">
{filtered.length === 0 && (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground text-center">
Natija topilmadi.
</p>
</CardContent>
</Card>
)}
{filtered.map((it) => (
<Card key={it.id} className="overflow-hidden">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div className="flex-1">
<h3 className="font-semibold text-lg">{it.title}</h3>
<p className="text-sm text-muted-foreground mt-1">
{it.audience} {new Date(it.createdAt).toLocaleString()}
</p>
<p className="mt-3 text-sm line-clamp-3">{it.content}</p>
</div>
<div className="flex gap-2 items-center flex-wrap md:flex-col md:flex-nowrap">
<Button
onClick={() => startEdit(it)}
variant="outline"
size="sm"
>
<Edit2 className="w-4 h-4 mr-1" />
Tahrirlash
</Button>
<Button
onClick={() => toggleActive(it.id)}
variant={it.active ? "default" : "outline"}
size="sm"
className={
it.active ? "bg-green-600 hover:bg-green-700" : ""
}
>
{it.active ? "Faol" : "Faol emas"}
</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash2 className="w-4 h-4 mr-1" />
O'chirish
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>O'chirish tasdiqlash</DialogTitle>
<DialogDescription>
Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni
bekor qilib bo'lmaydi.
</DialogDescription>
<div className="flex gap-3 justify-end pt-4">
<Button>Bekor qilish</Button>
<Button
variant={"destructive"}
onClick={() => removeItem(it.id)}
>
O'chirish
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,351 @@
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import { Checkbox } from "@/shared/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Edit2, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import ReactQuill from "react-quill-new";
import "react-quill-new/dist/quill.snow.css";
type Offer = {
id: string;
title: string;
audience: "Jismoniy shaxslar" | "Yuridik shaxslar";
content: string;
active: boolean;
createdAt: string;
};
const FAKE_DATA: Offer[] = [
{
id: "of-1",
title: "Ommaviy oferta - Standart shartlar",
audience: "Jismoniy shaxslar",
content:
"Bu hujjat kompaniya va xizmatlardan foydalanish bo'yicha umumiy shartlarni o'z ichiga oladi.",
active: true,
createdAt: new Date().toISOString(),
},
{
id: "of-2",
title: "Yuridik shaxslar uchun oferta",
audience: "Yuridik shaxslar",
content: "Yuridik shaxslar uchun maxsus shartlar va kafolatlar.",
active: false,
createdAt: new Date(Date.now() - 86400000).toISOString(),
},
];
const STORAGE_KEY = "ommaviy_oferta_v1";
export default function OmmaviyOfertaCRUD() {
const [items, setItems] = useState<Offer[]>([]);
const [query, setQuery] = useState("");
const [editing, setEditing] = useState<Offer | null>(null);
const [form, setForm] = useState<Partial<Offer>>({
title: "",
audience: "Jismoniy shaxslar",
content: "",
active: true,
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as Offer[];
setItems(parsed);
} catch {
setItems(FAKE_DATA);
}
} else {
setItems(FAKE_DATA);
}
}, []);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}, [items]);
function resetForm() {
setForm({
title: "",
audience: "Jismoniy shaxslar",
content: "",
active: true,
});
setErrors({});
setEditing(null);
}
function validate(f: Partial<Offer>) {
const e: Record<string, string> = {};
if (!f.title || f.title.trim().length < 3)
e.title = "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak";
if (!f.content || f.content.trim().length < 10)
e.content = "Kontent kamida 10 ta belgidan iborat bo'lishi kerak";
return e;
}
function handleCreateOrUpdate() {
const validation = validate(form);
if (Object.keys(validation).length) {
setErrors(validation);
return;
}
if (editing) {
setItems((prev) =>
prev.map((it) =>
it.id === editing.id ? { ...it, ...(form as Offer) } : it,
),
);
resetForm();
} else {
const newItem: Offer = {
id: `of-${Date.now()}`,
title: (form.title || "Untitled").trim(),
audience: (form.audience as Offer["audience"]) || "Barcha",
content: (form.content || "").trim(),
active: form.active ?? true,
createdAt: new Date().toISOString(),
};
setItems((prev) => [newItem, ...prev]);
resetForm();
}
}
function startEdit(item: Offer) {
setEditing(item);
setForm({ ...item });
setErrors({});
window.scrollTo({ top: 0, behavior: "smooth" });
}
function removeItem(id: string) {
setItems((prev) => prev.filter((p) => p.id !== id));
}
function toggleActive(id: string) {
setItems((prev) =>
prev.map((p) => (p.id === id ? { ...p, active: !p.active } : p)),
);
}
const filtered = items.filter((it) => {
const q = query.trim().toLowerCase();
if (!q) return true;
return (
it.title.toLowerCase().includes(q) ||
it.content.toLowerCase().includes(q) ||
it.audience.toLowerCase().includes(q)
);
});
return (
<div className="min-h-screen w-full p-6 bg-gray-900">
<div className="max-w-[90%] mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Ommaviy oferta</h1>
</div>
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>
{editing ? "Tahrirish" : "Yangi oferta yaratish"}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Sarlavha</label>
<Input
value={form.title || ""}
onChange={(e) =>
setForm((s) => ({ ...s, title: e.target.value }))
}
placeholder="Ommaviy oferta sarlavhasi"
className="mt-1"
/>
{errors.title && (
<p className="text-destructive text-sm mt-1">{errors.title}</p>
)}
</div>
<div className="h-full w-[100%]">
<label className="text-sm font-medium">Kontent</label>
<div className="mt-1">
<ReactQuill
value={form.content || ""}
onChange={(value) =>
setForm((s) => ({ ...s, content: value }))
}
className="bg-gray-900 h-48"
placeholder="Oferta matnini kiriting..."
/>
</div>
{errors.content && (
<p className="text-destructive text-sm mt-1">
{errors.content}
</p>
)}
</div>
<div className="grid grid-cols-1 gap-4 mt-24">
<div>
<label className="text-sm font-medium">Kimlar uchun</label>
<Select
value={form.audience || "Barcha"}
onValueChange={(value) =>
setForm((s) => ({
...s,
audience: value as Offer["audience"],
}))
}
>
<SelectTrigger className="mt-1 w-full !h-12">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Barcha">Barcha</SelectItem>
<SelectItem value="Jismoniy shaxslar">
Jismoniy shaxslar uchun
</SelectItem>
<SelectItem value="Yuridik shaxslar">
Yuridik shaxslar uchun
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={!!form.active}
onCheckedChange={(checked) =>
setForm((s) => ({ ...s, active: checked ? true : false }))
}
/>
<span>Faol</span>
</label>
</div>
</div>
<div className="flex gap-2 pt-4">
<Button
onClick={handleCreateOrUpdate}
className="bg-blue-600 hover:bg-blue-700"
>
{editing ? "Saqlash" : "Yaratish"}
</Button>
<Button variant="outline" onClick={resetForm}>
Bekor qilish
</Button>
</div>
</CardContent>
</Card>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Qidirish sarlavha, kontent yoki auditoriya bo'yicha..."
className="max-w-sm"
/>
<div className="flex gap-4 text-sm text-muted-foreground">
<span>Natija: {filtered.length}</span>
<span>Barcha: {items.length}</span>
</div>
</div>
<div className="space-y-3">
{filtered.length === 0 && (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground text-center">
Natija topilmadi.
</p>
</CardContent>
</Card>
)}
{filtered.map((it) => (
<Card key={it.id} className="overflow-hidden">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div className="flex-1">
<h3 className="font-semibold text-lg">{it.title}</h3>
<p className="text-sm text-muted-foreground mt-1">
{it.audience} {new Date(it.createdAt).toLocaleString()}
</p>
<p className="mt-3 text-sm line-clamp-3">{it.content}</p>
</div>
<div className="flex gap-2 items-center flex-wrap md:flex-col md:flex-nowrap">
<Button
onClick={() => startEdit(it)}
variant="outline"
size="sm"
>
<Edit2 className="w-4 h-4 mr-1" />
Tahrirlash
</Button>
<Button
onClick={() => toggleActive(it.id)}
variant={it.active ? "default" : "outline"}
size="sm"
className={
it.active ? "bg-green-600 hover:bg-green-700" : ""
}
>
{it.active ? "Faol" : "Faol emas"}
</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">
<Trash2 className="w-4 h-4 mr-1" />
O'chirish
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>O'chirish tasdiqlash</DialogTitle>
<DialogDescription>
Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni
bekor qilib bo'lmaydi.
</DialogDescription>
<div className="flex gap-3 justify-end pt-4">
<Button>Bekor qilish</Button>
<Button
variant={"destructive"}
onClick={() => removeItem(it.id)}
>
O'chirish
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,280 @@
import { XIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { Link } from "react-router-dom";
interface Data {
id: number;
name: string;
address: string;
email: string;
phone: string;
instagram: string;
web_site: string;
documents: string[];
}
const sampleData: Data[] = [
{
id: 1,
name: "Alpha Travel",
address: "Tashkent, Mustaqillik ko'chasi 12",
email: "alpha@example.com",
phone: "+998901234567",
instagram: "https://instagram.com/alphatravel",
web_site: "https://alphatravel.uz",
documents: [
"https://turan-travel.com/uploads/files/license/sertificate.jpg",
"https://turan-travel.com/uploads/files/license/sertificate.jpg",
],
},
{
id: 2,
name: "Samarkand Tours",
address: "Samarkand, Registon 5",
email: "info@samarktours.uz",
phone: "+998903334455",
instagram: "https://instagram.com/samarktours",
web_site: "https://samarktours.uz",
documents: [
"https://turan-travel.com/uploads/files/license/sertificate.jpg",
],
},
];
const SupportAgency = ({ requests = sampleData }) => {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<Data | null>(null);
const filtered = useMemo(() => {
if (!query.trim()) return requests;
const q = query.toLowerCase();
return requests.filter(
(r) =>
(r.name && r.name.toLowerCase().includes(q)) ||
(r.email && r.email.toLowerCase().includes(q)) ||
(r.phone && r.phone.toLowerCase().includes(q)),
);
}, [requests, query]);
return (
<div className="p-4 w-full mx-auto">
<h2 className="text-2xl font-semibold mb-4">Agentlik soʻrovlari</h2>
<div className="flex gap-3 mb-6">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Qidiruv (ism, email yoki telefon)..."
className="flex-1 p-2 border rounded-md focus:outline-none focus:ring"
/>
<button
onClick={() => setQuery("")}
className="px-4 py-2 bg-gray-500 rounded-md hover:bg-gray-300 transition"
>
Tozalash
</button>
</div>
{filtered.length === 0 ? (
<div className="text-center text-gray-500">Soʻrov topilmadi.</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((r) => (
<div
key={r.id}
className="border rounded-lg p-4 shadow-sm hover:shadow-md transition bg-gray-800 text-white"
>
<div className="flex items-start justify-between">
<div>
<h3 className="text-lg font-medium">{r.name}</h3>
<p className="text-md">{r.address}</p>
</div>
<div className="text-md">{r.phone}</div>
</div>
<div className="mt-3 text-sm text-white">
<div>
<strong>Email:</strong>{" "}
<Link to={`mailto:${r.email}`} className="text-white">
{r.email}
</Link>
</div>
{r.instagram && (
<div>
<strong>Instagram:</strong>{" "}
<a
href={r.instagram}
target="_blank"
rel="noopener noreferrer"
className="text-white hover:underline"
>
@
{r.instagram.replace(
/^https?:\/\/(www\.)?instagram\.com\/?/,
"",
)}
</a>
</div>
)}
{r.web_site && (
<div>
<strong>Website:</strong>{" "}
<a
href={r.web_site}
target="_blank"
rel="noopener noreferrer"
className="text-white hover:underline"
>
{r.web_site.replace(/^https?:\/\//, "")}
</a>
</div>
)}
</div>
<div className="mt-4 flex gap-2">
<button
onClick={() => setSelected(r)}
className="px-3 py-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition"
>
Tafsilotlar
</button>
<Link
to={`mailto:${r.email}`}
className="px-3 py-1 rounded border text-sm transition"
>
Javob yozish
</Link>
</div>
</div>
))}
</div>
)}
{selected && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
onClick={() => setSelected(null)}
>
<div
className="bg-gray-900 rounded-lg max-w-2xl w-full p-6 relative max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setSelected(null)}
className="absolute top-3 right-3 text-white cursor-pointer text-2xl"
>
<XIcon />
</button>
<h3 className="text-xl font-semibold mb-2">{selected.name}</h3>
<p className="text-md text-white mb-4">{selected.address}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div className="text-md text-white">Email</div>
<a
href={`mailto:${selected.email}`}
className="block text-white hover:underline"
>
{selected.email}
</a>
</div>
<div>
<div className="text-md text-white">Telefon</div>
<div>{selected.phone}</div>
</div>
<div>
<div className="text-md text-white">Instagram</div>
{selected.instagram ? (
<a
href={selected.instagram}
target="_blank"
rel="noopener noreferrer"
className="block text-white hover:underline"
>
{selected.instagram}
</a>
) : (
<div className="text-white"></div>
)}
</div>
<div>
<div className="text-xs text-white">Website</div>
{selected.web_site ? (
<a
href={selected.web_site}
target="_blank"
rel="noopener noreferrer"
className="block text-white hover:underline"
>
{selected.web_site}
</a>
) : (
<div className="text-gray-400"></div>
)}
</div>
</div>
<div className="mt-5">
<div className="text-sm font-medium mb-2">Hujjatlar</div>
{selected.documents && selected.documents.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{selected.documents.map((doc, i) => (
<a
key={i}
href={doc}
target="_blank"
rel="noopener noreferrer"
className="group relative aspect-square border-2 border-gray-200 rounded-lg overflow-hidden hover:border-blue-500 transition"
>
<img
src={doc}
alt={`Hujjat ${i + 1}`}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition flex items-end">
<div className="w-full bg-gradient-to-t from-black/70 to-transparent p-2">
<span className="text-white text-xs font-medium">
Hujjat {i + 1}
</span>
</div>
</div>
</a>
))}
</div>
) : (
<div className="text-gray-500">Hujjat topilmadi</div>
)}
</div>
<div className="mt-6 flex justify-end gap-3">
<button
onClick={() => {
alert("Qabul qilindi (ishlab chiqishingiz kerak)");
setSelected(null);
}}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition"
>
Qabul qilish
</button>
<button
onClick={() => {
alert("Rad etildi (ishlab chiqishingiz kerak)");
setSelected(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition"
>
Rad etish
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default SupportAgency;

View File

@@ -0,0 +1,199 @@
"use client";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { MessageCircle, Phone, User } from "lucide-react";
import { useState } from "react";
type SupportRequest = {
id: number;
name: string;
phone: string;
message: string;
status: "Pending" | "Resolved";
};
const initialRequests: SupportRequest[] = [
{
id: 1,
name: "Alisher Karimov",
phone: "+998 90 123 45 67",
message: "Sayohat uchun viza hujjatlarini tayyorlashda yordam kerak.",
status: "Pending",
},
{
id: 2,
name: "Dilnoza Tursunova",
phone: "+998 91 765 43 21",
message: "Tolov muvaffaqiyatli otmadi, yordam bera olasizmi?",
status: "Resolved",
},
{
id: 3,
name: "Jamshid Abdullayev",
phone: "+998 93 555 22 11",
message: "Sayohat sanasini ozgartirishni istayman.",
status: "Pending",
},
{
id: 4,
name: "Jamshid Abdullayev",
phone: "+998 93 555 22 11",
message: "Sayohat sanasini ozgartirishni istayman.",
status: "Pending",
},
];
const SupportTours = () => {
const [requests, setRequests] = useState<SupportRequest[]>(initialRequests);
const [selected, setSelected] = useState<SupportRequest | null>(null);
const handleToggleStatus = (id: number) => {
setRequests((prev) =>
prev.map((req) =>
req.id === id
? {
...req,
status: req.status === "Pending" ? "Resolved" : "Pending",
}
: req,
),
);
setSelected((prev) =>
prev
? {
...prev,
status: prev.status === "Pending" ? "Resolved" : "Pending",
}
: prev,
);
};
return (
<div className="min-h-screen bg-gray-900 text-gray-100 p-6 space-y-6 w-full">
<h1 className="text-3xl font-bold tracking-tight mb-4 text-white">
Yordam sorovlari
</h1>
<div className="grid gap-5 sm:grid-cols-3 lg:grid-cols-3">
{requests.map((req) => (
<Card
key={req.id}
className="bg-gray-800/70 border border-gray-700 shadow-md hover:shadow-lg hover:bg-gray-800 transition-all duration-200"
>
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2 text-lg font-semibold text-white">
<User className="w-5 h-5 text-blue-400" />
{req.name}
</span>
<Badge
variant="outline"
className={`px-2 py-1 rounded-md text-xs font-medium ${
req.status === "Pending"
? "bg-red-500/10 text-red-400 border-red-400/40"
: "bg-green-500/10 text-green-400 border-green-400/40"
}`}
>
{req.status === "Pending" ? "Kutilmoqda" : "Yakunlangan"}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 mt-1">
<div className="flex items-center gap-2 text-sm text-gray-400">
<Phone className="w-4 h-4 text-gray-400" />
{req.phone}
</div>
<div className="flex items-start gap-2 text-gray-300">
<MessageCircle className="w-4 h-4 text-gray-400 mt-1" />
<p className="text-sm leading-relaxed">{req.message}</p>
</div>
<Button
variant="outline"
size="sm"
className="mt-3 w-full border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
onClick={() => setSelected(req)}
>
Batafsil korish
</Button>
</CardContent>
</Card>
))}
</div>
{/* Modal (Dialog) */}
<Dialog open={!!selected} onOpenChange={() => setSelected(null)}>
<DialogContent className="bg-gray-800 border border-gray-700 text-gray-100 sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl font-semibold text-white flex items-center gap-2">
<User className="w-5 h-5 text-blue-400" />
{selected?.name}
</DialogTitle>
</DialogHeader>
<div className="space-y-3 mt-2">
<div className="flex items-center gap-2 text-gray-400">
<Phone className="w-4 h-4" />
{selected?.phone}
</div>
<div className="flex items-start gap-2 text-gray-300">
<MessageCircle className="w-4 h-4 mt-1" />
<p className="text-sm leading-relaxed">{selected?.message}</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400">Status:</span>
<Badge
variant="outline"
className={`px-2 py-1 rounded-md text-xs font-medium ${
selected?.status === "Pending"
? "bg-red-500/10 text-red-400 border-red-400/40"
: "bg-green-500/10 text-green-400 border-green-400/40"
}`}
>
{selected?.status === "Pending" ? "Kutilmoqda" : "Yakunlangan"}
</Badge>
</div>
</div>
<DialogFooter className="flex justify-end mt-4">
<Button
variant="outline"
className="border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
onClick={() => setSelected(null)}
>
Yopish
</Button>
{selected && (
<Button
onClick={() => handleToggleStatus(selected.id)}
className={`${
selected.status === "Pending"
? "bg-green-600 hover:bg-green-700"
: "bg-red-600 hover:bg-red-700"
} text-white`}
>
{selected.status === "Pending"
? "Yakunlandi deb belgilash"
: "Kutilmoqda deb belgilash"}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default SupportTours;

View File

@@ -0,0 +1,316 @@
"use client";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { Edit, Plus, Trash } from "lucide-react";
import { useEffect, useState } from "react";
type MapClickEvent = {
get: (key: "coords") => [number, number];
};
type ContactInfo = {
telegram?: string;
instagram?: string;
facebook?: string;
twiter?: string;
linkedin?: string;
address?: string;
email?: string;
phonePrimary?: string;
phoneSecondary?: string;
};
const STORAGE_KEY = "site_contact_info";
async function getAddressFromCoords(lat: number, lon: number) {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`,
);
const data = await response.json();
return data.display_name || "";
}
export default function ContactSettings() {
const [contact, setContact] = useState<ContactInfo | null>(null);
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [form, setForm] = useState<ContactInfo>({});
const [coords, setCoords] = useState({
latitude: 41.311081,
longitude: 69.240562,
}); // Toshkent default
// Load saved contact
useEffect(() => {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) setContact(JSON.parse(raw));
}, []);
// Populate form when editing
useEffect(() => {
if (open && editing && contact) setForm(contact);
if (!open && !editing) setForm({});
}, [open, editing, contact]);
const handleChange = <K extends keyof ContactInfo>(
key: K,
value: ContactInfo[K],
) => {
setForm((s) => ({ ...s, [key]: value }));
};
const handleMapClick = async (e: MapClickEvent) => {
const lat = e.get("coords")[0];
const lon = e.get("coords")[1];
setCoords({ latitude: lat, longitude: lon });
const addressName = await getAddressFromCoords(lat, lon);
setForm((s) => ({ ...s, address: addressName }));
};
const saveContact = () => {
if (!form.email && !form.phonePrimary) {
alert("Iltimos email yoki telefon kiriting");
return;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
setContact(form);
setOpen(false);
setEditing(false);
};
const startAdd = () => {
setForm({});
setEditing(false);
setOpen(true);
};
const startEdit = () => {
setEditing(true);
setOpen(true);
};
const removeContact = () => {
if (!confirm("Contact ma'lumotlarini o'chirishni xohlaysizmi?")) return;
localStorage.removeItem(STORAGE_KEY);
setContact(null);
};
return (
<div className="w-full mx-auto p-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-gray-100">
Contact settings
</h2>
{!contact && (
<Button onClick={startAdd} className="flex items-center gap-2">
<Plus size={16} /> Qo'shish
</Button>
)}
</div>
{!contact ? (
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>Hozircha kontakt ma'lumotlari qo'shilmagan</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Sayt uchun telegram, instagram, manzil, email va telefonni bu
yerda saqlang. Siz faqat bir marta qo'sha olasiz keyin
tahrirlash mumkin.
</p>
<div className="mt-4">
<Button onClick={startAdd} className="flex items-center gap-2">
<Plus size={14} /> Qo'shish
</Button>
</div>
</CardContent>
</Card>
) : (
<Card className="bg-gray-900">
<CardHeader className="flex items-center justify-between">
<CardTitle>Kontakt ma'lumotlari</CardTitle>
<div className="flex gap-2">
<Button
variant="outline"
onClick={startEdit}
className="flex items-center gap-2"
>
<Edit size={14} /> Tahrirlash
</Button>
<Button
variant="destructive"
onClick={removeContact}
className="flex items-center gap-2"
>
<Trash size={14} /> O'chirish
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Telegram</div>
<div className="text-sm">{contact.telegram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Instagram</div>
<div className="text-sm">{contact.instagram || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Facebook</div>
<div className="text-sm">{contact.facebook || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">LinkedIn</div>
<div className="text-sm">{contact.linkedin || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Twitter</div>
<div className="text-sm">{contact.twiter || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Manzil</div>
<div className="text-sm">{contact.address || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Email</div>
<div className="text-sm">{contact.email || "—"}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Telefonlar</div>
<div className="text-sm">
{contact.phonePrimary || "—"}
{contact.phoneSecondary ? ` • ${contact.phoneSecondary}` : ""}
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* Dialog */}
<Dialog
open={open}
onOpenChange={(v) => {
setOpen(v);
if (!v) setEditing(false);
}}
>
<DialogContent className="h-[90%] overflow-y-scroll">
<DialogHeader>
<DialogTitle>
{editing ? "Kontaktni tahrirlash" : "Kontakt qo'shish"}
</DialogTitle>
</DialogHeader>
{/* Map */}
<YMaps query={{ lang: "en_RU" }}>
<Map
defaultState={{
center: [coords.latitude, coords.longitude],
zoom: 13,
}}
width="100%"
height="400px"
onClick={handleMapClick}
>
<Placemark geometry={[coords.latitude, coords.longitude]} />
</Map>
</YMaps>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mt-4">
<div className="flex flex-col gap-2 sm:col-span-2">
<Label>Manzil</Label>
<Input
value={form.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Telegram</Label>
<Input
value={form.telegram || ""}
onChange={(e) => handleChange("telegram", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Instagram</Label>
<Input
value={form.instagram || ""}
onChange={(e) => handleChange("instagram", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Linkedin</Label>
<Input
value={form.linkedin || ""}
onChange={(e) => handleChange("linkedin", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Facebook</Label>
<Input
value={form.facebook || ""}
onChange={(e) => handleChange("facebook", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Twitter</Label>
<Input
value={form.twiter || ""}
onChange={(e) => handleChange("twiter", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Email</Label>
<Input
value={form.email || ""}
onChange={(e) => handleChange("email", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label>Asosiy telefon</Label>
<Input
value={form.phonePrimary || ""}
onChange={(e) => handleChange("phonePrimary", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2 w-full">
<Label>Qo'shimcha telefon</Label>
<Input
value={form.phoneSecondary || ""}
onChange={(e) => handleChange("phoneSecondary", e.target.value)}
/>
</div>
</div>
<DialogFooter className="mt-6 flex justify-end gap-2">
<Button
variant="ghost"
onClick={() => {
setOpen(false);
setEditing(false);
}}
>
Bekor qilish
</Button>
<Button onClick={saveContact}>Saqlash</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,90 @@
// src/models/tour.ts
// Ilovada foydalaniladigan barcha modellar
export type TicketAmenity = {
name: string;
icon_name: string; // Misol uchun: "wifi", "pool"
};
export type IncludedService = {
image: string;
title: string;
description: string;
};
export type HotelMeal = {
image: string;
name: string;
description: string;
};
export type HotelData = {
name: string; // Joylashadigan manzil nomi
rating: number; // Yulduz
meal_plan: string; // Ovqatlanish rejasi
// Bu malumotlar API orqali keladi deb faraz qilinadi
hotel_type_id: number;
hotel_amenity_ids: number[];
hotel_feature_ids: number[];
};
export type TourFormData = {
// Etap 1: Asosiy Tur Ma'lumotlari
title: string;
price: number;
departure: string;
destination: string;
departure_time: string;
travel_time: string;
passenger_count: number;
languages: string;
duration_days: number;
hotel_meals_option: string; // 'nonushta', 'yarmpansion'
tariff: string; // 'econom', 'business'
badge: string; // 'hot', 'new'
visa_required: boolean;
banner_image: File[];
transport: string; // 'airplane', 'bus'
// Qo'shimcha Modellar
tickets_images: string[]; // Bilet rasmlari URL massivi
tickets_amenities: TicketAmenity[];
tickets_included_services: IncludedService[];
tickets_hotel_meals: HotelMeal[];
// Etap 2: Mehmonxona Ma'lumotlari (yoki uni qismi)
hotel_info: string; // Asosiy matn
hotel_data: HotelData;
};
// Forma holatini boshqarish uchun boshlang'ich qiymat
export const initialTourData: TourFormData = {
title: "",
price: 0,
departure: "",
destination: "",
departure_time: "",
travel_time: "",
passenger_count: 1,
languages: "O'zbek, Rus",
duration_days: 1,
hotel_meals_option: "Nonushta",
tariff: "Econom",
badge: "New",
visa_required: false,
banner_image: [],
transport: "Samolyot",
tickets_images: [],
tickets_amenities: [{ name: "Wi-Fi", icon_name: "Wifi" }],
tickets_included_services: [],
tickets_hotel_meals: [],
hotel_info: "",
hotel_data: {
name: "",
rating: 5,
meal_plan: "Nonushta",
hotel_type_id: 1,
hotel_amenity_ids: [],
hotel_feature_ids: [],
},
};

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;

354
src/pages/users/Create.tsx Normal file
View File

@@ -0,0 +1,354 @@
import formatPhone from "@/shared/lib/formatPhone";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import clsx from "clsx";
import {
ArrowLeft,
Eye,
EyeOff,
Lock,
Mail,
Phone,
Sparkles,
User,
} from "lucide-react";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
export default function CreateUser() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "+998",
password: "",
confirmPassword: "",
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = "Username majburiy";
} else if (formData.username.length < 3) {
newErrors.username =
"Username kamida 3 ta belgidan iborat bo'lishi kerak";
}
if (!formData.email.trim() && !formData.phone.trim()) {
newErrors.contact =
"Email yoki telefon raqamlardan kamida bittasi kiritilishi kerak";
}
if (
formData.email.trim() &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)
) {
newErrors.email = "Email formati noto'g'ri";
}
if (
formData.phone.trim() &&
!/^\+998\d{9}$/.test(formData.phone.replace(/\s/g, ""))
) {
newErrors.phone = "Telefon raqami formati: +998901234567";
}
if (!formData.password) {
newErrors.password = "Parol majburiy";
} else if (formData.password.length < 6) {
newErrors.password = "Parol kamida 6 ta belgidan iborat bo'lishi kerak";
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "Parollar mos kelmaydi";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
// const payload = {
// username: formData.username,
// email: formData.email || null,
// phone: formData.phone || null,
// password: formData.password,
// };
// navigate("/");
}
};
return (
<div className="min-h-screen bg-gradient-to-br w-full from-slate-950 via-slate-900 to-slate-950 p-4 md:p-8">
<div className="w-full mx-auto">
{/* Header */}
<div className="mb-8">
<Button
variant="ghost"
onClick={() => navigate("/")}
className="mb-4 -ml-2 text-slate-400 hover:text-slate-100 hover:bg-slate-800/50 cursor-pointer"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Orqaga
</Button>
<div className="flex items-center gap-3 mb-2">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center shadow-lg shadow-blue-500/20">
<Sparkles className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-100">
Yangi foydalanuvchi
</h1>
<p className="text-slate-400 mt-1">
Ma'lumotlarni to'ldiring va saqlang
</p>
</div>
</div>
</div>
{/* Form Card */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-3xl shadow-2xl border border-slate-800/50 overflow-hidden">
<div className="p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Username */}
<div className="space-y-2">
<Label
htmlFor="username"
className="text-slate-300 font-medium"
>
Username <span className="text-red-400">*</span>
</Label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
id="username"
value={formData.username}
onChange={(e) => {
setFormData((p) => ({ ...p, username: e.target.value }));
setErrors((p) => ({ ...p, username: "" }));
}}
placeholder="john_doe"
className={clsx(
"h-14 pl-12 pr-4 rounded-xl border-2 transition-all duration-200 bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.username
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-blue-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.username && (
<p className="text-sm text-red-400 flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-red-400"></span>
{errors.username}
</p>
)}
</div>
{/* Email */}
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-300 font-medium">
Email
</Label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => {
setFormData((p) => ({ ...p, email: e.target.value }));
setErrors((p) => ({ ...p, email: "", contact: "" }));
}}
placeholder="email@example.com"
className={clsx(
"h-14 pl-12 pr-4 rounded-xl border-2 transition-all duration-200 bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.email
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-blue-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.email && (
<p className="text-sm text-red-400 flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-red-400"></span>
{errors.email}
</p>
)}
</div>
{/* Phone */}
<div className="space-y-2">
<Label htmlFor="phone" className="text-slate-300 font-medium">
Telefon raqami
</Label>
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
id="phone"
type="tel"
value={formatPhone(formData.phone)}
onChange={(e) => {
setFormData((p) => ({ ...p, phone: e.target.value }));
setErrors((p) => ({ ...p, phone: "", contact: "" }));
}}
placeholder="+998 90 123 45 67"
className={clsx(
"h-14 pl-12 pr-4 rounded-xl border-2 transition-all duration-200 bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.phone
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-blue-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.phone && (
<p className="text-sm text-red-400 flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-red-400"></span>
{errors.phone}
</p>
)}
</div>
{/* Contact Error */}
{errors.contact && (
<div className="bg-amber-500/10 border-2 border-amber-500/30 rounded-xl p-4">
<p className="text-sm text-amber-400 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
{errors.contact}
</p>
</div>
)}
{/* Password */}
<div className="space-y-2">
<Label
htmlFor="password"
className="text-slate-300 font-medium"
>
Parol <span className="text-red-400">*</span>
</Label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
id="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData((p) => ({ ...p, password: e.target.value }));
setErrors((p) => ({ ...p, password: "" }));
}}
placeholder="••••••••"
className={clsx(
"h-14 pl-12 pr-12 rounded-xl border-2 transition-all duration-200 bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.password
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-blue-500 hover:border-slate-600/50",
)}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-10 w-10 hover:bg-slate-700/50 rounded-lg"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-slate-500" />
) : (
<Eye className="h-5 w-5 text-slate-500" />
)}
</Button>
</div>
{errors.password && (
<p className="text-sm text-red-400 flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-red-400"></span>
{errors.password}
</p>
)}
</div>
{/* Confirm Password */}
<div className="space-y-2">
<Label
htmlFor="confirmPassword"
className="text-slate-300 font-medium"
>
Parolni tasdiqlang <span className="text-red-400">*</span>
</Label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => {
setFormData((p) => ({
...p,
confirmPassword: e.target.value,
}));
setErrors((p) => ({ ...p, confirmPassword: "" }));
}}
placeholder="••••••••"
className={clsx(
"h-14 pl-12 pr-12 rounded-xl border-2 transition-all duration-200 bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.confirmPassword
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-blue-500 hover:border-slate-600/50",
)}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-10 w-10 hover:bg-slate-700/50 rounded-lg"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-slate-500" />
) : (
<Eye className="h-5 w-5 text-slate-500" />
)}
</Button>
</div>
{errors.confirmPassword && (
<p className="text-sm text-red-400 flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-red-400"></span>
{errors.confirmPassword}
</p>
)}
</div>
{/* Action Buttons */}
<div className="flex gap-3 pt-6">
<Button
type="button"
variant="outline"
onClick={() => navigate("/")}
className="flex-1 h-14 rounded-xl cursor-pointer border-2 border-slate-700/50 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 hover:text-slate-100 hover:border-slate-600/50 font-medium transition-all duration-200"
>
Bekor qilish
</Button>
<Button
type="submit"
className="flex-1 h-14 rounded-xl cursor-pointer bg-gradient-to-r from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800 text-white font-medium shadow-lg shadow-blue-500/20 hover:shadow-xl hover:shadow-blue-500/30 transition-all duration-200"
>
Saqlash
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}

220
src/pages/users/Edit.tsx Normal file
View File

@@ -0,0 +1,220 @@
import formatPhone from "@/shared/lib/formatPhone";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import clsx from "clsx";
import { ArrowLeft, Mail, Phone, Save, User } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
export default function EditUser() {
const navigate = useNavigate();
const { id } = useParams();
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "+998",
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
setFormData({
username: "john_doe",
email: "john@example.com",
phone: "+998901234567",
});
}, [id]);
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = "Username majburiy";
} else if (formData.username.length < 3) {
newErrors.username =
"Username kamida 3 ta belgidan iborat bo'lishi kerak";
}
if (!formData.email.trim() && !formData.phone.trim()) {
newErrors.contact = "Email yoki telefon raqami kiritilishi shart";
}
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "Email formati noto'g'ri";
}
if (
formData.phone &&
!/^\+998\d{9}$/.test(formData.phone.replace(/\s/g, ""))
) {
newErrors.phone = "Telefon raqami formati: +998901234567";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
navigate("/");
}
};
return (
<div className="min-h-screen w-full bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 p-4 md:p-8">
<div className="w-full mx-auto">
{/* Header */}
<div className="mb-6">
<Button
variant="ghost"
onClick={() => navigate(-1)}
className="mb-4 -ml-2 text-slate-400 cursor-pointer hover:text-slate-100 hover:bg-slate-800/50"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Orqaga
</Button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-700 flex items-center justify-center shadow-lg shadow-emerald-500/20">
<User className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-slate-100">Tahrirlash</h1>
<p className="text-slate-400 text-sm">Ma'lumotlarni yangilang</p>
</div>
</div>
</div>
{/* Form Card */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6 md:p-8">
<form onSubmit={handleSubmit} className="space-y-5">
{/* Username */}
<div className="space-y-2">
<Label
htmlFor="username"
className="text-slate-300 font-medium text-sm"
>
Username <span className="text-red-400">*</span>
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
id="username"
value={formData.username}
onChange={(e) => {
setFormData((p) => ({ ...p, username: e.target.value }));
setErrors((p) => ({ ...p, username: "" }));
}}
placeholder="john_doe"
className={clsx(
"h-11 pl-10 rounded-lg border transition-colors bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.username
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-emerald-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.username && (
<p className="text-xs text-red-400">{errors.username}</p>
)}
</div>
{/* Email */}
<div className="space-y-2">
<Label
htmlFor="email"
className="text-slate-300 font-medium text-sm"
>
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => {
setFormData((p) => ({ ...p, email: e.target.value }));
setErrors((p) => ({ ...p, email: "", contact: "" }));
}}
placeholder="email@example.com"
className={clsx(
"h-11 pl-10 rounded-lg border transition-colors bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.email
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-emerald-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.email && (
<p className="text-xs text-red-400">{errors.email}</p>
)}
</div>
{/* Phone */}
<div className="space-y-2">
<Label
htmlFor="phone"
className="text-slate-300 font-medium text-sm"
>
Telefon raqami
</Label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
id="phone"
type="tel"
value={formatPhone(formData.phone)}
onChange={(e) => {
setFormData((p) => ({ ...p, phone: e.target.value }));
setErrors((p) => ({ ...p, phone: "", contact: "" }));
}}
placeholder="+998 90 123 45 67"
className={clsx(
"h-11 pl-10 rounded-lg border transition-colors bg-slate-800/50 text-slate-200 placeholder:text-slate-500",
errors.phone
? "border-red-500/50 focus:border-red-500 bg-red-500/10"
: "border-slate-700/50 focus:border-emerald-500 hover:border-slate-600/50",
)}
/>
</div>
{errors.phone && (
<p className="text-xs text-red-400">{errors.phone}</p>
)}
</div>
{/* Contact Error */}
{errors.contact && (
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3">
<p className="text-xs text-amber-400">{errors.contact}</p>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => navigate(-1)}
className="flex-1 h-11 rounded-lg border-slate-700/50 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 hover:text-slate-100 font-medium cursor-pointer"
>
Bekor qilish
</Button>
<Button
type="submit"
className="flex-1 h-11 rounded-lg bg-gradient-to-r cursor-pointer text-md from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 text-white font-medium shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 transition-all"
>
<Save className="!w-5 !h-5 mr-2" />
Yangilash
</Button>
</div>
</form>
</div>
</div>
</div>
);
}

398
src/pages/users/User.tsx Normal file
View File

@@ -0,0 +1,398 @@
import { Button } from "@/shared/ui/button";
import {
AlertTriangle,
Calendar,
ChevronLeft,
ChevronRight,
Eye,
Mail,
Pencil,
Phone,
Plus,
Search,
Trash2,
Users,
X,
} from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
type User = {
id: number;
username: string;
email?: string;
phone?: string;
createdAt: string;
};
export default function UserList() {
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const usersPerPage = 6;
const navigate = useNavigate();
const [users, setUsers] = useState<User[]>([
{
id: 1,
username: "john_doe",
email: "john@example.com",
createdAt: "2024-01-15",
},
{
id: 2,
username: "jane_smith",
phone: "+998907654321",
createdAt: "2024-01-20",
},
{
id: 3,
username: "ali_karimov",
phone: "+998909876543",
createdAt: "2024-02-01",
},
{
id: 4,
username: "sara_johnson",
email: "sara@example.com",
phone: "+998901234567",
createdAt: "2024-02-10",
},
{
id: 5,
username: "murod_toshev",
email: "murod@example.com",
createdAt: "2024-02-15",
},
{
id: 6,
username: "aziza_sobirova",
email: "aziza@example.com",
createdAt: "2024-03-01",
},
{
id: 7,
username: "timur_ergashev",
phone: "+998907777777",
createdAt: "2024-03-10",
},
{
id: 8,
username: "odil_akbarov",
email: "odil@example.com",
createdAt: "2024-03-12",
},
{
id: 9,
username: "lola_nazarova",
phone: "+998909111222",
createdAt: "2024-04-05",
},
{
id: 10,
username: "bahrom_tursunov",
email: "bahrom@example.com",
createdAt: "2024-04-10",
},
]);
const [confirmDelete, setConfirmDelete] = useState<User | null>(null);
const handleDelete = (id: number) => {
setUsers((prev) => prev.filter((u) => u.id !== id));
setConfirmDelete(null);
};
const formatPhone = (phone: string) => {
if (phone.startsWith("+998")) {
return phone.replace(
/(\+998)(\d{2})(\d{3})(\d{2})(\d{2})/,
"$1 $2 $3 $4 $5",
);
}
return phone;
};
const filteredUsers = users.filter(
(user) =>
user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.phone?.includes(searchQuery),
);
const totalPages = Math.ceil(filteredUsers.length / usersPerPage);
const startIndex = (currentPage - 1) * usersPerPage;
const paginatedUsers = filteredUsers.slice(
startIndex,
startIndex + usersPerPage,
);
const getInitials = (username: string) => username.slice(0, 2).toUpperCase();
const getAvatarGradient = (id: number) => {
const gradients = ["from-blue-600 to-cyan-500"];
return gradients[id % gradients.length];
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 py-12 px-4 w-full">
<div className="max-w-[90%] mx-auto">
<div className="flex justify-between items-center">
<div className="mb-12">
<div className="flex items-center gap-3 mb-3">
<div className="p-3 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl shadow-lg shadow-cyan-500/20">
<Users className="w-8 h-8 text-white" />
</div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 via-cyan-300 to-blue-400 bg-clip-text text-transparent">
Foydalanuvchilar
</h1>
</div>
<p className="text-slate-400 text-lg ml-14">
Jami {users.length} ta foydalanuvchini boshqaring
</p>
</div>
<Button
onClick={() => navigate("/users/create")}
className="py-5 px-4 bg-gradient-to-br text-white cursor-pointer from-blue-500 to-cyan-500 rounded-xl shadow-lg shadow-cyan-500/20"
>
<Plus />
<p>Foydalanuvchi Qo'shish</p>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-12">
<StatCard
title="Jami foydalanuvchilar"
value={users.length.toString()}
icon={<Users className="w-6 h-6" />}
gradient="from-blue-600 to-blue-400"
/>
<StatCard
title="Email bilan ro'yxatlangan"
value={users.filter((u) => u.email).length.toString()}
icon={<Mail className="w-6 h-6" />}
gradient="from-cyan-600 to-cyan-400"
/>
<StatCard
title="Telefon bilan ro'yxatlangan"
value={users.filter((u) => u.phone).length.toString()}
icon={<Phone className="w-6 h-6" />}
gradient="from-purple-600 to-pink-400"
/>
</div>
<div className="mb-10 relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-cyan-600/10 rounded-2xl blur-xl" />
<div className="relative bg-slate-800/50 border border-slate-700/50 rounded-2xl p-6 backdrop-blur-sm">
<Search className="absolute left-8 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder="Username, email yoki telefon raqami bo'yicha qidirish..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
className="w-full pl-14 pr-4 py-3 bg-slate-700/30 border border-slate-600/50 text-white placeholder-slate-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
{paginatedUsers.map((user) => (
<div className="group relative hover:scale-105 transition-transform duration-300">
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-cyan-600/20 rounded-2xl blur-xl group-hover:blur-2xl transition-all opacity-0 group-hover:opacity-100" />
<div className="relative h-full flex flex-col justify-between bg-gradient-to-br from-slate-800 to-slate-900 border border-slate-700/50 rounded-2xl p-6 shadow-2xl hover:border-slate-600/70 transition-all backdrop-blur-sm">
<div>
<div className="flex items-center gap-4 mb-4">
<div
className={`w-16 h-16 rounded-xl bg-gradient-to-br ${getAvatarGradient(
user.id,
)} flex items-center justify-center text-white text-xl font-bold shadow-lg`}
>
{getInitials(user.username)}
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-white truncate">
{user.username}
</h3>
<span className="inline-block mt-1 px-3 py-1 bg-green-500/20 text-green-300 text-xs font-semibold rounded-full border border-green-500/50">
Faol
</span>
</div>
</div>
<div className="space-y-3 pt-4 border-t border-slate-700/50">
{user.email && (
<div className="flex items-center gap-3">
<Mail className="w-5 h-5 text-cyan-400" />
<span className="text-slate-300 text-sm truncate">
{user.email}
</span>
</div>
)}
{user.phone && (
<div className="flex items-center gap-3">
<Phone className="w-5 h-5 text-cyan-400" />
<span className="text-slate-300 text-sm">
{formatPhone(user.phone)}
</span>
</div>
)}
<div className="flex items-center gap-3">
<Calendar className="w-5 h-5 text-cyan-400" />
<span className="text-slate-300 text-sm">
{user.createdAt}
</span>
</div>
</div>
</div>
<div className="flex gap-2 pt-4 mt-4 border-t border-slate-700/50">
<button
onClick={() => navigate(`/users/${user.id}/`)}
className="flex-1 flex cursor-pointer items-center justify-center gap-2 px-4 py-2.5 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 font-medium rounded-lg transition-all border border-blue-500/30 hover:border-blue-500/50"
>
<Eye className="w-4 h-4" />
Ko'rish
</button>
<button
onClick={() => navigate(`/users/${user.id}/edit`)}
className="flex-1 flex items-center cursor-pointer justify-center gap-2 px-4 py-2.5 bg-green-500/20 hover:bg-green-500/30 text-green-300 font-medium rounded-lg transition-all border border-green-500/30 hover:border-green-500/50"
>
<Pencil className="w-4 h-4" />
Tahrirlash
</button>
<button
onClick={() => setConfirmDelete(user)}
className="flex-1 flex items-center cursor-pointer justify-center gap-2 px-4 py-2.5 bg-red-500/20 hover:bg-red-500/30 text-red-300 font-medium rounded-lg transition-all border border-red-500/30 hover:border-red-500/50"
>
<Trash2 className="w-4 h-4" />
O'chirish
</button>
</div>
</div>
</div>
))}
</div>
<div className="flex justify-end gap-2">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(totalPages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
currentPage === i + 1
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
}`}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((p) => Math.min(p + 1, totalPages))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
{confirmDelete && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-gradient-to-br from-slate-800 to-slate-900 border border-slate-700/50 rounded-2xl shadow-2xl max-w-md w-full p-6 space-y-6">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-3 bg-red-500/20 rounded-xl border border-red-500/30">
<Trash2 className="w-6 h-6 text-red-400" />
</div>
<h2 className="text-xl font-bold text-white">
Foydalanuvchini o'chirish
</h2>
</div>
<button
onClick={() => setConfirmDelete(null)}
className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="py-4">
<p className="text-slate-300">
Siz{" "}
<span className="font-semibold text-white">
{confirmDelete.username}
</span>{" "}
foydalanuvchini o'chirmoqchimisiz?
</p>
<div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<p className="text-sm text-red-300 font-medium flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
<span>Ushbu amalni qaytarib bo'lmaydi.</span>
</p>
</div>
</div>
<div className="flex gap-3 pt-2">
<button
onClick={() => setConfirmDelete(null)}
className="flex-1 px-4 py-3 border border-slate-600 hover:bg-slate-700/50 text-slate-300 font-medium rounded-lg transition-all"
>
Bekor qilish
</button>
<button
onClick={() => handleDelete(confirmDelete.id)}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white font-medium rounded-lg transition-all shadow-lg"
>
<Trash2 className="w-4 h-4" />
<span>O'chirish</span>
</button>
</div>
</div>
</div>
)}
</div>
);
}
function StatCard({
title,
value,
icon,
gradient,
}: {
title: string;
value: string;
icon: React.ReactNode;
gradient: string;
}) {
return (
<div className="group relative hover:scale-105 transition-transform duration-300">
<div
className={`absolute inset-0 bg-gradient-to-r ${gradient} rounded-xl blur-lg opacity-20 group-hover:opacity-40 transition-all`}
/>
<div
className={`relative bg-gradient-to-br ${gradient} bg-opacity-10 border border-white/10 backdrop-blur-sm rounded-xl p-6 shadow-xl hover:shadow-2xl transition-all hover:border-white/20`}
>
<div className="flex justify-between items-start mb-3">
<p className="text-slate-300 text-sm font-medium">{title}</p>
<div
className={`bg-gradient-to-br ${gradient} p-2 rounded-lg text-white shadow-lg`}
>
{icon}
</div>
</div>
<p className="text-3xl font-bold text-white">{value}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,689 @@
import { Button } from "@/shared/ui/button";
import {
ArrowLeft,
Bus,
Calendar,
Clock,
CreditCard,
DollarSign,
Download,
Edit,
Mail,
MapPin,
Package,
Phone,
Shield,
Ticket,
User,
Users as UsersIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
type PassportImage = {
id: number;
image: string;
};
type Companion = {
id: number;
first_name: string;
last_name: string;
birth_date: string;
phone_number: string;
gender: "male" | "female";
participant_pasport_image: PassportImage[];
};
type Participant = {
id: number;
first_name: string;
last_name: string;
gender: "male" | "female";
};
type TicketInfo = {
id: number;
title: string;
service_name: string;
location_name: string;
};
type ExtraService = {
id: number;
name: string;
};
type ExtraPaidService = {
id: number;
name: string;
price: number;
};
type BookingTicket = {
id: number;
departure: string;
destination: string;
departure_date: string;
arrival_time: string;
participant: Participant[];
ticket: TicketInfo;
tariff: string;
transport: string;
extra_service: ExtraService[];
extra_paid_service: ExtraPaidService[];
total_price: number;
order_status: "pending_payment" | "confirmed" | "cancelled";
};
type UserData = {
id: number;
username: string;
email?: string;
phone?: string;
createdAt: string;
status: "active" | "inactive";
companions: Companion[];
bookings: BookingTicket[];
};
const UserDetail = () => {
const navigate = useNavigate();
const { id } = useParams();
const [user, setUser] = useState<UserData | null>(null);
useEffect(() => {
// Backend'dan ma'lumot olish
setUser({
id: Number(id),
username: "john_doe",
email: "john@example.com",
phone: "+998901234567",
createdAt: "2024-01-15",
status: "active",
companions: [
{
id: 1,
first_name: "Aziza",
last_name: "Karimova",
birth_date: "1995-05-20",
phone_number: "+998901111111",
gender: "female",
participant_pasport_image: [
{ id: 1, image: "/images/passport1.jpg" },
],
},
{
id: 2,
first_name: "Sardor",
last_name: "Toshev",
birth_date: "1990-08-15",
phone_number: "+998902222222",
gender: "male",
participant_pasport_image: [
{ id: 2, image: "/images/passport2.jpg" },
],
},
],
bookings: [
{
id: 1,
departure: "Toshkent",
destination: "Samarqand",
departure_date: "2024-06-20",
arrival_time: "2024-06-20T18:30:00",
participant: [
{ id: 1, first_name: "John", last_name: "Doe", gender: "male" },
],
ticket: {
id: 1,
title: "Premium Class",
service_name: "Express Service",
location_name: "Central Station",
},
tariff: "Standard",
transport: "Bus",
extra_service: [
{ id: 1, name: "Wi-Fi" },
{ id: 2, name: "Refreshments" },
],
extra_paid_service: [{ id: 1, name: "Extra Luggage", price: 50000 }],
total_price: 150000,
order_status: "confirmed",
},
{
id: 2,
departure: "Samarqand",
destination: "Buxoro",
departure_date: "2024-06-25",
arrival_time: "2024-06-25T15:00:00",
participant: [
{ id: 2, first_name: "Jane", last_name: "Smith", gender: "female" },
],
ticket: {
id: 2,
title: "Economy Class",
service_name: "Standard Service",
location_name: "Main Terminal",
},
tariff: "Economy",
transport: "Train",
extra_service: [{ id: 3, name: "AC" }],
extra_paid_service: [],
total_price: 120000,
order_status: "confirmed",
},
{
id: 3,
departure: "Samarqand",
destination: "Buxoro",
departure_date: "2024-06-25",
arrival_time: "2024-06-25T15:00:00",
participant: [
{ id: 2, first_name: "Jane", last_name: "Smith", gender: "female" },
],
ticket: {
id: 2,
title: "Economy Class",
service_name: "Standard Service",
location_name: "Main Terminal",
},
tariff: "Economy",
transport: "Train",
extra_service: [{ id: 3, name: "AC" }],
extra_paid_service: [],
total_price: 120000,
order_status: "confirmed",
},
],
});
}, [id]);
const formatPhone = (phone: string) => {
if (phone.startsWith("+998")) {
return phone.replace(
/(\+998)(\d{2})(\d{3})(\d{2})(\d{2})/,
"$1 $2 $3 $4 $5",
);
}
return phone;
};
const formatPrice = (price: number) => {
return price.toLocaleString("uz-UZ") + " so'm";
};
const getStatusBadge = (status: string) => {
const badges = {
confirmed:
"bg-emerald-500/20 text-emerald-400 border border-emerald-500/30",
pending_payment:
"bg-amber-500/20 text-amber-400 border border-amber-500/30",
cancelled: "bg-red-500/20 text-red-400 border border-red-500/30",
};
const labels = {
confirmed: "Tasdiqlangan",
pending_payment: "To'lov kutilmoqda",
cancelled: "Bekor qilingan",
};
return {
class: badges[status as keyof typeof badges],
label: labels[status as keyof typeof labels],
};
};
const handleDownloadPDF = (bookingId: number) => {
console.log("Downloading PDF for booking:", bookingId);
};
if (!user) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="mt-4 text-slate-400">Yuklanmoqda...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen w-full bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 p-4 md:p-8">
<div className="w-full mx-auto">
{/* Header */}
<div className="mb-6">
<Button
variant="ghost"
onClick={() => navigate("/")}
className="mb-4 -ml-2 text-slate-400 hover:text-slate-100 hover:bg-slate-800/50 cursor-pointer"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Orqaga
</Button>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center shadow-lg shadow-blue-500/20">
<User className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-100">
{user.username}
</h1>
<span
className={`inline-block mt-2 px-3 py-1 text-xs font-medium rounded-full border ${
user.status === "active"
? "bg-emerald-500/20 text-emerald-400 border-emerald-500/30"
: "bg-slate-700/50 text-slate-400 border-slate-600/30"
}`}
>
{user.status === "active" ? "Faol" : "Nofaol"}
</span>
</div>
</div>
<Button
onClick={() => navigate(`/users/${id}/edit`)}
className="bg-gradient-to-r cursor-pointer from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800 text-white shadow-lg shadow-blue-500/20"
>
<Edit className="w-4 h-4 mr-2" />
Tahrirlash
</Button>
</div>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Main Info */}
<div className="lg:col-span-2 space-y-6">
{/* Contact Information */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Mail className="w-5 h-5 text-blue-400" />
Aloqa ma'lumotlari
</h2>
<div className="space-y-4">
{user.email && (
<div className="flex items-start gap-3 p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center flex-shrink-0 border border-blue-500/30">
<Mail className="w-5 h-5 text-blue-400" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">
Email
</p>
<p className="text-slate-200 font-medium">{user.email}</p>
</div>
</div>
)}
{user.phone && (
<div className="flex items-start gap-3 p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="w-10 h-10 rounded-lg bg-emerald-500/20 flex items-center justify-center flex-shrink-0 border border-emerald-500/30">
<Phone className="w-5 h-5 text-emerald-400" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">
Telefon
</p>
<p className="text-slate-200 font-medium">
{formatPhone(user.phone)}
</p>
</div>
</div>
)}
</div>
</div>
{/* Account Information */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Shield className="w-5 h-5 text-indigo-400" />
Hisob ma'lumotlari
</h2>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center flex-shrink-0 border border-purple-500/30">
<User className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">
Username
</p>
<p className="text-slate-200 font-medium">
{user.username}
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center flex-shrink-0 border border-orange-500/30">
<Calendar className="w-5 h-5 text-orange-400" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">
Yaratilgan sana
</p>
<p className="text-slate-200 font-medium">
{user.createdAt}
</p>
</div>
</div>
</div>
</div>
{/* Booking Tickets */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Ticket className="w-5 h-5 text-emerald-400" />
Sotib olingan chiptalar
<span className="ml-auto text-sm font-normal text-slate-500">
{user.bookings.length} ta
</span>
</h2>
<div className="space-y-4 max-h-[600px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-700 hover:scrollbar-thumb-slate-600 scrollbar-track-transparent">
{user.bookings.length > 0 ? (
user.bookings.map((booking) => {
const statusBadge = getStatusBadge(booking.order_status);
return (
<div
key={booking.id}
className="p-5 bg-slate-800/50 rounded-xl border border-slate-700/50 hover:border-blue-500/50 transition-colors space-y-4"
>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<MapPin className="w-5 h-5 text-blue-400" />
<span className="font-semibold text-slate-100 text-lg">
{booking.departure} {booking.destination}
</span>
</div>
<span
className={`px-3 py-1 text-xs font-medium rounded-full ${statusBadge.class}`}
>
{statusBadge.label}
</span>
</div>
{/* Ticket Info */}
<div className="grid grid-cols-2 gap-3 text-sm bg-slate-900/50 p-3 rounded-lg border border-slate-700/30">
<div>
<p className="text-xs text-slate-500">
Chipta turi
</p>
<p className="text-slate-200 font-medium">
{booking.ticket.title}
</p>
</div>
<div>
<p className="text-xs text-slate-500">Xizmat</p>
<p className="text-slate-200 font-medium">
{booking.ticket.service_name}
</p>
</div>
<div>
<p className="text-xs text-slate-500">Manzil</p>
<p className="text-slate-200 font-medium">
{booking.ticket.location_name}
</p>
</div>
<div>
<p className="text-xs text-slate-500">Transport</p>
<p className="text-slate-200 font-medium flex items-center gap-1">
<Bus className="w-3 h-3" />
{booking.transport}
</p>
</div>
</div>
{/* Dates */}
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-500" />
<div>
<p className="text-xs text-slate-500">Jo'nash</p>
<p className="text-slate-200 font-medium">
{booking.departure_date}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-slate-500" />
<div>
<p className="text-xs text-slate-500">Yetish</p>
<p className="text-slate-200 font-medium">
{booking.arrival_time.split("T")[1].slice(0, 5)}
</p>
</div>
</div>
</div>
{/* Participants */}
<div>
<p className="text-xs text-slate-500 mb-2">
Yo'lovchilar:
</p>
<div className="flex flex-wrap gap-2">
{booking.participant.map((p) => (
<span
key={p.id}
className="px-3 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30"
>
{p.first_name} {p.last_name}
</span>
))}
</div>
</div>
{/* Services */}
{booking.extra_service.length > 0 && (
<div>
<p className="text-xs text-slate-500 mb-2">
Qo'shimcha xizmatlar:
</p>
<div className="flex flex-wrap gap-2">
{booking.extra_service.map((service) => (
<span
key={service.id}
className="px-2 py-1 bg-emerald-500/20 text-emerald-400 text-xs rounded-lg flex items-center gap-1 border border-emerald-500/30"
>
<Package className="w-3 h-3" />
{service.name}
</span>
))}
</div>
</div>
)}
{/* Paid Services */}
{booking.extra_paid_service.length > 0 && (
<div>
<p className="text-xs text-slate-500 mb-2">
Pullik xizmatlar:
</p>
<div className="space-y-1">
{booking.extra_paid_service.map((service) => (
<div
key={service.id}
className="flex items-center justify-between text-xs bg-amber-500/10 px-3 py-2 rounded-lg border border-amber-500/20"
>
<span className="text-slate-300">
{service.name}
</span>
<span className="font-medium text-amber-400">
{formatPrice(service.price)}
</span>
</div>
))}
</div>
</div>
)}
{/* Total & Actions */}
<div className="flex items-center justify-between pt-3 border-t border-slate-700/50">
<div className="flex items-center gap-2">
<DollarSign className="w-5 h-5 text-emerald-400" />
<div>
<p className="text-xs text-slate-500">
Jami narx
</p>
<p className="text-lg font-bold text-emerald-400">
{formatPrice(booking.total_price)}
</p>
</div>
</div>
<Button
onClick={() => handleDownloadPDF(booking.id)}
variant="outline"
className="flex items-center gap-2 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 border-slate-700/50 hover:border-slate-600/50"
>
<Download className="w-4 h-4" />
PDF yuklab olish
</Button>
</div>
</div>
);
})
) : (
<p className="text-sm text-slate-500 text-center py-4">
Hozircha chiptalar mavjud emas
</p>
)}
</div>
</div>
{/* Companions */}
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<UsersIcon className="w-5 h-5 text-purple-400" />
Hamrohlar
<span className="ml-auto text-sm font-normal text-slate-500">
{user.companions.length} ta
</span>
</h2>
<div className="space-y-4 max-h-[300px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-700 hover:scrollbar-thumb-slate-600 scrollbar-track-transparent">
{user.companions.length > 0 ? (
user.companions.map((companion) => (
<div
key={companion.id}
className="p-4 bg-slate-800/50 rounded-xl border border-slate-700/50"
>
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-lg bg-gradient-to-br from-purple-600 to-pink-600 flex items-center justify-center flex-shrink-0 shadow-lg shadow-purple-500/20">
<User className="w-7 h-7 text-white" />
</div>
<div className="flex-1 space-y-3">
<div>
<h3 className="font-semibold text-slate-100 text-lg">
{companion.first_name} {companion.last_name}
</h3>
<div className="flex items-center gap-2 mt-1">
<span
className={`px-2 py-1 text-xs rounded-full border ${
companion.gender === "male"
? "bg-blue-500/20 text-blue-400 border-blue-500/30"
: "bg-pink-500/20 text-pink-400 border-pink-500/30"
}`}
>
{companion.gender === "male" ? "Erkak" : "Ayol"}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-xs text-slate-500">
Tug'ilgan sana
</p>
<p className="text-slate-200 font-medium">
{companion.birth_date}
</p>
</div>
<div>
<p className="text-xs text-slate-500">Telefon</p>
<p className="text-slate-200 font-medium">
{formatPhone(companion.phone_number)}
</p>
</div>
</div>
{companion.participant_pasport_image.length > 0 && (
<div>
<p className="text-xs text-slate-500 mb-2">
Passport rasmlari:
</p>
<div className="flex gap-2">
{companion.participant_pasport_image.map(
(img) => (
<div
key={img.id}
className="w-20 h-20 rounded-lg bg-slate-700/50 flex items-center justify-center overflow-hidden border border-slate-600/50"
>
<CreditCard className="w-8 h-8 text-slate-500" />
</div>
),
)}
</div>
</div>
)}
</div>
</div>
</div>
))
) : (
<p className="text-sm text-slate-500 text-center py-4">
Hozircha hamrohlar qo'shilmagan
</p>
)}
</div>
</div>
</div>
{/* Right Column - Stats */}
<div className="space-y-6">
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-800/50 p-6">
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-emerald-400" />
Statistika
</h2>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-emerald-500/10 rounded-lg border border-emerald-500/20">
<span className="text-sm text-slate-300">Chiptalar</span>
<span className="text-sm font-semibold text-emerald-400">
{user.bookings.length} ta
</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-500/10 rounded-lg border border-purple-500/20">
<span className="text-sm text-slate-300">Hamrohlar</span>
<span className="text-sm font-semibold text-purple-400">
{user.companions.length} ta
</span>
</div>
<div className="flex items-center justify-between p-3 bg-blue-500/10 rounded-lg border border-blue-500/20">
<span className="text-sm text-slate-300">Status</span>
<span className="text-sm font-semibold text-blue-400">
{user.status === "active" ? "Faol" : "Nofaol"}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-orange-500/10 rounded-lg border border-orange-500/20">
<span className="text-sm text-slate-300">ID</span>
<span className="text-sm font-semibold text-orange-400">
#{user.id}
</span>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-blue-600 to-indigo-700 rounded-2xl shadow-xl shadow-blue-500/20 p-6 text-white border border-blue-500/20">
<h3 className="font-semibold mb-2">Qo'shimcha ma'lumot</h3>
<p className="text-sm text-blue-100">
Bu foydalanuvchi hozirda tizimda faol holatda. Barcha
ma'lumotlar to'liq va tasdiqlangan.
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default UserDetail;