connected api with profile, order

This commit is contained in:
Jelaletdin12
2025-11-15 16:14:01 +05:00
parent 21b9e88c5c
commit f867896817
70 changed files with 2370 additions and 2317 deletions

View File

@@ -1,38 +0,0 @@
"use client"
import Image, { type StaticImageData } from "next/image"
import { Swiper, SwiperSlide } from "swiper/react"
import { Autoplay } from "swiper/modules"
import "swiper/css"
type CarouselItem = {
title: string
image: StaticImageData | string
url?: string | null
}
export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
return (
<section className="rounded-2xl overflow-hidden">
<Swiper
modules={[Autoplay]}
slidesPerView={1}
loop
autoplay={{ delay: 3000, disableOnInteraction: false }}
>
{items.map((item, i) => (
<SwiperSlide key={i}>
<div className="relative w-full h-[200px] sm:h-[300px] md:h-[420px]">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
priority={i === 0}
/>
</div>
</SwiperSlide>
))}
</Swiper>
</section>
)
}

View File

@@ -1,67 +0,0 @@
"use client"
import Image from "next/image"
import Link from "next/link"
import { Card, CardContent } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import type { Category } from "@/lib/types/api"
type Props = {
categories: Category[] | undefined
isLoading: boolean
isError: boolean
locale: string
title: string
}
export default function CategoryGrid({ categories, isLoading, isError, locale, title }: Props) {
if (isError) {
return (
<section className="bg-white rounded-2xl shadow-sm p-6">
<h2 className="text-xl font-semibold mb-4">{title}</h2>
<p className="text-red-600">Failed to load categories. Please try again.</p>
</section>
)
}
if (isLoading) {
return (
<section className="bg-white rounded-2xl shadow-sm p-6">
<h2 className="text-xl font-semibold mb-4">{title}</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="w-full h-36 rounded-lg" />
<Skeleton className="w-full h-4 rounded" />
</div>
))}
</div>
</section>
)
}
return (
<section className="bg-white rounded-2xl shadow-sm p-6">
<h2 className="text-xl font-semibold mb-4">{title}</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{categories?.map((cat) => (
<Link key={cat.id} href={`/${locale}/category/${cat.slug}?category_id=${cat.id}`}>
<Card className="hover:shadow-md border-none shadow-none p-0 gap-2 transition-all cursor-pointer">
<div className="relative w-full h-36 overflow-hidden rounded-lg">
<Image
src={cat.media?.[0]?.images_400x400 || "/placeholder.svg"}
alt={cat.name}
fill
className="object-contain"
/>
</div>
<CardContent className="py-2">
<p className="text-sm font-medium text-gray-800 truncate text-center">{cat.name}</p>
</CardContent>
</Card>
</Link>
))}
</div>
</section>
)
}

View File

@@ -1,182 +0,0 @@
"use client"
import { useLocale, useTranslations } from "next-intl"
import { useEffect, useState, useCallback } from "react"
import InfiniteScroll from "react-infinite-scroll-component"
import HeroCarousel from "./Carousel"
import CategoryGrid from "./CategoryGrid"
import CollectionSection from "./ProductGrid"
import { useCategories, useCarousels, useCollections } from "@/lib/hooks"
import type { Collection } from "@/lib/types/api"
export default function HomePage() {
const locale = useLocale()
const t = useTranslations("common")
const [mounted, setMounted] = useState(false)
const [visibleCollections, setVisibleCollections] = useState<Collection[]>([])
const [hasMore, setHasMore] = useState(true)
const itemsPerPage = 10
const { data: categories, isLoading: categoriesLoading, isError: categoriesError } = useCategories()
const { data: carousels, isLoading: carouselsLoading } = useCarousels()
const { data: collections, isLoading: collectionsLoading, isError: collectionsError } = useCollections()
useEffect(() => setMounted(true), [])
// Initialize visible collections when data first loads
useEffect(() => {
console.log("=== Collections Data Change ===")
console.log("Collections:", collections)
console.log("Collections length:", collections?.length)
console.log("Visible collections length:", visibleCollections.length)
if (collections && collections.length > 0 && visibleCollections.length === 0) {
console.log("🟢 Initializing first batch of collections")
const initial = collections.slice(0, itemsPerPage)
console.log("Initial collections to show:", initial.length)
setVisibleCollections(initial)
setHasMore(collections.length > itemsPerPage)
console.log("Has more after init:", collections.length > itemsPerPage)
}
}, [collections, visibleCollections.length])
const loadMoreCollections = useCallback(() => {
console.log("=== loadMoreCollections Called ===")
console.log("Collections available:", collections?.length)
console.log("Visible collections:", visibleCollections.length)
console.log("Has more:", hasMore)
if (!collections) {
console.log("❌ No collections data")
return
}
const currentLength = visibleCollections.length
const nextCollections = collections.slice(
currentLength,
currentLength + itemsPerPage
)
console.log("Current length:", currentLength)
console.log("Next batch size:", nextCollections.length)
console.log("Next batch:", nextCollections.map(c => c.id))
if (nextCollections.length > 0) {
console.log("🟢 Adding", nextCollections.length, "more collections")
setVisibleCollections((prev) => {
const updated = [...prev, ...nextCollections]
console.log("Updated visible collections count:", updated.length)
return updated
})
} else {
console.log("⚠️ No more collections to load")
}
// Check if we've loaded all collections
const newTotal = currentLength + nextCollections.length
const shouldHaveMore = newTotal < collections.length
console.log("New total:", newTotal, "/ Total available:", collections.length)
console.log("Should have more:", shouldHaveMore)
if (!shouldHaveMore) {
console.log("🔴 Setting hasMore to false")
setHasMore(false)
}
}, [collections, visibleCollections.length, itemsPerPage, hasMore])
useEffect(() => {
console.log("=== State Update ===")
console.log("Visible collections count:", visibleCollections.length)
console.log("Has more:", hasMore)
}, [visibleCollections.length, hasMore])
if (!mounted) return <div className="p-8">Loading...</div>
// Transform carousel data to match component props
const carouselItems = carousels?.map(carousel => ({
title: carousel.title || "",
image: carousel.image || carousel.thumbnail,
url: carousel.link || null
})) || []
console.log("=== Render ===")
console.log("Collections loading:", collectionsLoading)
console.log("Visible collections for render:", visibleCollections.length)
console.log("Has more for InfiniteScroll:", hasMore)
return (
<div className="px-4 md:px-8 lg:px-12 pt-8 pb-12 space-y-8">
{/* Hero Carousel with API data */}
{!carouselsLoading && carouselItems.length > 0 && (
<HeroCarousel items={carouselItems} />
)}
{/* Categories Grid */}
<CategoryGrid
categories={categories}
isLoading={categoriesLoading}
isError={categoriesError}
locale={locale}
title={t("categories")}
/>
{/* Collections Sections with Infinite Scroll */}
{collectionsError ? (
<section className="bg-white rounded-2xl shadow-sm p-6">
<p className="text-red-600">Failed to load collections. Please try again.</p>
</section>
) : collectionsLoading ? (
<div className="space-y-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl shadow-sm p-6">
<div className="h-8 bg-gray-200 rounded w-48 mb-4 animate-pulse" />
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, j) => (
<div key={j} className="h-64 bg-gray-200 rounded-lg animate-pulse" />
))}
</div>
</div>
))}
</div>
) : (
<>
<div className="bg-yellow-100 border border-yellow-400 rounded p-4 text-sm">
<strong>Debug Info:</strong><br/>
Total Collections: {collections?.length || 0}<br/>
Visible: {visibleCollections.length}<br/>
Has More: {hasMore ? "Yes" : "No"}
</div>
<InfiniteScroll
dataLength={visibleCollections.length}
next={loadMoreCollections}
hasMore={hasMore}
loader={
<div className="text-center py-8 bg-blue-50 border-2 border-blue-200 rounded">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
<p className="text-gray-500 mt-2 font-bold">Loading more collections...</p>
</div>
}
endMessage={
<div className="text-center py-8 bg-green-50 border-2 border-green-200 rounded">
<p className="text-gray-600 font-bold"> You've reached the end</p>
</div>
}
scrollThreshold={0.8}
>
<div className="space-y-8">
{visibleCollections.map((collection, index) => (
<div key={collection.id}>
<div className="text-xs text-gray-400 mb-2">Collection #{index + 1}</div>
<CollectionSection
collection={collection}
locale={locale}
/>
</div>
))}
</div>
</InfiniteScroll>
</>
)}
</div>
)
}

View File

@@ -1,112 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronRight } from "lucide-react"
import ProductCard from "@/components/ProductCard"
import { Skeleton } from "@/components/ui/skeleton"
import { useCollectionProducts } from "@/lib/hooks"
import type { Collection } from "@/lib/types/api"
type Props = {
collection: Collection
locale: string
}
export default function CollectionSection({ collection, locale }: Props) {
const router = useRouter()
const [shouldRender, setShouldRender] = useState(true)
const {
data: productsData,
isLoading,
isError
} = useCollectionProducts(collection.id, { enabled: shouldRender })
// Determine if section should render based on products
useEffect(() => {
if (!isLoading && productsData) {
const hasProducts = productsData.data && productsData.data.length > 0
setShouldRender(hasProducts)
}
}, [isLoading, productsData])
// Don't render if no products after loading
if (!isLoading && (!productsData?.data || productsData.data.length === 0)) {
return null
}
const handleTitleClick = () => {
router.push(`/${locale}/collections/${collection.id}`)
}
// Show skeleton while loading
if (isLoading) {
return (
<section className="bg-white rounded-2xl shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-6 w-6 rounded-full" />
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="w-full h-64 rounded-lg" />
))}
</div>
</section>
)
}
// Show error state
if (isError) {
return null // Silently skip errored collections
}
// Slice to show only first 4 products
const displayProducts = productsData?.data.slice(0, 4) || []
return (
<section className="bg-white rounded-2xl shadow-sm p-6">
<div
className="flex items-center justify-between mb-4 cursor-pointer group"
onClick={handleTitleClick}
>
<h2 className="text-xl font-semibold group-hover:text-blue-600 transition-colors">
{collection.name}
</h2>
<ChevronRight className="w-6 h-6 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all" />
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{displayProducts.map((product) => {
// Extract first media image or use placeholder
const firstImage = product.media?.[0]?.images_800x800 ||
product.media?.[0]?.images_720x720 ||
product.media?.[0]?.thumbnail ||
"/placeholder-product.jpg"
// Format price
const formattedPrice = product.price_amount
? `${parseFloat(product.price_amount).toFixed(2)} TMT`
: "Price not available"
return (
<ProductCard
key={product.id}
id={product.id}
name={product.name}
price={product.price_amount ? parseFloat(product.price_amount) : null}
struct_price_text={formattedPrice}
images={[firstImage]}
is_favorite={false}
labels={[]}
price_color="#111"
height={360}
width={250}
button={false}
/>
)
})}
</div>
</section>
)
}

View File

@@ -1,56 +0,0 @@
import temp1 from "@/public/temp1.jpg"
import temp2 from "@/public/temp2.jpg"
import temp3 from "@/public/temp3.jpg"
import jbl from "@/public/jbl.png"
import jbll from "@/public/jbll.png"
import jbl3 from "@/public/jbl3.webp"
import jb from "@/public/jb.webp"
export const carouselItems = [
{ title: "Banner 1", image: temp1, url: "#" },
{ title: "Banner 2", image: temp2, url: "#" },
{ title: "Banner 3", image: temp3, url: "#" },
]
export const categories = [
{ id: 1, slug: "sneakers", name: "Sneakers", image: jbl },
{ id: 2, slug: "boots", name: "Boots", image: jbl3 },
{ id: 3, slug: "sandals", name: "Sandals", image: jbll },
{ id: 4, slug: "heels", name: "Heels", image: jb },
]
export const products = [
{
id: 1,
name: "Nike Air Max 270",
struct_price_text: "$120",
price: 120,
images: [jb, jbll, jbl, jbl3],
is_favorite: false,
labels: [{ text: "New", bg_color: "#10B981" }],
},
{
id: 2,
name: "Adidas Ultraboost",
struct_price_text: "$150",
price: 150,
images: [jbll, jb, jbl, jbl3],
is_favorite: true,
},
{
id: 3,
name: "Puma RS-X",
struct_price_text: "$110",
price: 110,
images: [jbl3, jbll, jbl, jb],
is_favorite: false,
},
{
id: 4,
name: "New Balance 327",
struct_price_text: "$130",
price: 130,
images: [jbl, jbll, jb, jbl3],
is_favorite: false,
},
]

View File

@@ -3,18 +3,24 @@
import { useState, useEffect } from "react"
import Link from "next/link"
import Image from "next/image"
import { X, Menu, Search, Store } from "lucide-react"
import { X, Menu, Search, Store, LogOut, User as UserIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import Logo from "@/public/logo.png"
import CategoryMenu from "./ui/CategoryMenu"
import SearchBar from "./ui/SearchBar"
import AuthDialog from "./ui/AuthDialog"
import ActionButtons from "./ui/ActionButtons"
import LanguageSelector from "./ui/LanguageSelector"
import { useAuthStatus, useLogout } from "@/lib/hooks/useAuth"
interface HeaderProps {
locale?: string
isAuthenticated?: boolean
translations?: {
catalog: string
search: string
@@ -27,8 +33,17 @@ interface HeaderProps {
phone: string
code: string
send: string
verify: string
sending: string
verifying: string
enterPhone: string
weWillSendCode: string
invalidPhone: string
invalidCode: string
loginSuccess: string
codeSent: string
logout: string
loggingOut: string
}
}
@@ -44,17 +59,29 @@ const DEFAULT_TRANSLATIONS = {
phone: "Номер телефона",
code: "Код",
send: "Отправить",
verify: "Подтвердить",
sending: "Отправка...",
verifying: "Проверка...",
enterPhone: "Введите свой номер телефона",
weWillSendCode: "Мы вышлем вам код",
invalidPhone: "Неверный номер телефона",
invalidCode: "Неверный код",
loginSuccess: "Вход выполнен успешно",
codeSent: "Код отправлен на ваш номер",
logout: "Выйти",
loggingOut: "Выход...",
}
export default function Header({ locale = "ru", isAuthenticated = false, translations }: HeaderProps) {
export default function Header({ locale = "ru", translations }: HeaderProps) {
const [isClient, setIsClient] = useState(false)
const [isCategoryOpen, setIsCategoryOpen] = useState(false)
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false)
const [isLoginOpen, setIsLoginOpen] = useState(false)
const t = translations || DEFAULT_TRANSLATIONS
const t = { ...DEFAULT_TRANSLATIONS, ...translations }
const { isAuthenticated, isLoading } = useAuthStatus()
const { mutate: logout, isPending: isLoggingOut } = useLogout()
useEffect(() => {
setIsClient(true)
@@ -68,6 +95,10 @@ export default function Header({ locale = "ru", isAuthenticated = false, transla
}
}
const handleLogout = () => {
logout()
}
const toggleCategoryMenu = () => setIsCategoryOpen(!isCategoryOpen)
const closeCategoryMenu = () => setIsCategoryOpen(false)
@@ -80,7 +111,7 @@ export default function Header({ locale = "ru", isAuthenticated = false, transla
<div className="flex h-16 items-center justify-between gap-4">
<Link href="/" className="shrink-0">
<div className="relative h-8 w-[180px]">
<Image src={Logo || "/placeholder.svg"} alt="Logo" fill className="object-contain" priority />
<Image src={Logo} alt="Logo" fill className="object-contain" priority />
</div>
</Link>
@@ -106,6 +137,36 @@ export default function Header({ locale = "ru", isAuthenticated = false, transla
<SearchBar isMobile={false} searchPlaceholder={t.search} className="hidden flex-1 md:flex" />
<div className="hidden md:flex items-center gap-2">
{isLoading ? (
<div className="h-10 w-24 animate-pulse bg-gray-200 rounded" />
) : isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="flex-col gap-0.5 h-auto px-2 py-2">
<UserIcon className="h-5 w-5 text-gray-600" />
<span className="text-xs text-gray-700">{t.profile}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => (window.location.href = `/${locale}/me`)}>
<UserIcon className="mr-2 h-4 w-4" />
{t.profile}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
<LogOut className="mr-2 h-4 w-4" />
{isLoggingOut ? t.loggingOut : t.logout}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button variant="ghost" size="sm" className="flex-col gap-0.5 h-auto px-2 py-2" onClick={handleAuthClick}>
<UserIcon className="h-5 w-5 text-gray-600" />
<span className="text-xs text-gray-700">{t.login}</span>
</Button>
)}
</div>
<ActionButtons
isAuthenticated={isAuthenticated}
onAuthClick={handleAuthClick}
@@ -146,8 +207,15 @@ export default function Header({ locale = "ru", isAuthenticated = false, transla
phone: t.phone,
code: t.code,
send: t.send,
verify: t.verify,
sending: t.sending,
verifying: t.verifying,
invalidPhone: t.invalidPhone,
invalidCode: t.invalidCode,
loginSuccess: t.loginSuccess,
codeSent: t.codeSent,
}}
/>
</>
)
}
}

View File

@@ -1,59 +1,112 @@
import React, { useState } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import Logo from "@/public/logo.png";
"use client"
import React, { useState } from "react"
import Image from "next/image"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { toast } from "sonner"
import Logo from "@/public/logo.png"
import { useLogin, useVerifyToken } from "@/lib/hooks/useAuth"
interface AuthDialogProps {
isOpen: boolean;
onClose: () => void;
isOpen: boolean
onClose: () => void
translations: {
enterPhone: string;
weWillSendCode: string;
phone: string;
code: string;
send: string;
};
enterPhone: string
weWillSendCode: string
phone: string
code: string
send: string
verify: string
sending: string
verifying: string
invalidPhone: string
invalidCode: string
loginSuccess: string
codeSent: string
}
}
export default function AuthDialog({
isOpen,
onClose,
translations: t,
}: AuthDialogProps) {
const [phone, setPhone] = useState("993");
const [otp, setOtp] = useState("");
const [otpSent, setOtpSent] = useState(false);
export default function AuthDialog({ isOpen, onClose, translations: t }: AuthDialogProps) {
const [phone, setPhone] = useState("993")
const [otp, setOtp] = useState("")
const [otpSent, setOtpSent] = useState(false)
const [rawPhone, setRawPhone] = useState("")
const handleSendOtp = () => {
if (phone.length > 3) {
setOtpSent(true);
}
};
const handleLogin = () => {
// Here you can add authentication logic
resetDialog();
};
const { mutate: login, isPending: isLoginLoading } = useLogin()
const { mutate: verifyToken, isPending: isVerifyLoading } = useVerifyToken()
const resetDialog = () => {
onClose();
setOtpSent(false);
setPhone("993");
setOtp("");
};
setOtpSent(false)
setPhone("993")
setOtp("")
setRawPhone("")
onClose()
}
const handleSendOtp = () => {
const cleanPhone = phone.replace(/\D/g, "")
if (cleanPhone.length !== 11 || !cleanPhone.startsWith("993")) {
toast.error(t.invalidPhone)
return
}
const phoneNumber = cleanPhone.substring(3)
setRawPhone(phoneNumber)
login(
{ phone_number: phoneNumber },
{
onSuccess: () => {
toast.success(t.codeSent)
setOtpSent(true)
},
onError: (error: any) => {
toast.error(error?.response?.data?.message || "Hata oluştu")
},
}
)
}
const handleLogin = () => {
if (otp.length < 4) {
toast.error(t.invalidCode)
return
}
verifyToken(
{
phone_number: rawPhone,
code: otp,
},
{
onSuccess: () => {
toast.success(t.loginSuccess)
resetDialog()
window.location.reload()
},
onError: (error: any) => {
toast.error(error?.response?.data?.message || "Kod yanlış")
},
}
)
}
const handleKeyPress = (e: React.KeyboardEvent, action: () => void) => {
if (e.key === "Enter") {
action();
action()
}
};
}
const formatPhoneInput = (value: string) => {
const cleaned = value.replace(/\D/g, "")
if (!cleaned.startsWith("993")) {
return "993"
}
return cleaned.substring(0, 11)
}
return (
<Dialog open={isOpen} onOpenChange={resetDialog}>
@@ -64,34 +117,36 @@ export default function AuthDialog({
<Image src={Logo} alt="Logo" fill className="object-contain" />
</div>
</div>
<DialogTitle className="text-2xl text-center">
{t.enterPhone}
</DialogTitle>
<p className="text-center text-sm text-gray-600">
{t.weWillSendCode}
</p>
<DialogTitle className="text-2xl text-center">{t.enterPhone}</DialogTitle>
<p className="text-center text-sm text-gray-600">{t.weWillSendCode}</p>
</DialogHeader>
<div className="space-y-4 mt-4">
<Input
type="tel"
placeholder={t.phone}
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="h-12 rounded-xl"
onKeyDown={(e) => handleKeyPress(e, handleSendOtp)}
disabled={otpSent}
/>
<div>
<Input
type="tel"
placeholder={t.phone}
value={phone}
onChange={(e) => setPhone(formatPhoneInput(e.target.value))}
className="h-12 rounded-xl"
onKeyDown={(e) => handleKeyPress(e, handleSendOtp)}
disabled={otpSent || isLoginLoading}
maxLength={11}
/>
<p className="text-xs text-gray-500 mt-1">Format: 99365123456</p>
</div>
{otpSent && (
<Input
type="text"
placeholder={t.code}
value={otp}
onChange={(e) => setOtp(e.target.value)}
onChange={(e) => setOtp(e.target.value.replace(/\D/g, "").substring(0, 6))}
className="h-12 rounded-xl"
onKeyDown={(e) => handleKeyPress(e, handleLogin)}
disabled={isVerifyLoading}
autoFocus
maxLength={6}
/>
)}
@@ -99,11 +154,18 @@ export default function AuthDialog({
onClick={otpSent ? handleLogin : handleSendOtp}
className="w-full h-12 rounded-xl font-bold text-base"
size="lg"
disabled={isLoginLoading || isVerifyLoading}
>
{t.send}
{isLoginLoading
? t.sending
: isVerifyLoading
? t.verifying
: otpSent
? t.verify
: t.send}
</Button>
</div>
</DialogContent>
</Dialog>
);
)
}

View File

@@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}