refactor: apply consistent formatting to improve code quality
Ensure uniform code formatting across components by aligning with the established code style. Adjust imports, indentation, and spacing to enhance readability and maintainability.
This commit is contained in:
@@ -18,6 +18,11 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
|
"a11y": {
|
||||||
|
"useAriaPropsForRole": "warn",
|
||||||
|
"useSemanticElements": "warn",
|
||||||
|
"useFocusableInteractive": "warn"
|
||||||
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noUnknownAtRules": "off"
|
"noUnknownAtRules": "off"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/app/globals.css",
|
||||||
"baseColor": "stone",
|
"baseColor": "stone",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide",
|
"iconLibrary": "lucide",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"registries": {}
|
"registries": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,128 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z.string().email({ message: "Email invalide" }),
|
email: z.string().email({ message: "Email invalide" }),
|
||||||
password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type LoginFormValues = z.infer<typeof loginSchema>;
|
type LoginFormValues = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
const form = useForm<LoginFormValues>({
|
const form = useForm<LoginFormValues>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: LoginFormValues) {
|
async function onSubmit(values: LoginFormValues) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await login(values.email, values.password);
|
await login(values.email, values.password);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// Error is handled in useAuth via toast
|
// Error is handled in useAuth via toast
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||||
<div className="w-full max-w-md space-y-4">
|
<div className="w-full max-w-md space-y-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Retour à l'accueil
|
Retour à l'accueil
|
||||||
</Link>
|
</Link>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Connexion</CardTitle>
|
<CardTitle className="text-2xl">Connexion</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Entrez vos identifiants pour accéder à votre compte MemeGoat.
|
Entrez vos identifiants pour accéder à votre compte MemeGoat.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="goat@example.com" {...field} />
|
<Input placeholder="goat@example.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Mot de passe</FormLabel>
|
<FormLabel>Mot de passe</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
{loading ? "Connexion en cours..." : "Se connecter"}
|
{loading ? "Connexion en cours..." : "Se connecter"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col space-y-2">
|
<CardFooter className="flex flex-col space-y-2">
|
||||||
<p className="text-sm text-center text-muted-foreground">
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
Vous n'avez pas de compte ?{" "}
|
Vous n'avez pas de compte ?{" "}
|
||||||
<Link href="/register" className="text-primary hover:underline font-medium">
|
<Link
|
||||||
S'inscrire
|
href="/register"
|
||||||
</Link>
|
className="text-primary hover:underline font-medium"
|
||||||
</p>
|
>
|
||||||
</CardFooter>
|
S'inscrire
|
||||||
</Card>
|
</Link>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</CardFooter>
|
||||||
);
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,153 +1,157 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
|
|
||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
username: z.string().min(3, { message: "Le pseudo doit faire au moins 3 caractères" }),
|
username: z
|
||||||
email: z.string().email({ message: "Email invalide" }),
|
.string()
|
||||||
password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
.min(3, { message: "Le pseudo doit faire au moins 3 caractères" }),
|
||||||
displayName: z.string().optional(),
|
email: z.string().email({ message: "Email invalide" }),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
||||||
|
displayName: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type RegisterFormValues = z.infer<typeof registerSchema>;
|
type RegisterFormValues = z.infer<typeof registerSchema>;
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
const form = useForm<RegisterFormValues>({
|
const form = useForm<RegisterFormValues>({
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(registerSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
displayName: "",
|
displayName: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: RegisterFormValues) {
|
async function onSubmit(values: RegisterFormValues) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await register(values);
|
await register(values);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// Error handled in useAuth
|
// Error handled in useAuth
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||||
<div className="w-full max-w-md space-y-4">
|
<div className="w-full max-w-md space-y-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Retour à l'accueil
|
Retour à l'accueil
|
||||||
</Link>
|
</Link>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Inscription</CardTitle>
|
<CardTitle className="text-2xl">Inscription</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Rejoignez la communauté MemeGoat dès aujourd'hui.
|
Rejoignez la communauté MemeGoat dès aujourd'hui.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Pseudo</FormLabel>
|
<FormLabel>Pseudo</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="supergoat" {...field} />
|
<Input placeholder="supergoat" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="goat@example.com" {...field} />
|
<Input placeholder="goat@example.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="displayName"
|
name="displayName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Le Roi des Chèvres" {...field} />
|
<Input placeholder="Le Roi des Chèvres" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Mot de passe</FormLabel>
|
<FormLabel>Mot de passe</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="••••••••" {...field} />
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
{loading ? "Création du compte..." : "S'inscrire"}
|
{loading ? "Création du compte..." : "S'inscrire"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col space-y-2">
|
<CardFooter className="flex flex-col space-y-2">
|
||||||
<p className="text-sm text-center text-muted-foreground">
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
Vous avez déjà un compte ?{" "}
|
Vous avez déjà un compte ?{" "}
|
||||||
<Link href="/login" className="text-primary hover:underline font-medium">
|
<Link href="/login" className="text-primary hover:underline font-medium">
|
||||||
Se connecter
|
Se connecter
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,58 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
import * as React from "react";
|
||||||
import { ContentService } from "@/services/content.service";
|
|
||||||
import { ContentCard } from "@/components/content-card";
|
import { ContentCard } from "@/components/content-card";
|
||||||
import type { Content } from "@/types/content";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { ContentService } from "@/services/content.service";
|
||||||
|
import type { Content } from "@/types/content";
|
||||||
|
|
||||||
export default function MemeModal({ params }: { params: Promise<{ slug: string }> }) {
|
export default function MemeModal({
|
||||||
const { slug } = React.use(params);
|
params,
|
||||||
const router = useRouter();
|
}: {
|
||||||
const [content, setContent] = React.useState<Content | null>(null);
|
params: Promise<{ slug: string }>;
|
||||||
const [loading, setLoading] = React.useState(true);
|
}) {
|
||||||
|
const { slug } = React.use(params);
|
||||||
|
const router = useRouter();
|
||||||
|
const [content, setContent] = React.useState<Content | null>(null);
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
ContentService.getOne(slug)
|
ContentService.getOne(slug)
|
||||||
.then(setContent)
|
.then(setContent)
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && router.back()}>
|
<Dialog open onOpenChange={(open) => !open && router.back()}>
|
||||||
<DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none">
|
<DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none">
|
||||||
<DialogTitle className="sr-only">{content?.title || "Détail du mème"}</DialogTitle>
|
<DialogTitle className="sr-only">
|
||||||
<DialogDescription className="sr-only">Affiche le mème en grand avec ses détails</DialogDescription>
|
{content?.title || "Détail du mème"}
|
||||||
{loading ? (
|
</DialogTitle>
|
||||||
<div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg">
|
<DialogDescription className="sr-only">
|
||||||
<Spinner className="h-10 w-10 text-white" />
|
Affiche le mème en grand avec ses détails
|
||||||
</div>
|
</DialogDescription>
|
||||||
) : content ? (
|
{loading ? (
|
||||||
<div className="bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
|
<div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg">
|
||||||
<ContentCard content={content} />
|
<Spinner className="h-10 w-10 text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : content ? (
|
||||||
<div className="p-8 bg-white dark:bg-zinc-900 rounded-lg text-center">
|
<div className="bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
|
||||||
<p>Impossible de charger ce mème.</p>
|
<ContentCard content={content} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</DialogContent>
|
<div className="p-8 bg-white dark:bg-zinc-900 rounded-lg text-center">
|
||||||
</Dialog>
|
<p>Impossible de charger ce mème.</p>
|
||||||
);
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function Default() {
|
export default function Default() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
interface UseInfiniteScrollOptions {
|
interface UseInfiniteScrollOptions {
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInfiniteScroll({
|
export function useInfiniteScroll({
|
||||||
threshold = 1.0,
|
threshold = 1.0,
|
||||||
hasMore,
|
hasMore,
|
||||||
loading,
|
loading,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
}: UseInfiniteScrollOptions) {
|
}: UseInfiniteScrollOptions) {
|
||||||
const loaderRef = React.useRef<HTMLDivElement>(null);
|
const loaderRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting && hasMore && !loading) {
|
if (entries[0].isIntersecting && hasMore && !loading) {
|
||||||
onLoadMore();
|
onLoadMore();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold }
|
{ threshold },
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentLoader = loaderRef.current;
|
const currentLoader = loaderRef.current;
|
||||||
if (currentLoader) {
|
if (currentLoader) {
|
||||||
observer.observe(currentLoader);
|
observer.observe(currentLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (currentLoader) {
|
if (currentLoader) {
|
||||||
observer.unobserve(currentLoader);
|
observer.unobserve(currentLoader);
|
||||||
}
|
}
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [onLoadMore, hasMore, loading, threshold]);
|
}, [onLoadMore, hasMore, loading, threshold]);
|
||||||
|
|
||||||
return { loaderRef };
|
return { loaderRef };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import * as React from "react";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { CategoryContent } from "@/components/category-content";
|
import { CategoryContent } from "@/components/category-content";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ slug: string }>
|
params: Promise<{ slug: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
try {
|
try {
|
||||||
const categories = await CategoryService.getAll();
|
const categories = await CategoryService.getAll();
|
||||||
const category = categories.find(c => c.slug === slug);
|
const category = categories.find((c) => c.slug === slug);
|
||||||
return {
|
return {
|
||||||
title: `${category?.name || slug} | MemeGoat`,
|
title: `${category?.name || slug} | MemeGoat`,
|
||||||
description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`,
|
description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
return { title: `Catégorie : ${slug} | MemeGoat` };
|
return { title: `Catégorie : ${slug} | MemeGoat` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CategoryPage({
|
export default async function CategoryPage({
|
||||||
params
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ slug: string }>
|
params: Promise<{ slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
return <CategoryContent slug={slug} />;
|
return <CategoryContent slug={slug} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import { LayoutGrid } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { LayoutGrid } from "lucide-react";
|
|
||||||
|
|
||||||
export default function CategoriesPage() {
|
export default function CategoriesPage() {
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
CategoryService.getAll()
|
CategoryService.getAll()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||||
<div className="flex items-center gap-2 mb-8">
|
<div className="flex items-center gap-2 mb-8">
|
||||||
<LayoutGrid className="h-6 w-6" />
|
<LayoutGrid className="h-6 w-6" />
|
||||||
<h1 className="text-3xl font-bold">Catégories</h1>
|
<h1 className="text-3xl font-bold">Catégories</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
{loading ? (
|
{loading
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
? Array.from({ length: 6 }).map((_, i) => (
|
||||||
<Card key={i} className="animate-pulse">
|
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
|
||||||
<CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" />
|
<Card key={`skeleton-${i}`} className="animate-pulse">
|
||||||
<CardContent className="h-12" />
|
<CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" />
|
||||||
</Card>
|
<CardContent className="h-12" />
|
||||||
))
|
</Card>
|
||||||
) : (
|
))
|
||||||
categories.map((category) => (
|
: categories.map((category) => (
|
||||||
<Link key={category.id} href={`/category/${category.slug}`}>
|
<Link key={category.id} href={`/category/${category.slug}`}>
|
||||||
<Card className="hover:border-primary transition-colors cursor-pointer group h-full">
|
<Card className="hover:border-primary transition-colors cursor-pointer group h-full">
|
||||||
<CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors">
|
<CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors">
|
||||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{category.description || `Découvrez tous les mèmes de la catégorie ${category.name}.`}
|
{category.description ||
|
||||||
</p>
|
`Découvrez tous les mèmes de la catégorie ${category.name}.`}
|
||||||
</CardContent>
|
</p>
|
||||||
</Card>
|
</CardContent>
|
||||||
</Link>
|
</Card>
|
||||||
))
|
</Link>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar";
|
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
import { AppSidebar } from "@/components/app-sidebar";
|
||||||
import { SearchSidebar } from "@/components/search-sidebar";
|
|
||||||
import { MobileFilters } from "@/components/mobile-filters";
|
import { MobileFilters } from "@/components/mobile-filters";
|
||||||
|
import { SearchSidebar } from "@/components/search-sidebar";
|
||||||
|
import {
|
||||||
|
SidebarInset,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarTrigger,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export default function DashboardLayout({
|
export default function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
modal,
|
modal,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
modal: React.ReactNode;
|
modal: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset className="flex flex-row overflow-hidden">
|
<SidebarInset className="flex flex-row overflow-hidden">
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 lg:hidden">
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 lg:hidden">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
|
<main className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
|
||||||
{children}
|
{children}
|
||||||
{modal}
|
{modal}
|
||||||
</main>
|
</main>
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<MobileFilters />
|
<MobileFilters />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<SearchSidebar />
|
<SearchSidebar />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ContentSkeleton } from "@/components/content-skeleton";
|
import { ContentSkeleton } from "@/components/content-skeleton";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
<ContentSkeleton key={i} />
|
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
|
||||||
))}
|
<ContentSkeleton key={`loading-skeleton-${i}`} />
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,98 @@
|
|||||||
import * as React from "react";
|
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { ContentService } from "@/services/content.service";
|
|
||||||
import { ContentCard } from "@/components/content-card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { ContentCard } from "@/components/content-card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ContentService } from "@/services/content.service";
|
||||||
|
|
||||||
export const revalidate = 3600; // ISR: Revalider toutes les heures
|
export const revalidate = 3600; // ISR: Revalider toutes les heures
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ slug: string }>
|
params: Promise<{ slug: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
try {
|
try {
|
||||||
const content = await ContentService.getOne(slug);
|
const content = await ContentService.getOne(slug);
|
||||||
return {
|
return {
|
||||||
title: `${content.title} | MemeGoat`,
|
title: `${content.title} | MemeGoat`,
|
||||||
description: content.description || `Regardez ce mème : ${content.title}`,
|
description: content.description || `Regardez ce mème : ${content.title}`,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: [content.thumbnailUrl || content.url],
|
images: [content.thumbnailUrl || content.url],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
return { title: "Mème non trouvé | MemeGoat" };
|
return { title: "Mème non trouvé | MemeGoat" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function MemePage({
|
export default async function MemePage({
|
||||||
params
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ slug: string }>
|
params: Promise<{ slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await ContentService.getOne(slug);
|
const content = await ContentService.getOne(slug);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||||
<Link href="/" className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors">
|
<Link
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
href="/"
|
||||||
Retour au flux
|
className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors"
|
||||||
</Link>
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
Retour au flux
|
||||||
<div className="lg:col-span-2">
|
</Link>
|
||||||
<ContentCard content={content} />
|
|
||||||
</div>
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
<div className="space-y-6">
|
<ContentCard content={content} />
|
||||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border">
|
</div>
|
||||||
<h2 className="font-bold text-lg mb-4">À propos de ce mème</h2>
|
|
||||||
<div className="space-y-4 text-sm">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border">
|
||||||
<p className="text-muted-foreground">Publié par</p>
|
<h2 className="font-bold text-lg mb-4">À propos de ce mème</h2>
|
||||||
<p className="font-medium">{content.author.displayName || content.author.username}</p>
|
<div className="space-y-4 text-sm">
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<p className="text-muted-foreground">Publié par</p>
|
||||||
<p className="text-muted-foreground">Date</p>
|
<p className="font-medium">
|
||||||
<p className="font-medium">{new Date(content.createdAt).toLocaleDateString('fr-FR', {
|
{content.author.displayName || content.author.username}
|
||||||
day: 'numeric',
|
</p>
|
||||||
month: 'long',
|
</div>
|
||||||
year: 'numeric'
|
<div>
|
||||||
})}</p>
|
<p className="text-muted-foreground">Date</p>
|
||||||
</div>
|
<p className="font-medium">
|
||||||
{content.description && (
|
{new Date(content.createdAt).toLocaleDateString("fr-FR", {
|
||||||
<div>
|
day: "numeric",
|
||||||
<p className="text-muted-foreground">Description</p>
|
month: "long",
|
||||||
<p>{content.description}</p>
|
year: "numeric",
|
||||||
</div>
|
})}
|
||||||
)}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{content.description && (
|
||||||
|
<div>
|
||||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center">
|
<p className="text-muted-foreground">Description</p>
|
||||||
<p className="text-sm text-muted-foreground mb-4">Envie de créer votre propre mème ?</p>
|
<p>{content.description}</p>
|
||||||
<Button className="w-full">Utiliser ce template</Button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center">
|
||||||
} catch (error) {
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
notFound();
|
Envie de créer votre propre mème ?
|
||||||
}
|
</p>
|
||||||
|
<Button className="w-full">Utiliser ce template</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch (_error) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import * as React from "react";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import * as React from "react";
|
||||||
import { HomeContent } from "@/components/home-content";
|
import { HomeContent } from "@/components/home-content";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres",
|
title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres",
|
||||||
description: "Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.",
|
description:
|
||||||
|
"Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={
|
<React.Suspense
|
||||||
<div className="flex items-center justify-center p-12">
|
fallback={
|
||||||
<Spinner className="h-8 w-8 text-primary" />
|
<div className="flex items-center justify-center p-12">
|
||||||
</div>
|
<Spinner className="h-8 w-8 text-primary" />
|
||||||
}>
|
</div>
|
||||||
<HomeContent />
|
}
|
||||||
</React.Suspense>
|
>
|
||||||
);
|
<HomeContent />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,96 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import { Calendar, LogOut, Settings } from "lucide-react";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
|
||||||
import { ContentList } from "@/components/content-list";
|
|
||||||
import { ContentService } from "@/services/content.service";
|
|
||||||
import { FavoriteService } from "@/services/favorite.service";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Settings, LogOut, Calendar } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ContentList } from "@/components/content-list";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
|
import { ContentService } from "@/services/content.service";
|
||||||
|
import { FavoriteService } from "@/services/favorite.service";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { user, isAuthenticated, isLoading, logout } = useAuth();
|
const { user, isAuthenticated, isLoading, logout } = useAuth();
|
||||||
|
|
||||||
if (isLoading) return null;
|
const fetchMyMemes = React.useCallback(
|
||||||
if (!isAuthenticated || !user) {
|
(params: { limit: number; offset: number }) =>
|
||||||
redirect("/login");
|
ContentService.getExplore({ ...params, author: user?.username }),
|
||||||
}
|
[user?.username],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchMyMemes = React.useCallback((params: { limit: number; offset: number }) =>
|
const fetchMyFavorites = React.useCallback(
|
||||||
ContentService.getExplore({ ...params, author: user.username }),
|
(params: { limit: number; offset: number }) => FavoriteService.list(params),
|
||||||
[user.username]);
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchMyFavorites = React.useCallback((params: { limit: number; offset: number }) =>
|
if (isLoading) return null;
|
||||||
FavoriteService.list(params),
|
if (!isAuthenticated || !user) {
|
||||||
[]);
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8">
|
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8">
|
||||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||||
<Avatar className="h-32 w-32 border-4 border-primary/10">
|
<Avatar className="h-32 w-32 border-4 border-primary/10">
|
||||||
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
||||||
<AvatarFallback className="text-4xl">
|
<AvatarFallback className="text-4xl">
|
||||||
{user.username.slice(0, 2).toUpperCase()}
|
{user.username.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 text-center md:text-left space-y-4">
|
<div className="flex-1 text-center md:text-left space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">{user.displayName || user.username}</h1>
|
<h1 className="text-3xl font-bold">
|
||||||
<p className="text-muted-foreground">@{user.username}</p>
|
{user.displayName || user.username}
|
||||||
</div>
|
</h1>
|
||||||
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground">
|
<p className="text-muted-foreground">@{user.username}</p>
|
||||||
<span className="flex items-center gap-1">
|
</div>
|
||||||
<Calendar className="h-4 w-4" />
|
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground">
|
||||||
Membre depuis {new Date(user.createdAt).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
<span className="flex items-center gap-1">
|
||||||
</span>
|
<Calendar className="h-4 w-4" />
|
||||||
</div>
|
Membre depuis{" "}
|
||||||
<div className="flex flex-wrap justify-center md:justify-start gap-2">
|
{new Date(user.createdAt).toLocaleDateString("fr-FR", {
|
||||||
<Button asChild variant="outline" size="sm">
|
month: "long",
|
||||||
<Link href="/settings">
|
year: "numeric",
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
})}
|
||||||
Paramètres
|
</span>
|
||||||
</Link>
|
</div>
|
||||||
</Button>
|
<div className="flex flex-wrap justify-center md:justify-start gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => logout()} className="text-red-500 hover:text-red-600 hover:bg-red-50">
|
<Button asChild variant="outline" size="sm">
|
||||||
<LogOut className="h-4 w-4 mr-2" />
|
<Link href="/settings">
|
||||||
Déconnexion
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Paramètres
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
</div>
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => logout()}
|
||||||
|
className="text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="memes" className="w-full">
|
<Tabs defaultValue="memes" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-8">
|
<TabsList className="grid w-full grid-cols-2 mb-8">
|
||||||
<TabsTrigger value="memes">Mes Mèmes</TabsTrigger>
|
<TabsTrigger value="memes">Mes Mèmes</TabsTrigger>
|
||||||
<TabsTrigger value="favorites">Mes Favoris</TabsTrigger>
|
<TabsTrigger value="favorites">Mes Favoris</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="memes">
|
<TabsContent value="memes">
|
||||||
<ContentList fetchFn={fetchMyMemes} />
|
<ContentList fetchFn={fetchMyMemes} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="favorites">
|
<TabsContent value="favorites">
|
||||||
<ContentList fetchFn={fetchMyFavorites} />
|
<ContentList fetchFn={fetchMyFavorites} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
|
|||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
|
|
||||||
export default function RecentPage() {
|
export default function RecentPage() {
|
||||||
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
const fetchFn = React.useCallback(
|
||||||
ContentService.getRecent(params.limit, params.offset),
|
(params: { limit: number; offset: number }) =>
|
||||||
[]);
|
ContentService.getRecent(params.limit, params.offset),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />;
|
return <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
|
|||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
|
|
||||||
export default function TrendsPage() {
|
export default function TrendsPage() {
|
||||||
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
const fetchFn = React.useCallback(
|
||||||
ContentService.getTrends(params.limit, params.offset),
|
(params: { limit: number; offset: number }) =>
|
||||||
[]);
|
ContentService.getTrends(params.limit, params.offset),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return <ContentList fetchFn={fetchFn} title="Top Tendances" />;
|
return <ContentList fetchFn={fetchFn} title="Top Tendances" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,257 +1,283 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import * as z from "zod";
|
import { Image as ImageIcon, Loader2, Upload, X } from "lucide-react";
|
||||||
import { Upload, Image as ImageIcon, Film, X, Loader2 } from "lucide-react";
|
import NextImage from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import * as z from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
|
|
||||||
const uploadSchema = z.object({
|
const uploadSchema = z.object({
|
||||||
title: z.string().min(3, "Le titre doit faire au moins 3 caractères"),
|
title: z.string().min(3, "Le titre doit faire au moins 3 caractères"),
|
||||||
type: z.enum(["meme", "gif"]),
|
type: z.enum(["meme", "gif"]),
|
||||||
categoryId: z.string().optional(),
|
categoryId: z.string().optional(),
|
||||||
tags: z.string().optional(),
|
tags: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UploadFormValues = z.infer<typeof uploadSchema>;
|
type UploadFormValues = z.infer<typeof uploadSchema>;
|
||||||
|
|
||||||
export default function UploadPage() {
|
export default function UploadPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
const [file, setFile] = React.useState<File | null>(null);
|
const [file, setFile] = React.useState<File | null>(null);
|
||||||
const [preview, setPreview] = React.useState<string | null>(null);
|
const [preview, setPreview] = React.useState<string | null>(null);
|
||||||
const [isUploading, setIsUploading] = React.useState(false);
|
const [isUploading, setIsUploading] = React.useState(false);
|
||||||
|
|
||||||
const form = useForm<UploadFormValues>({
|
const form = useForm<UploadFormValues>({
|
||||||
resolver: zodResolver(uploadSchema),
|
resolver: zodResolver(uploadSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
type: "meme",
|
type: "meme",
|
||||||
tags: "",
|
tags: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
CategoryService.getAll().then(setCategories).catch(console.error);
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFile = e.target.files?.[0];
|
const selectedFile = e.target.files?.[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||||
toast.error("Le fichier est trop volumineux (max 10Mo)");
|
toast.error("Le fichier est trop volumineux (max 10Mo)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
setPreview(reader.result as string);
|
setPreview(reader.result as string);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(selectedFile);
|
reader.readAsDataURL(selectedFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (values: UploadFormValues) => {
|
const onSubmit = async (values: UploadFormValues) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
toast.error("Veuillez sélectionner un fichier");
|
toast.error("Veuillez sélectionner un fichier");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("title", values.title);
|
formData.append("title", values.title);
|
||||||
formData.append("type", values.type);
|
formData.append("type", values.type);
|
||||||
if (values.categoryId) formData.append("categoryId", values.categoryId);
|
if (values.categoryId) formData.append("categoryId", values.categoryId);
|
||||||
if (values.tags) {
|
if (values.tags) {
|
||||||
const tagsArray = values.tags.split(",").map(t => t.trim()).filter(t => t !== "");
|
const tagsArray = values.tags
|
||||||
tagsArray.forEach(tag => formData.append("tags[]", tag));
|
.split(",")
|
||||||
}
|
.map((t) => t.trim())
|
||||||
|
.filter((t) => t !== "");
|
||||||
|
for (const tag of tagsArray) {
|
||||||
|
formData.append("tags[]", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await ContentService.upload(formData);
|
await ContentService.upload(formData);
|
||||||
toast.success("Mème uploadé avec succès !");
|
toast.success("Mème uploadé avec succès !");
|
||||||
router.push("/");
|
router.push("/");
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Upload failed:", error);
|
console.error("Upload failed:", error);
|
||||||
toast.error(error.response?.data?.message || "Échec de l'upload. Êtes-vous connecté ?");
|
let errorMessage = "Échec de l'upload. Êtes-vous connecté ?";
|
||||||
} finally {
|
if (
|
||||||
setIsUploading(false);
|
error &&
|
||||||
}
|
typeof error === "object" &&
|
||||||
};
|
"response" in error &&
|
||||||
|
error.response &&
|
||||||
|
typeof error.response === "object" &&
|
||||||
|
"data" in error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"message" in error.response.data &&
|
||||||
|
typeof error.response.data.message === "string"
|
||||||
|
) {
|
||||||
|
errorMessage = error.response.data.message;
|
||||||
|
}
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto py-8 px-4">
|
<div className="max-w-2xl mx-auto py-8 px-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Upload className="h-5 w-5" />
|
<Upload className="h-5 w-5" />
|
||||||
Partager un mème
|
Partager un mème
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
||||||
{!preview ? (
|
{!preview ? (
|
||||||
<div
|
<button
|
||||||
className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
type="button"
|
||||||
onClick={() => document.getElementById("file-upload")?.click()}
|
className="w-full border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||||
>
|
onClick={() => document.getElementById("file-upload")?.click()}
|
||||||
<div className="bg-primary/10 p-4 rounded-full mb-4">
|
>
|
||||||
<ImageIcon className="h-8 w-8 text-primary" />
|
<div className="bg-primary/10 p-4 rounded-full mb-4">
|
||||||
</div>
|
<ImageIcon className="h-8 w-8 text-primary" />
|
||||||
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG ou GIF jusqu'à 10Mo</p>
|
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
||||||
<input
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
id="file-upload"
|
PNG, JPG ou GIF jusqu'à 10Mo
|
||||||
type="file"
|
</p>
|
||||||
className="hidden"
|
<input
|
||||||
accept="image/*,.gif"
|
id="file-upload"
|
||||||
onChange={handleFileChange}
|
type="file"
|
||||||
/>
|
className="hidden"
|
||||||
</div>
|
accept="image/*,.gif"
|
||||||
) : (
|
onChange={handleFileChange}
|
||||||
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
/>
|
||||||
<img
|
</button>
|
||||||
src={preview}
|
) : (
|
||||||
alt="Preview"
|
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
||||||
className="max-h-[400px] mx-auto object-contain"
|
<div className="relative h-[400px] w-full">
|
||||||
/>
|
<NextImage
|
||||||
<Button
|
src={preview}
|
||||||
type="button"
|
alt="Preview"
|
||||||
variant="destructive"
|
fill
|
||||||
size="icon"
|
className="object-contain"
|
||||||
className="absolute top-2 right-2 rounded-full"
|
/>
|
||||||
onClick={() => {
|
</div>
|
||||||
setFile(null);
|
<Button
|
||||||
setPreview(null);
|
type="button"
|
||||||
}}
|
variant="destructive"
|
||||||
>
|
size="icon"
|
||||||
<X className="h-4 w-4" />
|
className="absolute top-2 right-2 rounded-full z-10"
|
||||||
</Button>
|
onClick={() => {
|
||||||
</div>
|
setFile(null);
|
||||||
)}
|
setPreview(null);
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Titre</FormLabel>
|
<FormLabel>Titre</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Un titre génial pour votre mème..." {...field} />
|
<Input placeholder="Un titre génial pour votre mème..." {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="type"
|
name="type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Format</FormLabel>
|
<FormLabel>Format</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Sélectionnez un format" />
|
<SelectValue placeholder="Sélectionnez un format" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="meme">Image fixe</SelectItem>
|
<SelectItem value="meme">Image fixe</SelectItem>
|
||||||
<SelectItem value="gif">GIF Animé</SelectItem>
|
<SelectItem value="gif">GIF Animé</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Catégorie</FormLabel>
|
<FormLabel>Catégorie</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Sélectionnez une catégorie" />
|
<SelectValue placeholder="Sélectionnez une catégorie" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map(cat => (
|
{categories.map((cat) => (
|
||||||
<SelectItem key={cat.id} value={cat.id}>{cat.name}</SelectItem>
|
<SelectItem key={cat.id} value={cat.id}>
|
||||||
))}
|
{cat.name}
|
||||||
</SelectContent>
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
<FormMessage />
|
</SelectContent>
|
||||||
</FormItem>
|
</Select>
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="tags"
|
name="tags"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Tags</FormLabel>
|
<FormLabel>Tags</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="funny, coding, goat..." {...field} />
|
<Input placeholder="funny, coding, goat..." {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>Séparez les tags par des virgules.</FormDescription>
|
||||||
Séparez les tags par des virgules.
|
<FormMessage />
|
||||||
</FormDescription>
|
</FormItem>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isUploading}>
|
<Button type="submit" className="w-full" disabled={isUploading}>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Upload en cours...
|
Upload en cours...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Publier le mème"
|
"Publier le mème"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,50 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { AlertTriangle, Home, RefreshCw } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: correct use
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error & { digest?: string };
|
error: Error & { digest?: string };
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||||
<div className="text-center space-y-6 max-w-md">
|
<div className="text-center space-y-6 max-w-md">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="bg-orange-100 dark:bg-orange-900/30 p-4 rounded-full">
|
<div className="bg-orange-100 dark:bg-orange-900/30 p-4 rounded-full">
|
||||||
<AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" />
|
<AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold tracking-tight">Oups ! Une erreur est survenue</h1>
|
<h1 className="text-4xl font-bold tracking-tight">
|
||||||
<p className="text-muted-foreground text-lg">
|
Oups ! Une erreur est survenue
|
||||||
La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur pied.
|
</h1>
|
||||||
</p>
|
<p className="text-muted-foreground text-lg">
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur
|
||||||
<Button onClick={reset} size="lg" className="gap-2">
|
pied.
|
||||||
<RefreshCw className="h-4 w-4" />
|
</p>
|
||||||
Réessayer
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
</Button>
|
<Button onClick={reset} size="lg" className="gap-2">
|
||||||
<Button asChild variant="outline" size="lg" className="gap-2">
|
<RefreshCw className="h-4 w-4" />
|
||||||
<Link href="/">
|
Réessayer
|
||||||
<Home className="h-4 w-4" />
|
</Button>
|
||||||
Retourner à l'accueil
|
<Button asChild variant="outline" size="lg" className="gap-2">
|
||||||
</Link>
|
<Link href="/">
|
||||||
</Button>
|
<Home className="h-4 w-4" />
|
||||||
</div>
|
Retourner à l'accueil
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans:
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--font-mono:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -80,34 +83,37 @@
|
|||||||
--popover: oklch(0.9911 0 0);
|
--popover: oklch(0.9911 0 0);
|
||||||
--popover-foreground: oklch(0.2435 0 0);
|
--popover-foreground: oklch(0.2435 0 0);
|
||||||
--primary: oklch(0.4341 0.0392 41.9938);
|
--primary: oklch(0.4341 0.0392 41.9938);
|
||||||
--primary-foreground: oklch(1.0000 0 0);
|
--primary-foreground: oklch(1 0 0);
|
||||||
--secondary: oklch(0.9200 0.0651 74.3695);
|
--secondary: oklch(0.92 0.0651 74.3695);
|
||||||
--secondary-foreground: oklch(0.3499 0.0685 40.8288);
|
--secondary-foreground: oklch(0.3499 0.0685 40.8288);
|
||||||
--muted: oklch(0.9521 0 0);
|
--muted: oklch(0.9521 0 0);
|
||||||
--muted-foreground: oklch(0.5032 0 0);
|
--muted-foreground: oklch(0.5032 0 0);
|
||||||
--accent: oklch(0.9310 0 0);
|
--accent: oklch(0.931 0 0);
|
||||||
--accent-foreground: oklch(0.2435 0 0);
|
--accent-foreground: oklch(0.2435 0 0);
|
||||||
--destructive: oklch(0.6271 0.1936 33.3390);
|
--destructive: oklch(0.6271 0.1936 33.339);
|
||||||
--border: oklch(0.8822 0 0);
|
--border: oklch(0.8822 0 0);
|
||||||
--input: oklch(0.8822 0 0);
|
--input: oklch(0.8822 0 0);
|
||||||
--ring: oklch(0.4341 0.0392 41.9938);
|
--ring: oklch(0.4341 0.0392 41.9938);
|
||||||
--chart-1: oklch(0.4341 0.0392 41.9938);
|
--chart-1: oklch(0.4341 0.0392 41.9938);
|
||||||
--chart-2: oklch(0.9200 0.0651 74.3695);
|
--chart-2: oklch(0.92 0.0651 74.3695);
|
||||||
--chart-3: oklch(0.9310 0 0);
|
--chart-3: oklch(0.931 0 0);
|
||||||
--chart-4: oklch(0.9367 0.0523 75.5009);
|
--chart-4: oklch(0.9367 0.0523 75.5009);
|
||||||
--chart-5: oklch(0.4338 0.0437 41.6746);
|
--chart-5: oklch(0.4338 0.0437 41.6746);
|
||||||
--sidebar: oklch(0.9881 0 0);
|
--sidebar: oklch(0.9881 0 0);
|
||||||
--sidebar-foreground: oklch(0.2645 0 0);
|
--sidebar-foreground: oklch(0.2645 0 0);
|
||||||
--sidebar-primary: oklch(0.3250 0 0);
|
--sidebar-primary: oklch(0.325 0 0);
|
||||||
--sidebar-primary-foreground: oklch(0.9881 0 0);
|
--sidebar-primary-foreground: oklch(0.9881 0 0);
|
||||||
--sidebar-accent: oklch(0.9761 0 0);
|
--sidebar-accent: oklch(0.9761 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.3250 0 0);
|
--sidebar-accent-foreground: oklch(0.325 0 0);
|
||||||
--sidebar-border: oklch(0.9401 0 0);
|
--sidebar-border: oklch(0.9401 0 0);
|
||||||
--sidebar-ring: oklch(0.7731 0 0);
|
--sidebar-ring: oklch(0.7731 0 0);
|
||||||
--destructive-foreground: oklch(1.0000 0 0);
|
--destructive-foreground: oklch(1 0 0);
|
||||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans:
|
||||||
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
--font-mono:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
--shadow-color: oklch(0 0 0);
|
--shadow-color: oklch(0 0 0);
|
||||||
--shadow-opacity: 0.09;
|
--shadow-opacity: 0.09;
|
||||||
--shadow-blur: 5.5px;
|
--shadow-blur: 5.5px;
|
||||||
@@ -118,11 +124,21 @@
|
|||||||
--spacing: 0.2rem;
|
--spacing: 0.2rem;
|
||||||
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
||||||
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
||||||
--shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
--shadow-sm:
|
||||||
--shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
--shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
--shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
|
--shadow:
|
||||||
--shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-md:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-lg:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xl:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
|
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
|
||||||
--tracking-normal: 0em;
|
--tracking-normal: 0em;
|
||||||
}
|
}
|
||||||
@@ -135,35 +151,38 @@
|
|||||||
--popover: oklch(0.2134 0 0);
|
--popover: oklch(0.2134 0 0);
|
||||||
--popover-foreground: oklch(0.9491 0 0);
|
--popover-foreground: oklch(0.9491 0 0);
|
||||||
--primary: oklch(0.9247 0.0524 66.1732);
|
--primary: oklch(0.9247 0.0524 66.1732);
|
||||||
--primary-foreground: oklch(0.2490 0.0317 198.7326);
|
--primary-foreground: oklch(0.249 0.0317 198.7326);
|
||||||
--secondary: oklch(0.3163 0.0190 63.6992);
|
--secondary: oklch(0.3163 0.019 63.6992);
|
||||||
--secondary-foreground: oklch(0.9247 0.0524 66.1732);
|
--secondary-foreground: oklch(0.9247 0.0524 66.1732);
|
||||||
--muted: oklch(0.2520 0 0);
|
--muted: oklch(0.252 0 0);
|
||||||
--muted-foreground: oklch(0.7699 0 0);
|
--muted-foreground: oklch(0.7699 0 0);
|
||||||
--accent: oklch(0.2850 0 0);
|
--accent: oklch(0.285 0 0);
|
||||||
--accent-foreground: oklch(0.9491 0 0);
|
--accent-foreground: oklch(0.9491 0 0);
|
||||||
--destructive: oklch(0.6271 0.1936 33.3390);
|
--destructive: oklch(0.6271 0.1936 33.339);
|
||||||
--border: oklch(0.2351 0.0115 91.7467);
|
--border: oklch(0.2351 0.0115 91.7467);
|
||||||
--input: oklch(0.4017 0 0);
|
--input: oklch(0.4017 0 0);
|
||||||
--ring: oklch(0.9247 0.0524 66.1732);
|
--ring: oklch(0.9247 0.0524 66.1732);
|
||||||
--chart-1: oklch(0.9247 0.0524 66.1732);
|
--chart-1: oklch(0.9247 0.0524 66.1732);
|
||||||
--chart-2: oklch(0.3163 0.0190 63.6992);
|
--chart-2: oklch(0.3163 0.019 63.6992);
|
||||||
--chart-3: oklch(0.2850 0 0);
|
--chart-3: oklch(0.285 0 0);
|
||||||
--chart-4: oklch(0.3481 0.0219 67.0001);
|
--chart-4: oklch(0.3481 0.0219 67.0001);
|
||||||
--chart-5: oklch(0.9245 0.0533 67.0855);
|
--chart-5: oklch(0.9245 0.0533 67.0855);
|
||||||
--sidebar: oklch(0.2103 0.0059 285.8852);
|
--sidebar: oklch(0.2103 0.0059 285.8852);
|
||||||
--sidebar-foreground: oklch(0.9674 0.0013 286.3752);
|
--sidebar-foreground: oklch(0.9674 0.0013 286.3752);
|
||||||
--sidebar-primary: oklch(0.4882 0.2172 264.3763);
|
--sidebar-primary: oklch(0.4882 0.2172 264.3763);
|
||||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
--sidebar-accent: oklch(0.2739 0.0055 286.0326);
|
--sidebar-accent: oklch(0.2739 0.0055 286.0326);
|
||||||
--sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752);
|
--sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752);
|
||||||
--sidebar-border: oklch(0.2739 0.0055 286.0326);
|
--sidebar-border: oklch(0.2739 0.0055 286.0326);
|
||||||
--sidebar-ring: oklch(0.8711 0.0055 286.2860);
|
--sidebar-ring: oklch(0.8711 0.0055 286.286);
|
||||||
--destructive-foreground: oklch(1.0000 0 0);
|
--destructive-foreground: oklch(1 0 0);
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-sans:
|
||||||
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
--font-mono:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
--shadow-color: oklch(0 0 0);
|
--shadow-color: oklch(0 0 0);
|
||||||
--shadow-opacity: 0.09;
|
--shadow-opacity: 0.09;
|
||||||
--shadow-blur: 5.5px;
|
--shadow-blur: 5.5px;
|
||||||
@@ -174,20 +193,30 @@
|
|||||||
--spacing: 0.2rem;
|
--spacing: 0.2rem;
|
||||||
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
||||||
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
|
||||||
--shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
--shadow-sm:
|
||||||
--shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
--shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
--shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
|
--shadow:
|
||||||
--shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-md:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-lg:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xl:
|
||||||
|
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
|
||||||
|
2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
|
||||||
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
|
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
letter-spacing: var(--tracking-normal);
|
letter-spacing: var(--tracking-normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,36 @@ import { AuthProvider } from "@/providers/auth-provider";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const ubuntuSans = Ubuntu_Sans({
|
const ubuntuSans = Ubuntu_Sans({
|
||||||
variable: "--font-ubuntu-sans",
|
variable: "--font-ubuntu-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const ubuntuMono = Ubuntu_Mono({
|
const ubuntuMono = Ubuntu_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-geist-mono",
|
||||||
weight: ["400", "700"],
|
weight: ["400", "700"],
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MemeGoat",
|
title: "MemeGoat",
|
||||||
icons: "/memegoat-color.svg",
|
icons: "/memegoat-color.svg",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="fr" suppressHydrationWarning>
|
<html lang="fr" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${ubuntuSans.variable} ${ubuntuMono.variable} antialiased`}
|
className={`${ubuntuSans.variable} ${ubuntuMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { AlertCircle, Home } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Home, AlertCircle } from "lucide-react";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||||
<div className="text-center space-y-6 max-w-md">
|
<div className="text-center space-y-6 max-w-md">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="bg-red-100 dark:bg-red-900/30 p-4 rounded-full">
|
<div className="bg-red-100 dark:bg-red-900/30 p-4 rounded-full">
|
||||||
<AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" />
|
<AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold tracking-tight">404 - Perdu dans le troupeau ?</h1>
|
<h1 className="text-4xl font-bold tracking-tight">
|
||||||
<p className="text-muted-foreground text-lg">
|
404 - Perdu dans le troupeau ?
|
||||||
On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé.
|
</h1>
|
||||||
</p>
|
<p className="text-muted-foreground text-lg">
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé.
|
||||||
<Button asChild size="lg" className="gap-2">
|
</p>
|
||||||
<Link href="/">
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
<Home className="h-4 w-4" />
|
<Button asChild size="lg" className="gap-2">
|
||||||
Retourner à l'accueil
|
<Link href="/">
|
||||||
</Link>
|
<Home className="h-4 w-4" />
|
||||||
</Button>
|
Retourner à l'accueil
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
<div className="mt-12 text-8xl grayscale opacity-20 select-none">
|
</div>
|
||||||
🐐
|
</div>
|
||||||
</div>
|
<div className="mt-12 text-8xl grayscale opacity-20 select-none">🐐</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
userAgent: "*",
|
userAgent: "*",
|
||||||
allow: "/",
|
allow: "/",
|
||||||
disallow: ["/settings/", "/upload/", "/api/"],
|
disallow: ["/settings/", "/upload/", "/api/"],
|
||||||
},
|
},
|
||||||
sitemap: `${baseUrl}/sitemap.xml`,
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
import { ContentService } from "@/services/content.service";
|
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
|
import { ContentService } from "@/services/content.service";
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
||||||
|
|
||||||
// Pages statiques
|
// Pages statiques
|
||||||
const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map((route) => ({
|
const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map(
|
||||||
url: `${baseUrl}${route}`,
|
(route) => ({
|
||||||
lastModified: new Date(),
|
url: `${baseUrl}${route}`,
|
||||||
changeFrequency: "daily" as const,
|
lastModified: new Date(),
|
||||||
priority: route === "" ? 1 : 0.8,
|
changeFrequency: "daily" as const,
|
||||||
}));
|
priority: route === "" ? 1 : 0.8,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Catégories
|
// Catégories
|
||||||
try {
|
try {
|
||||||
const categories = await CategoryService.getAll();
|
const categories = await CategoryService.getAll();
|
||||||
const categoryRoutes = categories.map((category) => ({
|
const categoryRoutes = categories.map((category) => ({
|
||||||
url: `${baseUrl}/category/${category.slug}`,
|
url: `${baseUrl}/category/${category.slug}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "weekly" as const,
|
changeFrequency: "weekly" as const,
|
||||||
priority: 0.6,
|
priority: 0.6,
|
||||||
}));
|
}));
|
||||||
routes.push(...categoryRoutes);
|
routes.push(...categoryRoutes);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.error("Sitemap: Failed to fetch categories");
|
console.error("Sitemap: Failed to fetch categories");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mèmes (limité aux 100 derniers pour éviter un sitemap trop gros d'un coup)
|
// Mèmes (limité aux 100 derniers pour éviter un sitemap trop gros d'un coup)
|
||||||
try {
|
try {
|
||||||
const contents = await ContentService.getRecent(100, 0);
|
const contents = await ContentService.getRecent(100, 0);
|
||||||
const memeRoutes = contents.data.map((meme) => ({
|
const memeRoutes = contents.data.map((meme) => ({
|
||||||
url: `${baseUrl}/meme/${meme.slug}`,
|
url: `${baseUrl}/meme/${meme.slug}`,
|
||||||
lastModified: new Date(meme.updatedAt),
|
lastModified: new Date(meme.updatedAt),
|
||||||
changeFrequency: "monthly" as const,
|
changeFrequency: "monthly" as const,
|
||||||
priority: 0.5,
|
priority: 0.5,
|
||||||
}));
|
}));
|
||||||
routes.push(...memeRoutes);
|
routes.push(...memeRoutes);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.error("Sitemap: Failed to fetch memes");
|
console.error("Sitemap: Failed to fetch memes");
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,243 +1,243 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Clock,
|
||||||
|
HelpCircle,
|
||||||
|
Home,
|
||||||
|
LayoutGrid,
|
||||||
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
PlusCircle,
|
||||||
|
Settings,
|
||||||
|
TrendingUp,
|
||||||
|
User as UserIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import {
|
import {
|
||||||
Home,
|
Collapsible,
|
||||||
TrendingUp,
|
CollapsibleContent,
|
||||||
Clock,
|
CollapsibleTrigger,
|
||||||
LayoutGrid,
|
|
||||||
PlusCircle,
|
|
||||||
Settings,
|
|
||||||
HelpCircle,
|
|
||||||
ChevronRight,
|
|
||||||
LogOut,
|
|
||||||
User as UserIcon,
|
|
||||||
LogIn,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
|
||||||
|
|
||||||
const mainNav = [
|
const mainNav = [
|
||||||
{
|
{
|
||||||
title: "Accueil",
|
title: "Accueil",
|
||||||
url: "/",
|
url: "/",
|
||||||
icon: Home,
|
icon: Home,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Tendances",
|
title: "Tendances",
|
||||||
url: "/trends",
|
url: "/trends",
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Nouveautés",
|
title: "Nouveautés",
|
||||||
url: "/recent",
|
url: "/recent",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { user, logout, isAuthenticated, isLoading } = useAuth();
|
const { user, logout, isAuthenticated } = useAuth();
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
CategoryService.getAll().then(setCategories).catch(console.error);
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader className="flex items-center justify-center py-4">
|
<SidebarHeader className="flex items-center justify-center py-4">
|
||||||
<Link href="/" className="flex items-center gap-2 font-bold text-xl">
|
<Link href="/" className="flex items-center gap-2 font-bold text-xl">
|
||||||
<div className="bg-primary text-primary-foreground p-1 rounded">
|
<div className="bg-primary text-primary-foreground p-1 rounded">🐐</div>
|
||||||
🐐
|
<span className="group-data-[collapsible=icon]:hidden">MemeGoat</span>
|
||||||
</div>
|
</Link>
|
||||||
<span className="group-data-[collapsible=icon]:hidden">MemeGoat</span>
|
</SidebarHeader>
|
||||||
</Link>
|
<SidebarContent>
|
||||||
</SidebarHeader>
|
<SidebarGroup>
|
||||||
<SidebarContent>
|
<SidebarMenu>
|
||||||
<SidebarGroup>
|
{mainNav.map((item) => (
|
||||||
<SidebarMenu>
|
<SidebarMenuItem key={item.title}>
|
||||||
{mainNav.map((item) => (
|
<SidebarMenuButton
|
||||||
<SidebarMenuItem key={item.title}>
|
asChild
|
||||||
<SidebarMenuButton
|
isActive={pathname === item.url}
|
||||||
asChild
|
tooltip={item.title}
|
||||||
isActive={pathname === item.url}
|
>
|
||||||
tooltip={item.title}
|
<Link href={item.url}>
|
||||||
>
|
<item.icon />
|
||||||
<Link href={item.url}>
|
<span>{item.title}</span>
|
||||||
<item.icon />
|
</Link>
|
||||||
<span>{item.title}</span>
|
</SidebarMenuButton>
|
||||||
</Link>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuButton>
|
))}
|
||||||
</SidebarMenuItem>
|
</SidebarMenu>
|
||||||
))}
|
</SidebarGroup>
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
|
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Explorer</SidebarGroupLabel>
|
<SidebarGroupLabel>Explorer</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<Collapsible asChild className="group/collapsible">
|
<Collapsible asChild className="group/collapsible">
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton tooltip="Catégories">
|
<SidebarMenuButton tooltip="Catégories">
|
||||||
<LayoutGrid />
|
<LayoutGrid />
|
||||||
<span>Catégories</span>
|
<span>Catégories</span>
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<SidebarMenuSubItem key={category.id}>
|
<SidebarMenuSubItem key={category.id}>
|
||||||
<SidebarMenuSubButton asChild isActive={pathname === `/category/${category.slug}`}>
|
<SidebarMenuSubButton
|
||||||
<Link href={`/category/${category.slug}`}>
|
asChild
|
||||||
<span>{category.name}</span>
|
isActive={pathname === `/category/${category.slug}`}
|
||||||
</Link>
|
>
|
||||||
</SidebarMenuSubButton>
|
<Link href={`/category/${category.slug}`}>
|
||||||
</SidebarMenuSubItem>
|
<span>{category.name}</span>
|
||||||
))}
|
</Link>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSubButton>
|
||||||
</CollapsibleContent>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuItem>
|
))}
|
||||||
</Collapsible>
|
</SidebarMenuSub>
|
||||||
</SidebarMenu>
|
</CollapsibleContent>
|
||||||
</SidebarGroup>
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Communauté</SidebarGroupLabel>
|
<SidebarGroupLabel>Communauté</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Publier">
|
<SidebarMenuButton asChild tooltip="Publier">
|
||||||
<Link href="/upload">
|
<Link href="/upload">
|
||||||
<PlusCircle />
|
<PlusCircle />
|
||||||
<span>Publier</span>
|
<span>Publier</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{isAuthenticated && user ? (
|
{isAuthenticated && user ? (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
||||||
<AvatarFallback className="rounded-lg">
|
<AvatarFallback className="rounded-lg">
|
||||||
{user.username.slice(0, 2).toUpperCase()}
|
{user.username.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
||||||
<span className="truncate font-semibold">
|
<span className="truncate font-semibold">
|
||||||
{user.displayName || user.username}
|
{user.displayName || user.username}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
|
<ChevronRight className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
side="right"
|
side="right"
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
||||||
<AvatarFallback className="rounded-lg">
|
<AvatarFallback className="rounded-lg">
|
||||||
{user.username.slice(0, 2).toUpperCase()}
|
{user.username.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-semibold">
|
<span className="truncate font-semibold">
|
||||||
{user.displayName || user.username}
|
{user.displayName || user.username}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/profile" className="flex items-center gap-2">
|
<Link href="/profile" className="flex items-center gap-2">
|
||||||
<UserIcon className="size-4" />
|
<UserIcon className="size-4" />
|
||||||
<span>Profil</span>
|
<span>Profil</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/settings" className="flex items-center gap-2">
|
<Link href="/settings" className="flex items-center gap-2">
|
||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
<span>Paramètres</span>
|
<span>Paramètres</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => logout()}>
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
<LogOut className="size-4 mr-2" />
|
<LogOut className="size-4 mr-2" />
|
||||||
<span>Déconnexion</span>
|
<span>Déconnexion</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Se connecter">
|
<SidebarMenuButton asChild tooltip="Se connecter">
|
||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<LogIn className="size-4" />
|
<LogIn className="size-4" />
|
||||||
<span>Se connecter</span>
|
<span>Se connecter</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)}
|
)}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Aide">
|
<SidebarMenuButton asChild tooltip="Aide">
|
||||||
<Link href="/help">
|
<Link href="/help">
|
||||||
<HelpCircle />
|
<HelpCircle />
|
||||||
<span>Aide</span>
|
<span>Aide</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
|
|||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
|
|
||||||
export function CategoryContent({ slug }: { slug: string }) {
|
export function CategoryContent({ slug }: { slug: string }) {
|
||||||
const fetchFn = React.useCallback((p: { limit: number; offset: number }) =>
|
const fetchFn = React.useCallback(
|
||||||
ContentService.getExplore({ ...p, category: slug }),
|
(p: { limit: number; offset: number }) =>
|
||||||
[slug]);
|
ContentService.getExplore({ ...p, category: slug }),
|
||||||
|
[slug],
|
||||||
|
);
|
||||||
|
|
||||||
return <ContentList fetchFn={fetchFn} title={`Catégorie : ${slug}`} />;
|
return <ContentList fetchFn={fetchFn} title={`Catégorie : ${slug}`} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,143 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import { Eye, Heart, MoreHorizontal, Share2 } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Heart, MessageSquare, Share2, MoreHorizontal, Eye } from "lucide-react";
|
import { useRouter } from "next/navigation";
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
import * as React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { toast } from "sonner";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import type { Content } from "@/types/content";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { useAuth } from "@/providers/auth-provider";
|
import { useAuth } from "@/providers/auth-provider";
|
||||||
import { FavoriteService } from "@/services/favorite.service";
|
import { FavoriteService } from "@/services/favorite.service";
|
||||||
import { useRouter } from "next/navigation";
|
import type { Content } from "@/types/content";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface ContentCardProps {
|
interface ContentCardProps {
|
||||||
content: Content;
|
content: Content;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContentCard({ content }: ContentCardProps) {
|
export function ContentCard({ content }: ContentCardProps) {
|
||||||
const { isAuthenticated, user } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLiked, setIsLiked] = React.useState(false);
|
const [isLiked, setIsLiked] = React.useState(false);
|
||||||
const [likesCount, setLikesCount] = React.useState(content.favoritesCount);
|
const [likesCount, setLikesCount] = React.useState(content.favoritesCount);
|
||||||
|
|
||||||
const handleLike = async (e: React.MouseEvent) => {
|
const handleLike = async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
toast.error("Vous devez être connecté pour liker un mème");
|
toast.error("Vous devez être connecté pour liker un mème");
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isLiked) {
|
if (isLiked) {
|
||||||
await FavoriteService.remove(content.id);
|
await FavoriteService.remove(content.id);
|
||||||
setIsLiked(false);
|
setIsLiked(false);
|
||||||
setLikesCount(prev => prev - 1);
|
setLikesCount((prev) => prev - 1);
|
||||||
} else {
|
} else {
|
||||||
await FavoriteService.add(content.id);
|
await FavoriteService.add(content.id);
|
||||||
setIsLiked(true);
|
setIsLiked(true);
|
||||||
setLikesCount(prev => prev + 1);
|
setLikesCount((prev) => prev + 1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
toast.error("Une erreur est survenue");
|
toast.error("Une erreur est survenue");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden border-none shadow-sm hover:shadow-md transition-shadow">
|
<Card className="overflow-hidden border-none shadow-sm hover:shadow-md transition-shadow">
|
||||||
<CardHeader className="p-4 flex flex-row items-center space-y-0 gap-3">
|
<CardHeader className="p-4 flex flex-row items-center space-y-0 gap-3">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src={content.author.avatarUrl} />
|
<AvatarImage src={content.author.avatarUrl} />
|
||||||
<AvatarFallback>{content.author.username[0].toUpperCase()}</AvatarFallback>
|
<AvatarFallback>{content.author.username[0].toUpperCase()}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Link href={`/user/${content.author.username}`} className="text-sm font-semibold hover:underline">
|
<Link
|
||||||
{content.author.displayName || content.author.username}
|
href={`/user/${content.author.username}`}
|
||||||
</Link>
|
className="text-sm font-semibold hover:underline"
|
||||||
<span className="text-xs text-muted-foreground">
|
>
|
||||||
{new Date(content.createdAt).toLocaleDateString('fr-FR')}
|
{content.author.displayName || content.author.username}
|
||||||
</span>
|
</Link>
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
<Button variant="ghost" size="icon" className="ml-auto h-8 w-8">
|
{new Date(content.createdAt).toLocaleDateString("fr-FR")}
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
</span>
|
||||||
</Button>
|
</div>
|
||||||
</CardHeader>
|
<Button variant="ghost" size="icon" className="ml-auto h-8 w-8">
|
||||||
<CardContent className="p-0 relative bg-zinc-100 dark:bg-zinc-900 aspect-square flex items-center justify-center">
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<Link href={`/meme/${content.slug}`} className="w-full h-full relative">
|
</Button>
|
||||||
{content.type === "image" ? (
|
</CardHeader>
|
||||||
<Image
|
<CardContent className="p-0 relative bg-zinc-100 dark:bg-zinc-900 aspect-square flex items-center justify-center">
|
||||||
src={content.url}
|
<Link href={`/meme/${content.slug}`} className="w-full h-full relative">
|
||||||
alt={content.title}
|
{content.type === "image" ? (
|
||||||
fill
|
<Image
|
||||||
className="object-contain"
|
src={content.url}
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
alt={content.title}
|
||||||
/>
|
fill
|
||||||
) : (
|
className="object-contain"
|
||||||
<video
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
src={content.url}
|
/>
|
||||||
controls={false}
|
) : (
|
||||||
autoPlay
|
<video
|
||||||
muted
|
src={content.url}
|
||||||
loop
|
controls={false}
|
||||||
className="w-full h-full object-contain"
|
autoPlay
|
||||||
/>
|
muted
|
||||||
)}
|
loop
|
||||||
</Link>
|
className="w-full h-full object-contain"
|
||||||
</CardContent>
|
/>
|
||||||
<CardFooter className="p-4 flex flex-col gap-4">
|
)}
|
||||||
<div className="w-full flex items-center justify-between">
|
</Link>
|
||||||
<div className="flex items-center gap-2">
|
</CardContent>
|
||||||
<Button
|
<CardFooter className="p-4 flex flex-col gap-4">
|
||||||
variant="ghost"
|
<div className="w-full flex items-center justify-between">
|
||||||
size="sm"
|
<div className="flex items-center gap-2">
|
||||||
className={`gap-1.5 h-8 ${isLiked ? 'text-red-500 hover:text-red-600' : ''}`}
|
<Button
|
||||||
onClick={handleLike}
|
variant="ghost"
|
||||||
>
|
size="sm"
|
||||||
<Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} />
|
className={`gap-1.5 h-8 ${isLiked ? "text-red-500 hover:text-red-600" : ""}`}
|
||||||
<span className="text-xs">{likesCount}</span>
|
onClick={handleLike}
|
||||||
</Button>
|
>
|
||||||
<Button variant="ghost" size="sm" className="gap-1.5 h-8">
|
<Heart className={`h-4 w-4 ${isLiked ? "fill-current" : ""}`} />
|
||||||
<Eye className="h-4 w-4" />
|
<span className="text-xs">{likesCount}</span>
|
||||||
<span className="text-xs">{content.views}</span>
|
</Button>
|
||||||
</Button>
|
<Button variant="ghost" size="sm" className="gap-1.5 h-8">
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
<Eye className="h-4 w-4" />
|
||||||
<Share2 className="h-4 w-4" />
|
<span className="text-xs">{content.views}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
<Button size="sm" variant="secondary" className="text-xs h-8">
|
<Share2 className="h-4 w-4" />
|
||||||
Utiliser
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
<Button size="sm" variant="secondary" className="text-xs h-8">
|
||||||
|
Utiliser
|
||||||
<div className="w-full space-y-2">
|
</Button>
|
||||||
<h3 className="font-medium text-sm line-clamp-2">{content.title}</h3>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{content.tags.slice(0, 3).map((tag, i) => (
|
<div className="w-full space-y-2">
|
||||||
<Badge key={typeof tag === 'string' ? tag : tag.id} variant="secondary" className="text-[10px] py-0 px-1.5">
|
<h3 className="font-medium text-sm line-clamp-2">{content.title}</h3>
|
||||||
#{typeof tag === 'string' ? tag : tag.name}
|
<div className="flex flex-wrap gap-1">
|
||||||
</Badge>
|
{content.tags.slice(0, 3).map((tag, _i) => (
|
||||||
))}
|
<Badge
|
||||||
</div>
|
key={typeof tag === "string" ? tag : tag.id}
|
||||||
</div>
|
variant="secondary"
|
||||||
</CardFooter>
|
className="text-[10px] py-0 px-1.5"
|
||||||
</Card>
|
>
|
||||||
);
|
#{typeof tag === "string" ? tag : tag.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,93 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ContentCard } from "@/components/content-card";
|
|
||||||
import { ContentService } from "@/services/content.service";
|
|
||||||
import type { Content, PaginatedResponse } from "@/types/content";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
|
|
||||||
import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll";
|
import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll";
|
||||||
|
import { ContentCard } from "@/components/content-card";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import type { Content, PaginatedResponse } from "@/types/content";
|
||||||
|
|
||||||
interface ContentListProps {
|
interface ContentListProps {
|
||||||
fetchFn: (params: { limit: number; offset: number }) => Promise<PaginatedResponse<Content>>;
|
fetchFn: (params: {
|
||||||
title?: string;
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}) => Promise<PaginatedResponse<Content>>;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContentList({ fetchFn, title }: ContentListProps) {
|
export function ContentList({ fetchFn, title }: ContentListProps) {
|
||||||
const [contents, setContents] = React.useState<Content[]>([]);
|
const [contents, setContents] = React.useState<Content[]>([]);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [offset, setOffset] = React.useState(0);
|
const [offset, setOffset] = React.useState(0);
|
||||||
const [hasMore, setHasMore] = React.useState(true);
|
const [hasMore, setHasMore] = React.useState(true);
|
||||||
|
|
||||||
const loadMore = React.useCallback(async () => {
|
const loadMore = React.useCallback(async () => {
|
||||||
if (!hasMore || loading) return;
|
if (!hasMore || loading) return;
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await fetchFn({
|
|
||||||
limit: 10,
|
|
||||||
offset: offset + 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
setContents(prev => [...prev, ...response.data]);
|
|
||||||
setOffset(prev => prev + 10);
|
|
||||||
setHasMore(response.data.length === 10);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load more contents:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [offset, hasMore, loading, fetchFn]);
|
|
||||||
|
|
||||||
const { loaderRef } = useInfiniteScroll({
|
setLoading(true);
|
||||||
hasMore,
|
try {
|
||||||
loading,
|
const response = await fetchFn({
|
||||||
onLoadMore: loadMore,
|
limit: 10,
|
||||||
});
|
offset: offset + 10,
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
setContents((prev) => [...prev, ...response.data]);
|
||||||
const fetchInitial = async () => {
|
setOffset((prev) => prev + 10);
|
||||||
setLoading(true);
|
setHasMore(response.data.length === 10);
|
||||||
try {
|
} catch (error) {
|
||||||
const response = await fetchFn({
|
console.error("Failed to load more contents:", error);
|
||||||
limit: 10,
|
} finally {
|
||||||
offset: 0,
|
setLoading(false);
|
||||||
});
|
}
|
||||||
setContents(response.data);
|
}, [offset, hasMore, loading, fetchFn]);
|
||||||
setHasMore(response.data.length === 10);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch contents:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchInitial();
|
const { loaderRef } = useInfiniteScroll({
|
||||||
}, [fetchFn]);
|
hasMore,
|
||||||
|
loading,
|
||||||
|
onLoadMore: loadMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchInitial = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetchFn({
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
setContents(response.data);
|
||||||
|
setHasMore(response.data.length === 10);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch contents:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
fetchInitial();
|
||||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
}, [fetchFn]);
|
||||||
{title && <h1 className="text-2xl font-bold">{title}</h1>}
|
|
||||||
<div className="flex flex-col gap-6">
|
return (
|
||||||
{contents.map((content) => (
|
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
||||||
<ContentCard key={content.id} content={content} />
|
{title && <h1 className="text-2xl font-bold">{title}</h1>}
|
||||||
))}
|
<div className="flex flex-col gap-6">
|
||||||
</div>
|
{contents.map((content) => (
|
||||||
|
<ContentCard key={content.id} content={content} />
|
||||||
<div ref={loaderRef} className="py-8 flex justify-center">
|
))}
|
||||||
{loading && <Spinner className="h-8 w-8 text-primary" />}
|
</div>
|
||||||
{!hasMore && contents.length > 0 && (
|
|
||||||
<p className="text-muted-foreground text-sm italic">Vous avez atteint la fin ! 🐐</p>
|
<div ref={loaderRef} className="py-8 flex justify-center">
|
||||||
)}
|
{loading && <Spinner className="h-8 w-8 text-primary" />}
|
||||||
{!loading && contents.length === 0 && (
|
{!hasMore && contents.length > 0 && (
|
||||||
<p className="text-muted-foreground text-sm italic">Aucun mème trouvé ici... pour l'instant !</p>
|
<p className="text-muted-foreground text-sm italic">
|
||||||
)}
|
Vous avez atteint la fin ! 🐐
|
||||||
</div>
|
</p>
|
||||||
</div>
|
)}
|
||||||
);
|
{!loading && contents.length === 0 && (
|
||||||
|
<p className="text-muted-foreground text-sm italic">
|
||||||
|
Aucun mème trouvé ici... pour l'instant !
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export function ContentSkeleton() {
|
export function ContentSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden border-none shadow-sm">
|
<Card className="overflow-hidden border-none shadow-sm">
|
||||||
<CardHeader className="p-4 flex flex-row items-center space-y-0 gap-3">
|
<CardHeader className="p-4 flex flex-row items-center space-y-0 gap-3">
|
||||||
<Skeleton className="h-8 w-8 rounded-full" />
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Skeleton className="h-3 w-24" />
|
<Skeleton className="h-3 w-24" />
|
||||||
<Skeleton className="h-2 w-16" />
|
<Skeleton className="h-2 w-16" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0 aspect-square">
|
<CardContent className="p-0 aspect-square">
|
||||||
<Skeleton className="h-full w-full" />
|
<Skeleton className="h-full w-full" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="p-4 flex flex-col gap-4">
|
<CardFooter className="p-4 flex flex-col gap-4">
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Skeleton className="h-8 w-12 rounded-md" />
|
<Skeleton className="h-8 w-12 rounded-md" />
|
||||||
<Skeleton className="h-8 w-12 rounded-md" />
|
<Skeleton className="h-8 w-12 rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-8 w-20 rounded-md" />
|
<Skeleton className="h-8 w-20 rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-2">
|
<div className="w-full space-y-2">
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-4 w-full" />
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Skeleton className="h-4 w-12" />
|
<Skeleton className="h-4 w-12" />
|
||||||
<Skeleton className="h-4 w-12" />
|
<Skeleton className="h-4 w-12" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ContentList } from "@/components/content-list";
|
import { ContentList } from "@/components/content-list";
|
||||||
import { ContentService } from "@/services/content.service";
|
import { ContentService } from "@/services/content.service";
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
|
|
||||||
export function HomeContent() {
|
export function HomeContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const sort = (searchParams.get("sort") as "trend" | "recent") || "trend";
|
|
||||||
const category = searchParams.get("category") || undefined;
|
|
||||||
const tag = searchParams.get("tag") || undefined;
|
|
||||||
const query = searchParams.get("query") || undefined;
|
|
||||||
|
|
||||||
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
const sort = (searchParams.get("sort") as "trend" | "recent") || "trend";
|
||||||
ContentService.getExplore({
|
const category = searchParams.get("category") || undefined;
|
||||||
...params,
|
const tag = searchParams.get("tag") || undefined;
|
||||||
sort,
|
const query = searchParams.get("query") || undefined;
|
||||||
category,
|
|
||||||
tag,
|
|
||||||
query
|
|
||||||
}),
|
|
||||||
[sort, category, tag, query]);
|
|
||||||
|
|
||||||
const title = query
|
const fetchFn = React.useCallback(
|
||||||
? `Résultats pour "${query}"`
|
(params: { limit: number; offset: number }) =>
|
||||||
: category
|
ContentService.getExplore({
|
||||||
? `Catégorie : ${category}`
|
...params,
|
||||||
: sort === "trend"
|
sort,
|
||||||
? "Tendances du moment"
|
category,
|
||||||
: "Nouveautés";
|
tag,
|
||||||
|
query,
|
||||||
|
}),
|
||||||
|
[sort, category, tag, query],
|
||||||
|
);
|
||||||
|
|
||||||
return <ContentList fetchFn={fetchFn} title={title} />;
|
const title = query
|
||||||
|
? `Résultats pour "${query}"`
|
||||||
|
: category
|
||||||
|
? `Catégorie : ${category}`
|
||||||
|
: sort === "trend"
|
||||||
|
? "Tendances du moment"
|
||||||
|
: "Nouveautés";
|
||||||
|
|
||||||
|
return <ContentList fetchFn={fetchFn} title={title} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +1,153 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { Filter, Search } from "lucide-react";
|
import { Filter, Search } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import {
|
import * as React from "react";
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from "@/components/ui/sheet";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
|
|
||||||
export function MobileFilters() {
|
export function MobileFilters() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
|
||||||
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
if (open) {
|
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
||||||
CategoryService.getAll().then(setCategories).catch(console.error);
|
const [open, setOpen] = React.useState(false);
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const updateSearch = React.useCallback((name: string, value: string | null) => {
|
React.useEffect(() => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
if (open) {
|
||||||
if (value) {
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
||||||
params.set(name, value);
|
}
|
||||||
} else {
|
}, [open]);
|
||||||
params.delete(name);
|
|
||||||
}
|
|
||||||
router.push(`${pathname}?${params.toString()}`);
|
|
||||||
}, [router, pathname, searchParams]);
|
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const updateSearch = React.useCallback(
|
||||||
e.preventDefault();
|
(name: string, value: string | null) => {
|
||||||
updateSearch("query", query);
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
setOpen(false);
|
if (value) {
|
||||||
};
|
params.set(name, value);
|
||||||
|
} else {
|
||||||
|
params.delete(name);
|
||||||
|
}
|
||||||
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
|
},
|
||||||
|
[router, pathname, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
const currentSort = searchParams.get("sort") || "trend";
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
const currentCategory = searchParams.get("category");
|
e.preventDefault();
|
||||||
|
updateSearch("query", query);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const currentSort = searchParams.get("sort") || "trend";
|
||||||
<div className="lg:hidden fixed top-4 right-4 z-50">
|
const currentCategory = searchParams.get("category");
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
|
||||||
<SheetTrigger asChild>
|
return (
|
||||||
<Button size="icon" className="rounded-full shadow-lg h-12 w-12">
|
<div className="lg:hidden fixed top-4 right-4 z-50">
|
||||||
<Filter className="h-6 w-6" />
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
</Button>
|
<SheetTrigger asChild>
|
||||||
</SheetTrigger>
|
<Button size="icon" className="rounded-full shadow-lg h-12 w-12">
|
||||||
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
|
<Filter className="h-6 w-6" />
|
||||||
<SheetHeader>
|
</Button>
|
||||||
<SheetTitle>Recherche & Filtres</SheetTitle>
|
</SheetTrigger>
|
||||||
</SheetHeader>
|
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
|
||||||
<div className="mt-6 space-y-6">
|
<SheetHeader>
|
||||||
<form onSubmit={handleSearch} className="relative">
|
<SheetTitle>Recherche & Filtres</SheetTitle>
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
</SheetHeader>
|
||||||
<Input
|
<div className="mt-6 space-y-6">
|
||||||
type="search"
|
<form onSubmit={handleSearch} className="relative">
|
||||||
placeholder="Rechercher des mèmes..."
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
className="pl-8"
|
<Input
|
||||||
value={query}
|
type="search"
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
placeholder="Rechercher des mèmes..."
|
||||||
/>
|
className="pl-8"
|
||||||
</form>
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
/>
|
||||||
<div className="space-y-6 pr-4">
|
</form>
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium mb-3">Trier par</h3>
|
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="space-y-6 pr-4">
|
||||||
<Badge
|
<div>
|
||||||
variant={currentSort === "trend" ? "default" : "outline"}
|
<h3 className="text-sm font-medium mb-3">Trier par</h3>
|
||||||
className="cursor-pointer"
|
<div className="flex flex-wrap gap-2">
|
||||||
onClick={() => updateSearch("sort", "trend")}
|
<Badge
|
||||||
>
|
variant={currentSort === "trend" ? "default" : "outline"}
|
||||||
Tendances
|
className="cursor-pointer"
|
||||||
</Badge>
|
onClick={() => updateSearch("sort", "trend")}
|
||||||
<Badge
|
>
|
||||||
variant={currentSort === "recent" ? "default" : "outline"}
|
Tendances
|
||||||
className="cursor-pointer"
|
</Badge>
|
||||||
onClick={() => updateSearch("sort", "recent")}
|
<Badge
|
||||||
>
|
variant={currentSort === "recent" ? "default" : "outline"}
|
||||||
Récent
|
className="cursor-pointer"
|
||||||
</Badge>
|
onClick={() => updateSearch("sort", "recent")}
|
||||||
</div>
|
>
|
||||||
</div>
|
Récent
|
||||||
<Separator />
|
</Badge>
|
||||||
<div>
|
</div>
|
||||||
<h3 className="text-sm font-medium mb-3">Catégories</h3>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<Separator />
|
||||||
<Badge
|
<div>
|
||||||
variant={!currentCategory ? "default" : "outline"}
|
<h3 className="text-sm font-medium mb-3">Catégories</h3>
|
||||||
className="cursor-pointer"
|
<div className="flex flex-wrap gap-2">
|
||||||
onClick={() => updateSearch("category", null)}
|
<Badge
|
||||||
>
|
variant={!currentCategory ? "default" : "outline"}
|
||||||
Tout
|
className="cursor-pointer"
|
||||||
</Badge>
|
onClick={() => updateSearch("category", null)}
|
||||||
{categories.map((cat) => (
|
>
|
||||||
<Badge
|
Tout
|
||||||
key={cat.id}
|
</Badge>
|
||||||
variant={currentCategory === cat.slug ? "default" : "outline"}
|
{categories.map((cat) => (
|
||||||
className="cursor-pointer"
|
<Badge
|
||||||
onClick={() => updateSearch("category", cat.slug)}
|
key={cat.id}
|
||||||
>
|
variant={currentCategory === cat.slug ? "default" : "outline"}
|
||||||
{cat.name}
|
className="cursor-pointer"
|
||||||
</Badge>
|
onClick={() => updateSearch("category", cat.slug)}
|
||||||
))}
|
>
|
||||||
</div>
|
{cat.name}
|
||||||
</div>
|
</Badge>
|
||||||
<Separator />
|
))}
|
||||||
<div>
|
</div>
|
||||||
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<Separator />
|
||||||
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(tag => (
|
<div>
|
||||||
<Badge
|
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
||||||
key={tag}
|
<div className="flex flex-wrap gap-2">
|
||||||
variant={searchParams.get("tag") === tag ? "default" : "outline"}
|
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
|
||||||
className="cursor-pointer"
|
(tag) => (
|
||||||
onClick={() => updateSearch("tag", searchParams.get("tag") === tag ? null : tag)}
|
<Badge
|
||||||
>
|
key={tag}
|
||||||
#{tag}
|
variant={searchParams.get("tag") === tag ? "default" : "outline"}
|
||||||
</Badge>
|
className="cursor-pointer"
|
||||||
))}
|
onClick={() =>
|
||||||
</div>
|
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
|
||||||
</div>
|
}
|
||||||
</div>
|
>
|
||||||
</ScrollArea>
|
#{tag}
|
||||||
</div>
|
</Badge>
|
||||||
</SheetContent>
|
),
|
||||||
</Sheet>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,139 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Filter, Search } from "lucide-react";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Search, Filter } from "lucide-react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { CategoryService } from "@/services/category.service";
|
import { CategoryService } from "@/services/category.service";
|
||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
|
|
||||||
export function SearchSidebar() {
|
export function SearchSidebar() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
|
||||||
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||||
CategoryService.getAll().then(setCategories).catch(console.error);
|
const [query, setQuery] = React.useState(searchParams.get("query") || "");
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateSearch = React.useCallback((name: string, value: string | null) => {
|
React.useEffect(() => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
||||||
if (value) {
|
}, []);
|
||||||
params.set(name, value);
|
|
||||||
} else {
|
|
||||||
params.delete(name);
|
|
||||||
}
|
|
||||||
// If we are not on explore/trends/recent, maybe we should redirect to home?
|
|
||||||
// For now, let's just update the URL.
|
|
||||||
router.push(`${pathname}?${params.toString()}`);
|
|
||||||
}, [router, pathname, searchParams]);
|
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const updateSearch = React.useCallback(
|
||||||
e.preventDefault();
|
(name: string, value: string | null) => {
|
||||||
updateSearch("query", query);
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
};
|
if (value) {
|
||||||
|
params.set(name, value);
|
||||||
|
} else {
|
||||||
|
params.delete(name);
|
||||||
|
}
|
||||||
|
// If we are not on explore/trends/recent, maybe we should redirect to home?
|
||||||
|
// For now, let's just update the URL.
|
||||||
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
|
},
|
||||||
|
[router, pathname, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
const currentSort = searchParams.get("sort") || "trend";
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
const currentCategory = searchParams.get("category");
|
e.preventDefault();
|
||||||
|
updateSearch("query", query);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const currentSort = searchParams.get("sort") || "trend";
|
||||||
<aside className="hidden lg:flex flex-col w-80 border-l bg-background">
|
const currentCategory = searchParams.get("category");
|
||||||
<div className="p-4 border-b">
|
|
||||||
<h2 className="font-semibold mb-4">Rechercher</h2>
|
|
||||||
<form onSubmit={handleSearch} className="relative">
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="search"
|
|
||||||
placeholder="Rechercher des mèmes..."
|
|
||||||
className="pl-8"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<ScrollArea className="flex-1 p-4">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium mb-3 flex items-center gap-2">
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
Filtres
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-muted-foreground mb-2">Trier par</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Badge
|
|
||||||
variant={currentSort === "trend" ? "default" : "outline"}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => updateSearch("sort", "trend")}
|
|
||||||
>
|
|
||||||
Tendances
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant={currentSort === "recent" ? "default" : "outline"}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => updateSearch("sort", "recent")}
|
|
||||||
>
|
|
||||||
Récent
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-muted-foreground mb-2">Catégories</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Badge
|
|
||||||
variant={!currentCategory ? "default" : "outline"}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => updateSearch("category", null)}
|
|
||||||
>
|
|
||||||
Tout
|
|
||||||
</Badge>
|
|
||||||
{categories.map((cat) => (
|
|
||||||
<Badge
|
|
||||||
key={cat.id}
|
|
||||||
variant={currentCategory === cat.slug ? "default" : "outline"}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => updateSearch("category", cat.slug)}
|
|
||||||
>
|
|
||||||
{cat.name}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return (
|
||||||
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
<aside className="hidden lg:flex flex-col w-80 border-l bg-background">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="p-4 border-b">
|
||||||
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(tag => (
|
<h2 className="font-semibold mb-4">Rechercher</h2>
|
||||||
<Badge
|
<form onSubmit={handleSearch} className="relative">
|
||||||
key={tag}
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
variant={searchParams.get("tag") === tag ? "default" : "outline"}
|
<Input
|
||||||
className="cursor-pointer hover:bg-secondary"
|
type="search"
|
||||||
onClick={() => updateSearch("tag", searchParams.get("tag") === tag ? null : tag)}
|
placeholder="Rechercher des mèmes..."
|
||||||
>
|
className="pl-8"
|
||||||
#{tag}
|
value={query}
|
||||||
</Badge>
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
))}
|
/>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ScrollArea className="flex-1 p-4">
|
||||||
</ScrollArea>
|
<div className="space-y-6">
|
||||||
</aside>
|
<div>
|
||||||
);
|
<h3 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Filtres
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">Trier par</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
variant={currentSort === "trend" ? "default" : "outline"}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => updateSearch("sort", "trend")}
|
||||||
|
>
|
||||||
|
Tendances
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={currentSort === "recent" ? "default" : "outline"}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => updateSearch("sort", "recent")}
|
||||||
|
>
|
||||||
|
Récent
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">Catégories</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
variant={!currentCategory ? "default" : "outline"}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => updateSearch("category", null)}
|
||||||
|
>
|
||||||
|
Tout
|
||||||
|
</Badge>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<Badge
|
||||||
|
key={cat.id}
|
||||||
|
variant={currentCategory === cat.slug ? "default" : "outline"}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => updateSearch("category", cat.slug)}
|
||||||
|
>
|
||||||
|
{cat.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-3">Tags populaires</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
|
||||||
|
(tag) => (
|
||||||
|
<Badge
|
||||||
|
key={tag}
|
||||||
|
variant={searchParams.get("tag") === tag ? "default" : "outline"}
|
||||||
|
className="cursor-pointer hover:bg-secondary"
|
||||||
|
onClick={() =>
|
||||||
|
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Accordion({
|
function Accordion({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccordionItem({
|
function AccordionItem({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
data-slot="accordion-item"
|
data-slot="accordion-item"
|
||||||
className={cn("border-b last:border-b-0", className)}
|
className={cn("border-b last:border-b-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccordionTrigger({
|
function AccordionTrigger({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
data-slot="accordion-trigger"
|
data-slot="accordion-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccordionContent({
|
function AccordionContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
data-slot="accordion-content"
|
data-slot="accordion-content"
|
||||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
|
|||||||
@@ -1,157 +1,156 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import type * as React from "react";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
|
||||||
|
|
||||||
function AlertDialog({
|
function AlertDialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogTrigger({
|
function AlertDialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogPortal({
|
function AlertDialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
function AlertDialogOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
data-slot="alert-dialog-overlay"
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogContent({
|
function AlertDialogContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay />
|
<AlertDialogOverlay />
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
data-slot="alert-dialog-content"
|
data-slot="alert-dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogHeader({
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-dialog-header"
|
data-slot="alert-dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogFooter({
|
function AlertDialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-dialog-footer"
|
data-slot="alert-dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogTitle({
|
function AlertDialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Title
|
<AlertDialogPrimitive.Title
|
||||||
data-slot="alert-dialog-title"
|
data-slot="alert-dialog-title"
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogDescription({
|
function AlertDialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
data-slot="alert-dialog-description"
|
data-slot="alert-dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogAction({
|
function AlertDialogAction({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogCancel({
|
function AlertDialogCancel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
AlertDialogPortal,
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
import * as React from "react"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-card text-card-foreground",
|
default: "bg-card text-card-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Alert({
|
function Alert({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert"
|
data-slot="alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-title"
|
data-slot="alert-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDescription({
|
function AlertDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-description"
|
data-slot="alert-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||||
|
|
||||||
function AspectRatio({
|
function AspectRatio({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AspectRatio }
|
export { AspectRatio };
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Avatar({
|
function Avatar({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
data-slot="avatar"
|
data-slot="avatar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarImage({
|
function AvatarImage({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
data-slot="avatar-image"
|
data-slot="avatar-image"
|
||||||
className={cn("aspect-square size-full", className)}
|
className={cn("aspect-square size-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarFallback({
|
function AvatarFallback({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
data-slot="avatar-fallback"
|
data-slot="avatar-fallback"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import * as React from "react"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span"> &
|
}: React.ComponentProps<"span"> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "span"
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="badge"
|
data-slot="badge"
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@@ -1,109 +1,109 @@
|
|||||||
import * as React from "react"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
return (
|
return (
|
||||||
<ol
|
<ol
|
||||||
data-slot="breadcrumb-list"
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-slot="breadcrumb-item"
|
data-slot="breadcrumb-item"
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbLink({
|
function BreadcrumbLink({
|
||||||
asChild,
|
asChild,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"a"> & {
|
}: React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="breadcrumb-link"
|
data-slot="breadcrumb-link"
|
||||||
className={cn("hover:text-foreground transition-colors", className)}
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="breadcrumb-page"
|
data-slot="breadcrumb-page"
|
||||||
role="link"
|
role="link"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
className={cn("text-foreground font-normal", className)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
function BreadcrumbSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) {
|
}: React.ComponentProps<"li">) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-slot="breadcrumb-separator"
|
data-slot="breadcrumb-separator"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <ChevronRight />}
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
function BreadcrumbEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="breadcrumb-ellipsis"
|
data-slot="breadcrumb-ellipsis"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="size-4" />
|
<MoreHorizontal className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,83 +1,82 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
horizontal:
|
horizontal:
|
||||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||||
vertical:
|
vertical:
|
||||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function ButtonGroup({
|
function ButtonGroup({
|
||||||
className,
|
className,
|
||||||
orientation,
|
orientation,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="button-group"
|
data-slot="button-group"
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupText({
|
function ButtonGroupText({
|
||||||
className,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupSeparator({
|
function ButtonGroupSeparator({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
return (
|
return (
|
||||||
<Separator
|
<Separator
|
||||||
data-slot="button-group-separator"
|
data-slot="button-group-separator"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ButtonGroupSeparator,
|
ButtonGroupSeparator,
|
||||||
ButtonGroupText,
|
ButtonGroupText,
|
||||||
buttonGroupVariants,
|
buttonGroupVariants,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
import * as React from "react"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
ghost:
|
||||||
ghost:
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
},
|
||||||
},
|
size: {
|
||||||
size: {
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
icon: "size-9",
|
||||||
icon: "size-9",
|
"icon-sm": "size-8",
|
||||||
"icon-sm": "size-8",
|
"icon-lg": "size-10",
|
||||||
"icon-lg": "size-10",
|
},
|
||||||
},
|
},
|
||||||
},
|
defaultVariants: {
|
||||||
defaultVariants: {
|
variant: "default",
|
||||||
variant: "default",
|
size: "default",
|
||||||
size: "default",
|
},
|
||||||
},
|
},
|
||||||
}
|
);
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "default",
|
size = "default",
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -1,220 +1,209 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
DayPicker,
|
type DayButton,
|
||||||
getDefaultClassNames,
|
DayPicker,
|
||||||
type DayButton,
|
getDefaultClassNames,
|
||||||
} from "react-day-picker"
|
} from "react-day-picker";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
captionLayout = "label",
|
captionLayout = "label",
|
||||||
buttonVariant = "ghost",
|
buttonVariant = "ghost",
|
||||||
formatters,
|
formatters,
|
||||||
components,
|
components,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayPicker> & {
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
||||||
}) {
|
}) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
captionLayout={captionLayout}
|
captionLayout={captionLayout}
|
||||||
formatters={{
|
formatters={{
|
||||||
formatMonthDropdown: (date) =>
|
formatMonthDropdown: (date) =>
|
||||||
date.toLocaleString("default", { month: "short" }),
|
date.toLocaleString("default", { month: "short" }),
|
||||||
...formatters,
|
...formatters,
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
root: cn("w-fit", defaultClassNames.root),
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
months: cn(
|
months: cn(
|
||||||
"flex gap-4 flex-col md:flex-row relative",
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
defaultClassNames.months
|
defaultClassNames.months,
|
||||||
),
|
),
|
||||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
nav: cn(
|
nav: cn(
|
||||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
defaultClassNames.nav
|
defaultClassNames.nav,
|
||||||
),
|
),
|
||||||
button_previous: cn(
|
button_previous: cn(
|
||||||
buttonVariants({ variant: buttonVariant }),
|
buttonVariants({ variant: buttonVariant }),
|
||||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
defaultClassNames.button_previous
|
defaultClassNames.button_previous,
|
||||||
),
|
),
|
||||||
button_next: cn(
|
button_next: cn(
|
||||||
buttonVariants({ variant: buttonVariant }),
|
buttonVariants({ variant: buttonVariant }),
|
||||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
defaultClassNames.button_next
|
defaultClassNames.button_next,
|
||||||
),
|
),
|
||||||
month_caption: cn(
|
month_caption: cn(
|
||||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||||
defaultClassNames.month_caption
|
defaultClassNames.month_caption,
|
||||||
),
|
),
|
||||||
dropdowns: cn(
|
dropdowns: cn(
|
||||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||||
defaultClassNames.dropdowns
|
defaultClassNames.dropdowns,
|
||||||
),
|
),
|
||||||
dropdown_root: cn(
|
dropdown_root: cn(
|
||||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
defaultClassNames.dropdown_root
|
defaultClassNames.dropdown_root,
|
||||||
),
|
),
|
||||||
dropdown: cn(
|
dropdown: cn(
|
||||||
"absolute bg-popover inset-0 opacity-0",
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
defaultClassNames.dropdown
|
defaultClassNames.dropdown,
|
||||||
),
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
"select-none font-medium",
|
"select-none font-medium",
|
||||||
captionLayout === "label"
|
captionLayout === "label"
|
||||||
? "text-sm"
|
? "text-sm"
|
||||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||||
defaultClassNames.caption_label
|
defaultClassNames.caption_label,
|
||||||
),
|
),
|
||||||
table: "w-full border-collapse",
|
table: "w-full border-collapse",
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||||
defaultClassNames.weekday
|
defaultClassNames.weekday,
|
||||||
),
|
),
|
||||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||||
week_number_header: cn(
|
week_number_header: cn(
|
||||||
"select-none w-(--cell-size)",
|
"select-none w-(--cell-size)",
|
||||||
defaultClassNames.week_number_header
|
defaultClassNames.week_number_header,
|
||||||
),
|
),
|
||||||
week_number: cn(
|
week_number: cn(
|
||||||
"text-[0.8rem] select-none text-muted-foreground",
|
"text-[0.8rem] select-none text-muted-foreground",
|
||||||
defaultClassNames.week_number
|
defaultClassNames.week_number,
|
||||||
),
|
),
|
||||||
day: cn(
|
day: cn(
|
||||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
props.showWeekNumber
|
props.showWeekNumber
|
||||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||||
defaultClassNames.day
|
defaultClassNames.day,
|
||||||
),
|
),
|
||||||
range_start: cn(
|
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
|
||||||
"rounded-l-md bg-accent",
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
defaultClassNames.range_start
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
),
|
today: cn(
|
||||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
defaultClassNames.today,
|
||||||
today: cn(
|
),
|
||||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
outside: cn(
|
||||||
defaultClassNames.today
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
),
|
defaultClassNames.outside,
|
||||||
outside: cn(
|
),
|
||||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
disabled: cn(
|
||||||
defaultClassNames.outside
|
"text-muted-foreground opacity-50",
|
||||||
),
|
defaultClassNames.disabled,
|
||||||
disabled: cn(
|
),
|
||||||
"text-muted-foreground opacity-50",
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
defaultClassNames.disabled
|
...classNames,
|
||||||
),
|
}}
|
||||||
hidden: cn("invisible", defaultClassNames.hidden),
|
components={{
|
||||||
...classNames,
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
}}
|
return (
|
||||||
components={{
|
<div
|
||||||
Root: ({ className, rootRef, ...props }) => {
|
data-slot="calendar"
|
||||||
return (
|
ref={rootRef}
|
||||||
<div
|
className={cn(className)}
|
||||||
data-slot="calendar"
|
{...props}
|
||||||
ref={rootRef}
|
/>
|
||||||
className={cn(className)}
|
);
|
||||||
{...props}
|
},
|
||||||
/>
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
)
|
if (orientation === "left") {
|
||||||
},
|
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
|
||||||
Chevron: ({ className, orientation, ...props }) => {
|
}
|
||||||
if (orientation === "left") {
|
|
||||||
return (
|
|
||||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orientation === "right") {
|
if (orientation === "right") {
|
||||||
return (
|
return (
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon className={cn("size-4", className)} {...props} />
|
||||||
className={cn("size-4", className)}
|
);
|
||||||
{...props}
|
}
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
|
||||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
},
|
||||||
)
|
DayButton: CalendarDayButton,
|
||||||
},
|
WeekNumber: ({ children, ...props }) => {
|
||||||
DayButton: CalendarDayButton,
|
return (
|
||||||
WeekNumber: ({ children, ...props }) => {
|
<td {...props}>
|
||||||
return (
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
<td {...props}>
|
{children}
|
||||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
</div>
|
||||||
{children}
|
</td>
|
||||||
</div>
|
);
|
||||||
</td>
|
},
|
||||||
)
|
...components,
|
||||||
},
|
}}
|
||||||
...components,
|
{...props}
|
||||||
}}
|
/>
|
||||||
{...props}
|
);
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarDayButton({
|
function CalendarDayButton({
|
||||||
className,
|
className,
|
||||||
day,
|
day,
|
||||||
modifiers,
|
modifiers,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayButton>) {
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
const ref = React.useRef<HTMLButtonElement>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (modifiers.focused) ref.current?.focus()
|
if (modifiers.focused) ref.current?.focus();
|
||||||
}, [modifiers.focused])
|
}, [modifiers.focused]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
data-day={day.date.toLocaleDateString()}
|
data-day={day.date.toLocaleDateString()}
|
||||||
data-selected-single={
|
data-selected-single={
|
||||||
modifiers.selected &&
|
modifiers.selected &&
|
||||||
!modifiers.range_start &&
|
!modifiers.range_start &&
|
||||||
!modifiers.range_end &&
|
!modifiers.range_end &&
|
||||||
!modifiers.range_middle
|
!modifiers.range_middle
|
||||||
}
|
}
|
||||||
data-range-start={modifiers.range_start}
|
data-range-start={modifiers.range_start}
|
||||||
data-range-end={modifiers.range_end}
|
data-range-end={modifiers.range_end}
|
||||||
data-range-middle={modifiers.range_middle}
|
data-range-middle={modifiers.range_middle}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||||
defaultClassNames.day,
|
defaultClassNames.day,
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Calendar, CalendarDayButton }
|
export { Calendar, CalendarDayButton };
|
||||||
|
|||||||
@@ -1,92 +1,88 @@
|
|||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
className={cn("leading-none font-semibold", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-slot="card-content" className={cn("px-6", className)} {...props} />
|
||||||
data-slot="card-content"
|
);
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,241 +1,240 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
} from "embla-carousel-react"
|
} from "embla-carousel-react";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
import { Button } from "@/components/ui/button"
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
||||||
type CarouselOptions = UseCarouselParameters[0]
|
|
||||||
type CarouselPlugin = UseCarouselParameters[1]
|
|
||||||
|
|
||||||
type CarouselProps = {
|
type CarouselProps = {
|
||||||
opts?: CarouselOptions
|
opts?: CarouselOptions;
|
||||||
plugins?: CarouselPlugin
|
plugins?: CarouselPlugin;
|
||||||
orientation?: "horizontal" | "vertical"
|
orientation?: "horizontal" | "vertical";
|
||||||
setApi?: (api: CarouselApi) => void
|
setApi?: (api: CarouselApi) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type CarouselContextProps = {
|
type CarouselContextProps = {
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
scrollPrev: () => void
|
scrollPrev: () => void;
|
||||||
scrollNext: () => void
|
scrollNext: () => void;
|
||||||
canScrollPrev: boolean
|
canScrollPrev: boolean;
|
||||||
canScrollNext: boolean
|
canScrollNext: boolean;
|
||||||
} & CarouselProps
|
} & CarouselProps;
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
function useCarousel() {
|
function useCarousel() {
|
||||||
const context = React.useContext(CarouselContext)
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useCarousel must be used within a <Carousel />")
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Carousel({
|
function Carousel({
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
opts,
|
opts,
|
||||||
setApi,
|
setApi,
|
||||||
plugins,
|
plugins,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
},
|
},
|
||||||
plugins
|
plugins,
|
||||||
)
|
);
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) return
|
if (!api) return;
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
setCanScrollNext(api.canScrollNext())
|
setCanScrollNext(api.canScrollNext());
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
const scrollPrev = React.useCallback(() => {
|
||||||
api?.scrollPrev()
|
api?.scrollPrev();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
const scrollNext = React.useCallback(() => {
|
||||||
api?.scrollNext()
|
api?.scrollNext();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === "ArrowLeft") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollPrev()
|
scrollPrev();
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === "ArrowRight") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollNext()
|
scrollNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scrollPrev, scrollNext]
|
[scrollPrev, scrollNext],
|
||||||
)
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api || !setApi) return
|
if (!api || !setApi) return;
|
||||||
setApi(api)
|
setApi(api);
|
||||||
}, [api, setApi])
|
}, [api, setApi]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api) return
|
if (!api) return;
|
||||||
onSelect(api)
|
onSelect(api);
|
||||||
api.on("reInit", onSelect)
|
api.on("reInit", onSelect);
|
||||||
api.on("select", onSelect)
|
api.on("select", onSelect);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
api?.off("select", onSelect)
|
api?.off("select", onSelect);
|
||||||
}
|
};
|
||||||
}, [api, onSelect])
|
}, [api, onSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContext.Provider
|
<CarouselContext.Provider
|
||||||
value={{
|
value={{
|
||||||
carouselRef,
|
carouselRef,
|
||||||
api: api,
|
api: api,
|
||||||
opts,
|
opts,
|
||||||
orientation:
|
orientation:
|
||||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
scrollPrev,
|
scrollPrev,
|
||||||
scrollNext,
|
scrollNext,
|
||||||
canScrollPrev,
|
canScrollPrev,
|
||||||
canScrollNext,
|
canScrollNext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
role="region"
|
role="region"
|
||||||
aria-roledescription="carousel"
|
aria-roledescription="carousel"
|
||||||
data-slot="carousel"
|
data-slot="carousel"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { carouselRef, orientation } = useCarousel()
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
data-slot="carousel-content"
|
data-slot="carousel-content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"flex",
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { orientation } = useCarousel()
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
data-slot="carousel-item"
|
data-slot="carousel-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselPrevious({
|
function CarouselPrevious({
|
||||||
className,
|
className,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-slot="carousel-previous"
|
data-slot="carousel-previous"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute size-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -left-12 -translate-y-1/2"
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselNext({
|
function CarouselNext({
|
||||||
className,
|
className,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-slot="carousel-next"
|
data-slot="carousel-next"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute size-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -right-12 -translate-y-1/2"
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
CarouselItem,
|
CarouselItem,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
CarouselNext,
|
CarouselNext,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,357 +1,356 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as RechartsPrimitive from "recharts"
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
[k in string]: {
|
[k in string]: {
|
||||||
label?: React.ReactNode
|
label?: React.ReactNode;
|
||||||
icon?: React.ComponentType
|
icon?: React.ComponentType;
|
||||||
} & (
|
} & (
|
||||||
| { color?: string; theme?: never }
|
| { color?: string; theme?: never }
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
type ChartContextProps = {
|
type ChartContextProps = {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
function useChart() {
|
function useChart() {
|
||||||
const context = React.useContext(ChartContext)
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChartContainer({
|
function ChartContainer({
|
||||||
id,
|
id,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
config,
|
config,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
children: React.ComponentProps<
|
children: React.ComponentProps<
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
>["children"]
|
>["children"];
|
||||||
}) {
|
}) {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId();
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
data-slot="chart"
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
<ChartStyle id={chartId} config={config} />
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
{children}
|
{children}
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(
|
||||||
([, config]) => config.theme || config.color
|
([, config]) => config.theme || config.color,
|
||||||
)
|
);
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
if (!colorConfig.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<style
|
<style
|
||||||
dangerouslySetInnerHTML={{
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: no comment...
|
||||||
__html: Object.entries(THEMES)
|
dangerouslySetInnerHTML={{
|
||||||
.map(
|
__html: Object.entries(THEMES)
|
||||||
([theme, prefix]) => `
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
${prefix} [data-chart=${id}] {
|
${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
.map(([key, itemConfig]) => {
|
||||||
const color =
|
const color =
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
itemConfig.color
|
itemConfig.color;
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
})
|
})
|
||||||
.join("\n")}
|
.join("\n")}
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
function ChartTooltipContent({
|
function ChartTooltipContent({
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
className,
|
className,
|
||||||
indicator = "dot",
|
indicator = "dot",
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
hideIndicator = false,
|
hideIndicator = false,
|
||||||
label,
|
label,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
formatter,
|
formatter,
|
||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean;
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean;
|
||||||
indicator?: "line" | "dot" | "dashed"
|
indicator?: "line" | "dot" | "dashed";
|
||||||
nameKey?: string
|
nameKey?: string;
|
||||||
labelKey?: string
|
labelKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { config } = useChart()
|
const { config } = useChart();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
if (hideLabel || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [item] = payload
|
const [item] = payload;
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const value =
|
const value =
|
||||||
!labelKey && typeof label === "string"
|
!labelKey && typeof label === "string"
|
||||||
? config[label as keyof typeof config]?.label || label
|
? config[label as keyof typeof config]?.label || label
|
||||||
: itemConfig?.label
|
: itemConfig?.label;
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
{labelFormatter(value, payload)}
|
{labelFormatter(value, payload)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
}, [
|
}, [
|
||||||
label,
|
label,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
payload,
|
payload,
|
||||||
hideLabel,
|
hideLabel,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
config,
|
config,
|
||||||
labelKey,
|
labelKey,
|
||||||
])
|
]);
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
if (!active || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!nestLabel ? tooltipLabel : null}
|
{!nestLabel ? tooltipLabel : null}
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter((item) => item.type !== "none")
|
||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={item.dataKey}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{itemConfig?.icon ? (
|
{itemConfig?.icon ? (
|
||||||
<itemConfig.icon />
|
<itemConfig.icon />
|
||||||
) : (
|
) : (
|
||||||
!hideIndicator && (
|
!hideIndicator && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||||
{
|
{
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
"w-1": indicator === "line",
|
"w-1": indicator === "line",
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
indicator === "dashed",
|
indicator === "dashed",
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--color-bg": indicatorColor,
|
"--color-bg": indicatorColor,
|
||||||
"--color-border": indicatorColor,
|
"--color-border": indicatorColor,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 justify-between leading-none",
|
"flex flex-1 justify-between leading-none",
|
||||||
nestLabel ? "items-end" : "items-center"
|
nestLabel ? "items-end" : "items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
{nestLabel ? tooltipLabel : null}
|
{nestLabel ? tooltipLabel : null}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{itemConfig?.label || item.name}
|
{itemConfig?.label || item.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
{item.value.toLocaleString()}
|
{item.value.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
function ChartLegendContent({
|
function ChartLegendContent({
|
||||||
className,
|
className,
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
payload,
|
payload,
|
||||||
verticalAlign = "bottom",
|
verticalAlign = "bottom",
|
||||||
nameKey,
|
nameKey,
|
||||||
}: React.ComponentProps<"div"> &
|
}: React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean;
|
||||||
nameKey?: string
|
nameKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { config } = useChart()
|
const { config } = useChart();
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center gap-4",
|
"flex items-center justify-center gap-4",
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter((item) => item.type !== "none")
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
<itemConfig.icon />
|
<itemConfig.icon />
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: item.color,
|
backgroundColor: item.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{itemConfig?.label}
|
{itemConfig?.label}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(
|
||||||
config: ChartConfig,
|
config: ChartConfig,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
key: string
|
key: string,
|
||||||
) {
|
) {
|
||||||
if (typeof payload !== "object" || payload === null) {
|
if (typeof payload !== "object" || payload === null) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadPayload =
|
const payloadPayload =
|
||||||
"payload" in payload &&
|
"payload" in payload &&
|
||||||
typeof payload.payload === "object" &&
|
typeof payload.payload === "object" &&
|
||||||
payload.payload !== null
|
payload.payload !== null
|
||||||
? payload.payload
|
? payload.payload
|
||||||
: undefined
|
: undefined;
|
||||||
|
|
||||||
let configLabelKey: string = key
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
key in payload &&
|
key in payload &&
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
) {
|
) {
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
} else if (
|
} else if (
|
||||||
payloadPayload &&
|
payloadPayload &&
|
||||||
key in payloadPayload &&
|
key in payloadPayload &&
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
) {
|
) {
|
||||||
configLabelKey = payloadPayload[
|
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
|
||||||
key as keyof typeof payloadPayload
|
}
|
||||||
] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
return configLabelKey in config
|
||||||
? config[configLabelKey]
|
? config[configLabelKey]
|
||||||
: config[key as keyof typeof config]
|
: config[key as keyof typeof config];
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
ChartStyle,
|
ChartStyle,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import { CheckIcon } from "lucide-react";
|
||||||
import { CheckIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Checkbox({
|
function Checkbox({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
data-slot="checkbox-indicator"
|
data-slot="checkbox-indicator"
|
||||||
className="grid place-content-center text-current transition-none"
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckIcon className="size-3.5" />
|
<CheckIcon className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox };
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||||
|
|
||||||
function Collapsible({
|
function Collapsible({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollapsibleTrigger({
|
function CollapsibleTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
return (
|
return (
|
||||||
<CollapsiblePrimitive.CollapsibleTrigger
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
data-slot="collapsible-trigger"
|
data-slot="collapsible-trigger"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollapsibleContent({
|
function CollapsibleContent({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
return (
|
return (
|
||||||
<CollapsiblePrimitive.CollapsibleContent
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
data-slot="collapsible-content"
|
data-slot="collapsible-content"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
|
|||||||
@@ -1,184 +1,183 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { SearchIcon } from "lucide-react";
|
||||||
import { SearchIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Command({
|
function Command({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
data-slot="command"
|
data-slot="command"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandDialog({
|
function CommandDialog({
|
||||||
title = "Command Palette",
|
title = "Command Palette",
|
||||||
description = "Search for a command to run...",
|
description = "Search for a command to run...",
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Dialog> & {
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
title?: string
|
title?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogDescription>{description}</DialogDescription>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn("overflow-hidden p-0", className)}
|
className={cn("overflow-hidden p-0", className)}
|
||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
>
|
>
|
||||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandInput({
|
function CommandInput({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="command-input-wrapper"
|
data-slot="command-input-wrapper"
|
||||||
className="flex h-9 items-center gap-2 border-b px-3"
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
>
|
>
|
||||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandList({
|
function CommandList({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.List
|
<CommandPrimitive.List
|
||||||
data-slot="command-list"
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandEmpty({
|
function CommandEmpty({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Empty
|
<CommandPrimitive.Empty
|
||||||
data-slot="command-empty"
|
data-slot="command-empty"
|
||||||
className="py-6 text-center text-sm"
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandGroup({
|
function CommandGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandSeparator({
|
function CommandSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Separator
|
<CommandPrimitive.Separator
|
||||||
data-slot="command-separator"
|
data-slot="command-separator"
|
||||||
className={cn("bg-border -mx-1 h-px", className)}
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItem({
|
function CommandItem({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandShortcut({
|
function CommandShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="command-shortcut"
|
data-slot="command-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,252 +1,252 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function ContextMenu({
|
function ContextMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuTrigger({
|
function ContextMenuTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuGroup({
|
function ContextMenuGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuPortal({
|
function ContextMenuPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSub({
|
function ContextMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuRadioGroup({
|
function ContextMenuRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.RadioGroup
|
<ContextMenuPrimitive.RadioGroup
|
||||||
data-slot="context-menu-radio-group"
|
data-slot="context-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSubTrigger({
|
function ContextMenuSubTrigger({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.SubTrigger
|
<ContextMenuPrimitive.SubTrigger
|
||||||
data-slot="context-menu-sub-trigger"
|
data-slot="context-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
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 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",
|
"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 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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto" />
|
<ChevronRightIcon className="ml-auto" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSubContent({
|
function ContextMenuSubContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.SubContent
|
<ContextMenuPrimitive.SubContent
|
||||||
data-slot="context-menu-sub-content"
|
data-slot="context-menu-sub-content"
|
||||||
className={cn(
|
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-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
"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-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuContent({
|
function ContextMenuContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Portal>
|
<ContextMenuPrimitive.Portal>
|
||||||
<ContextMenuPrimitive.Content
|
<ContextMenuPrimitive.Content
|
||||||
data-slot="context-menu-content"
|
data-slot="context-menu-content"
|
||||||
className={cn(
|
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-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"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-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</ContextMenuPrimitive.Portal>
|
</ContextMenuPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuItem({
|
function ContextMenuItem({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive"
|
variant?: "default" | "destructive";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Item
|
<ContextMenuPrimitive.Item
|
||||||
data-slot="context-menu-item"
|
data-slot="context-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuCheckboxItem({
|
function ContextMenuCheckboxItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
data-slot="context-menu-checkbox-item"
|
data-slot="context-menu-checkbox-item"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuRadioItem({
|
function ContextMenuRadioItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.RadioItem
|
<ContextMenuPrimitive.RadioItem
|
||||||
data-slot="context-menu-radio-item"
|
data-slot="context-menu-radio-item"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<CircleIcon className="size-2 fill-current" />
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.RadioItem>
|
</ContextMenuPrimitive.RadioItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuLabel({
|
function ContextMenuLabel({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Label
|
<ContextMenuPrimitive.Label
|
||||||
data-slot="context-menu-label"
|
data-slot="context-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSeparator({
|
function ContextMenuSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Separator
|
<ContextMenuPrimitive.Separator
|
||||||
data-slot="context-menu-separator"
|
data-slot="context-menu-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuShortcut({
|
function ContextMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="context-menu-shortcut"
|
data-slot="context-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuCheckboxItem,
|
ContextMenuCheckboxItem,
|
||||||
ContextMenuRadioItem,
|
ContextMenuRadioItem,
|
||||||
ContextMenuLabel,
|
ContextMenuLabel,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuShortcut,
|
ContextMenuShortcut,
|
||||||
ContextMenuGroup,
|
ContextMenuGroup,
|
||||||
ContextMenuPortal,
|
ContextMenuPortal,
|
||||||
ContextMenuSub,
|
ContextMenuSub,
|
||||||
ContextMenuSubContent,
|
ContextMenuSubContent,
|
||||||
ContextMenuSubTrigger,
|
ContextMenuSubTrigger,
|
||||||
ContextMenuRadioGroup,
|
ContextMenuRadioGroup,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,143 +1,143 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { XIcon } from "lucide-react";
|
||||||
import { XIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,135 +1,135 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
import { Drawer as DrawerPrimitive } from "vaul";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Drawer({
|
function Drawer({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerTrigger({
|
function DrawerTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerPortal({
|
function DrawerPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerClose({
|
function DrawerClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerOverlay({
|
function DrawerOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<DrawerPrimitive.Overlay
|
<DrawerPrimitive.Overlay
|
||||||
data-slot="drawer-overlay"
|
data-slot="drawer-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerContent({
|
function DrawerContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<DrawerPortal data-slot="drawer-portal">
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerPrimitive.Content
|
<DrawerPrimitive.Content
|
||||||
data-slot="drawer-content"
|
data-slot="drawer-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
{children}
|
{children}
|
||||||
</DrawerPrimitive.Content>
|
</DrawerPrimitive.Content>
|
||||||
</DrawerPortal>
|
</DrawerPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="drawer-header"
|
data-slot="drawer-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="drawer-footer"
|
data-slot="drawer-footer"
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerTitle({
|
function DrawerTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<DrawerPrimitive.Title
|
<DrawerPrimitive.Title
|
||||||
data-slot="drawer-title"
|
data-slot="drawer-title"
|
||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawerDescription({
|
function DrawerDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<DrawerPrimitive.Description
|
<DrawerPrimitive.Description
|
||||||
data-slot="drawer-description"
|
data-slot="drawer-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerPortal,
|
DrawerPortal,
|
||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,257 +1,254 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
function DropdownMenuPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
function DropdownMenuTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Trigger
|
<DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
||||||
data-slot="dropdown-menu-trigger"
|
);
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuContent({
|
function DropdownMenuContent({
|
||||||
className,
|
className,
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
function DropdownMenuGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuItem({
|
function DropdownMenuItem({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive"
|
variant?: "default" | "destructive";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
data-slot="dropdown-menu-item"
|
data-slot="dropdown-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
function DropdownMenuRadioItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<CircleIcon className="size-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
function DropdownMenuLabel({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
function DropdownMenuSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSub({
|
function DropdownMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
function DropdownMenuSubTrigger({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
function DropdownMenuSubContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,104 +1,105 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty"
|
data-slot="empty"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-header"
|
data-slot="empty-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
const emptyMediaVariants = cva(
|
||||||
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
icon:
|
||||||
},
|
"bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
},
|
||||||
variant: "default",
|
defaultVariants: {
|
||||||
},
|
variant: "default",
|
||||||
}
|
},
|
||||||
)
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function EmptyMedia({
|
function EmptyMedia({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-icon"
|
data-slot="empty-icon"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-title"
|
data-slot="empty-title"
|
||||||
className={cn("text-lg font-medium tracking-tight", className)}
|
className={cn("text-lg font-medium tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-description"
|
data-slot="empty-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-content"
|
data-slot="empty-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,248 +1,246 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { useMemo } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils"
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Label } from "@/components/ui/label"
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||||
return (
|
return (
|
||||||
<fieldset
|
<fieldset
|
||||||
data-slot="field-set"
|
data-slot="field-set"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-6",
|
"flex flex-col gap-6",
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLegend({
|
function FieldLegend({
|
||||||
className,
|
className,
|
||||||
variant = "legend",
|
variant = "legend",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||||
return (
|
return (
|
||||||
<legend
|
<legend
|
||||||
data-slot="field-legend"
|
data-slot="field-legend"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-3 font-medium",
|
"mb-3 font-medium",
|
||||||
"data-[variant=legend]:text-base",
|
"data-[variant=legend]:text-base",
|
||||||
"data-[variant=label]:text-sm",
|
"data-[variant=label]:text-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-group"
|
data-slot="field-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldVariants = cva(
|
const fieldVariants = cva(
|
||||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||||
horizontal: [
|
horizontal: [
|
||||||
"flex-row items-center",
|
"flex-row items-center",
|
||||||
"[&>[data-slot=field-label]]:flex-auto",
|
"[&>[data-slot=field-label]]:flex-auto",
|
||||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
],
|
],
|
||||||
responsive: [
|
responsive: [
|
||||||
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
||||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
orientation: "vertical",
|
orientation: "vertical",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Field({
|
function Field({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="field"
|
data-slot="field"
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
className={cn(fieldVariants({ orientation }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-content"
|
data-slot="field-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLabel({
|
function FieldLabel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Label>) {
|
}: React.ComponentProps<typeof Label>) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="field-description"
|
data-slot="field-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldSeparator({
|
function FieldSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-separator"
|
data-slot="field-separator"
|
||||||
data-content={!!children}
|
data-content={!!children}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
{children && (
|
{children && (
|
||||||
<span
|
<span
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||||
data-slot="field-separator-content"
|
data-slot="field-separator-content"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldError({
|
function FieldError({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
errors,
|
errors,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
errors?: Array<{ message?: string } | undefined>
|
errors?: Array<{ message?: string } | undefined>;
|
||||||
}) {
|
}) {
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return children
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!errors?.length) {
|
if (!errors?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueErrors = [
|
const uniqueErrors = [
|
||||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||||
]
|
];
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
if (uniqueErrors?.length === 1) {
|
||||||
return uniqueErrors[0]?.message
|
return uniqueErrors[0]?.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||||
{uniqueErrors.map(
|
{uniqueErrors.map(
|
||||||
(error, index) =>
|
(error) => error?.message && <li key={error.message}>{error.message}</li>,
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
)}
|
||||||
)}
|
</ul>
|
||||||
</ul>
|
);
|
||||||
)
|
}, [children, errors]);
|
||||||
}, [children, errors])
|
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-slot="field-error"
|
data-slot="field-error"
|
||||||
className={cn("text-destructive text-sm font-normal", className)}
|
className={cn("text-destructive text-sm font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Field,
|
Field,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
FieldGroup,
|
FieldGroup,
|
||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldContent,
|
FieldContent,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,167 +1,164 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import type * as LabelPrimitive from "@radix-ui/react-label"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
FormProvider,
|
type ControllerProps,
|
||||||
useFormContext,
|
type FieldPath,
|
||||||
useFormState,
|
type FieldValues,
|
||||||
type ControllerProps,
|
FormProvider,
|
||||||
type FieldPath,
|
useFormContext,
|
||||||
type FieldValues,
|
useFormState,
|
||||||
} from "react-hook-form"
|
} from "react-hook-form";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
const Form = FormProvider;
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
|
|
||||||
const Form = FormProvider
|
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = {
|
> = {
|
||||||
name: TName
|
name: TName;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue
|
{} as FormFieldContextValue,
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
const itemContext = React.useContext(FormItemContext)
|
const itemContext = React.useContext(FormItemContext);
|
||||||
const { getFieldState } = useFormContext()
|
const { getFieldState } = useFormContext();
|
||||||
const formState = useFormState({ name: fieldContext.name })
|
const formState = useFormState({ name: fieldContext.name });
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
throw new Error("useFormField should be used within <FormField>")
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = itemContext
|
const { id } = itemContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: fieldContext.name,
|
name: fieldContext.name,
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type FormItemContextValue = {
|
type FormItemContextValue = {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue
|
{} as FormItemContextValue,
|
||||||
)
|
);
|
||||||
|
|
||||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const id = React.useId()
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div
|
<div
|
||||||
data-slot="form-item"
|
data-slot="form-item"
|
||||||
className={cn("grid gap-2", className)}
|
className={cn("grid gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormLabel({
|
function FormLabel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="form-label"
|
data-slot="form-label"
|
||||||
data-error={!!error}
|
data-error={!!error}
|
||||||
className={cn("data-[error=true]:text-destructive", className)}
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
data-slot="form-control"
|
data-slot="form-control"
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
|
||||||
? `${formDescriptionId}`
|
}
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
aria-invalid={!!error}
|
||||||
}
|
{...props}
|
||||||
aria-invalid={!!error}
|
/>
|
||||||
{...props}
|
);
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="form-description"
|
data-slot="form-description"
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
const { error, formMessageId } = useFormField()
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message ?? "") : props.children
|
const body = error ? String(error?.message ?? "") : props.children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="form-message"
|
data-slot="form-message"
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn("text-destructive text-sm", className)}
|
className={cn("text-destructive text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField,
|
FormField,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function HoverCard({
|
function HoverCard({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoverCardTrigger({
|
function HoverCardTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoverCardContent({
|
function HoverCardContent({
|
||||||
className,
|
className,
|
||||||
align = "center",
|
align = "center",
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||||
<HoverCardPrimitive.Content
|
<HoverCardPrimitive.Content
|
||||||
data-slot="hover-card-content"
|
data-slot="hover-card-content"
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"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 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</HoverCardPrimitive.Portal>
|
</HoverCardPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
|
|||||||
@@ -1,170 +1,177 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils"
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Input } from "@/components/ui/input"
|
import { cn } from "@/lib/utils";
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||||
|
|
||||||
// Focus state.
|
// Focus state.
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||||
|
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
"inline-start":
|
"inline-start":
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||||
"inline-end":
|
"inline-end":
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||||
"block-start":
|
"block-start":
|
||||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||||
"block-end":
|
"block-end":
|
||||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
align: "inline-start",
|
align: "inline-start",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function InputGroupAddon({
|
function InputGroupAddon({
|
||||||
className,
|
className,
|
||||||
align = "inline-start",
|
align = "inline-start",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="input-group-addon"
|
data-slot="input-group-addon"
|
||||||
data-align={align}
|
data-align={align}
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||||
}}
|
}}
|
||||||
{...props}
|
onKeyDown={(e) => {
|
||||||
/>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
)
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
const inputGroupButtonVariants = cva(
|
||||||
"text-sm shadow-none flex gap-2 items-center",
|
"text-sm shadow-none flex gap-2 items-center",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
||||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
||||||
"icon-xs":
|
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
},
|
||||||
},
|
},
|
||||||
},
|
defaultVariants: {
|
||||||
defaultVariants: {
|
size: "xs",
|
||||||
size: "xs",
|
},
|
||||||
},
|
},
|
||||||
}
|
);
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupButton({
|
function InputGroupButton({
|
||||||
className,
|
className,
|
||||||
type = "button",
|
type = "button",
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "xs",
|
size = "xs",
|
||||||
...props
|
...props
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type={type}
|
type={type}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupInput({
|
function InputGroupInput({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"input">) {
|
}: React.ComponentProps<"input">) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupTextarea({
|
function InputGroupTextarea({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"textarea">) {
|
}: React.ComponentProps<"textarea">) {
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { OTPInput, OTPInputContext } from "input-otp";
|
||||||
import { OTPInput, OTPInputContext } from "input-otp"
|
import { MinusIcon } from "lucide-react";
|
||||||
import { MinusIcon } from "lucide-react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function InputOTP({
|
function InputOTP({
|
||||||
className,
|
className,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof OTPInput> & {
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
containerClassName?: string
|
containerClassName?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<OTPInput
|
<OTPInput
|
||||||
data-slot="input-otp"
|
data-slot="input-otp"
|
||||||
containerClassName={cn(
|
containerClassName={cn(
|
||||||
"flex items-center gap-2 has-disabled:opacity-50",
|
"flex items-center gap-2 has-disabled:opacity-50",
|
||||||
containerClassName
|
containerClassName,
|
||||||
)}
|
)}
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
className={cn("disabled:cursor-not-allowed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="input-otp-group"
|
data-slot="input-otp-group"
|
||||||
className={cn("flex items-center", className)}
|
className={cn("flex items-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputOTPSlot({
|
function InputOTPSlot({
|
||||||
index,
|
index,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
index: number
|
index: number;
|
||||||
}) {
|
}) {
|
||||||
const inputOTPContext = React.useContext(OTPInputContext)
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="input-otp-slot"
|
data-slot="input-otp-slot"
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{char}
|
{char}
|
||||||
{hasFakeCaret && (
|
{hasFakeCaret && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|||||||
@@ -1,193 +1,193 @@
|
|||||||
import * as React from "react"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="list"
|
role="list"
|
||||||
data-slot="item-group"
|
data-slot="item-group"
|
||||||
className={cn("group/item-group flex flex-col", className)}
|
className={cn("group/item-group flex flex-col", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemSeparator({
|
function ItemSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
return (
|
return (
|
||||||
<Separator
|
<Separator
|
||||||
data-slot="item-separator"
|
data-slot="item-separator"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
className={cn("my-0", className)}
|
className={cn("my-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemVariants = cva(
|
const itemVariants = cva(
|
||||||
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
outline: "border-border",
|
outline: "border-border",
|
||||||
muted: "bg-muted/50",
|
muted: "bg-muted/50",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "p-4 gap-4 ",
|
default: "p-4 gap-4 ",
|
||||||
sm: "py-3 px-4 gap-2.5",
|
sm: "py-3 px-4 gap-2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Item({
|
function Item({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "default",
|
size = "default",
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> &
|
}: React.ComponentProps<"div"> &
|
||||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="item"
|
data-slot="item"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(itemVariants({ variant, size, className }))}
|
className={cn(itemVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemMediaVariants = cva(
|
const itemMediaVariants = cva(
|
||||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
icon:
|
||||||
image:
|
"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
||||||
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
|
image:
|
||||||
},
|
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
},
|
||||||
variant: "default",
|
defaultVariants: {
|
||||||
},
|
variant: "default",
|
||||||
}
|
},
|
||||||
)
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function ItemMedia({
|
function ItemMedia({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-media"
|
data-slot="item-media"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(itemMediaVariants({ variant, className }))}
|
className={cn(itemMediaVariants({ variant, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-content"
|
data-slot="item-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-title"
|
data-slot="item-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="item-description"
|
data-slot="item-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-actions"
|
data-slot="item-actions"
|
||||||
className={cn("flex items-center gap-2", className)}
|
className={cn("flex items-center gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-header"
|
data-slot="item-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex basis-full items-center justify-between gap-2",
|
"flex basis-full items-center justify-between gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="item-footer"
|
data-slot="item-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex basis-full items-center justify-between gap-2",
|
"flex basis-full items-center justify-between gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Item,
|
Item,
|
||||||
ItemMedia,
|
ItemMedia,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
ItemActions,
|
ItemActions,
|
||||||
ItemGroup,
|
ItemGroup,
|
||||||
ItemSeparator,
|
ItemSeparator,
|
||||||
ItemTitle,
|
ItemTitle,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemHeader,
|
ItemHeader,
|
||||||
ItemFooter,
|
ItemFooter,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||||
return (
|
return (
|
||||||
<kbd
|
<kbd
|
||||||
data-slot="kbd"
|
data-slot="kbd"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
||||||
"[&_svg:not([class*='size-'])]:size-3",
|
"[&_svg:not([class*='size-'])]:size-3",
|
||||||
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<kbd
|
<kbd
|
||||||
data-slot="kbd-group"
|
data-slot="kbd-group"
|
||||||
className={cn("inline-flex items-center gap-1", className)}
|
className={cn("inline-flex items-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Kbd, KbdGroup }
|
export { Kbd, KbdGroup };
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Label({
|
function Label({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
|
|||||||
@@ -1,276 +1,276 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Menubar({
|
function Menubar({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.Root
|
<MenubarPrimitive.Root
|
||||||
data-slot="menubar"
|
data-slot="menubar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarMenu({
|
function MenubarMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarGroup({
|
function MenubarGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarPortal({
|
function MenubarPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarRadioGroup({
|
function MenubarRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarTrigger({
|
function MenubarTrigger({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.Trigger
|
<MenubarPrimitive.Trigger
|
||||||
data-slot="menubar-trigger"
|
data-slot="menubar-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarContent({
|
function MenubarContent({
|
||||||
className,
|
className,
|
||||||
align = "start",
|
align = "start",
|
||||||
alignOffset = -4,
|
alignOffset = -4,
|
||||||
sideOffset = 8,
|
sideOffset = 8,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPortal>
|
<MenubarPortal>
|
||||||
<MenubarPrimitive.Content
|
<MenubarPrimitive.Content
|
||||||
data-slot="menubar-content"
|
data-slot="menubar-content"
|
||||||
align={align}
|
align={align}
|
||||||
alignOffset={alignOffset}
|
alignOffset={alignOffset}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in 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-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in 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-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</MenubarPortal>
|
</MenubarPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarItem({
|
function MenubarItem({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive"
|
variant?: "default" | "destructive";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.Item
|
<MenubarPrimitive.Item
|
||||||
data-slot="menubar-item"
|
data-slot="menubar-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarCheckboxItem({
|
function MenubarCheckboxItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.CheckboxItem
|
<MenubarPrimitive.CheckboxItem
|
||||||
data-slot="menubar-checkbox-item"
|
data-slot="menubar-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</MenubarPrimitive.CheckboxItem>
|
</MenubarPrimitive.CheckboxItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarRadioItem({
|
function MenubarRadioItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.RadioItem
|
<MenubarPrimitive.RadioItem
|
||||||
data-slot="menubar-radio-item"
|
data-slot="menubar-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<CircleIcon className="size-2 fill-current" />
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</MenubarPrimitive.RadioItem>
|
</MenubarPrimitive.RadioItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarLabel({
|
function MenubarLabel({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.Label
|
<MenubarPrimitive.Label
|
||||||
data-slot="menubar-label"
|
data-slot="menubar-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarSeparator({
|
function MenubarSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.Separator
|
<MenubarPrimitive.Separator
|
||||||
data-slot="menubar-separator"
|
data-slot="menubar-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarShortcut({
|
function MenubarShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="menubar-shortcut"
|
data-slot="menubar-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarSub({
|
function MenubarSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarSubTrigger({
|
function MenubarSubTrigger({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.SubTrigger
|
<MenubarPrimitive.SubTrigger
|
||||||
data-slot="menubar-sub-trigger"
|
data-slot="menubar-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
</MenubarPrimitive.SubTrigger>
|
</MenubarPrimitive.SubTrigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenubarSubContent({
|
function MenubarSubContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||||
return (
|
return (
|
||||||
<MenubarPrimitive.SubContent
|
<MenubarPrimitive.SubContent
|
||||||
data-slot="menubar-sub-content"
|
data-slot="menubar-sub-content"
|
||||||
className={cn(
|
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-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
"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-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Menubar,
|
Menubar,
|
||||||
MenubarPortal,
|
MenubarPortal,
|
||||||
MenubarMenu,
|
MenubarMenu,
|
||||||
MenubarTrigger,
|
MenubarTrigger,
|
||||||
MenubarContent,
|
MenubarContent,
|
||||||
MenubarGroup,
|
MenubarGroup,
|
||||||
MenubarSeparator,
|
MenubarSeparator,
|
||||||
MenubarLabel,
|
MenubarLabel,
|
||||||
MenubarItem,
|
MenubarItem,
|
||||||
MenubarShortcut,
|
MenubarShortcut,
|
||||||
MenubarCheckboxItem,
|
MenubarCheckboxItem,
|
||||||
MenubarRadioGroup,
|
MenubarRadioGroup,
|
||||||
MenubarRadioItem,
|
MenubarRadioItem,
|
||||||
MenubarSub,
|
MenubarSub,
|
||||||
MenubarSubTrigger,
|
MenubarSubTrigger,
|
||||||
MenubarSubContent,
|
MenubarSubContent,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,168 +1,166 @@
|
|||||||
import * as React from "react"
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
import { cva } from "class-variance-authority";
|
||||||
import { cva } from "class-variance-authority"
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function NavigationMenu({
|
function NavigationMenu({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
viewport = true,
|
viewport = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
viewport?: boolean
|
viewport?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Root
|
<NavigationMenuPrimitive.Root
|
||||||
data-slot="navigation-menu"
|
data-slot="navigation-menu"
|
||||||
data-viewport={viewport}
|
data-viewport={viewport}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{viewport && <NavigationMenuViewport />}
|
{viewport && <NavigationMenuViewport />}
|
||||||
</NavigationMenuPrimitive.Root>
|
</NavigationMenuPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuList({
|
function NavigationMenuList({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.List
|
<NavigationMenuPrimitive.List
|
||||||
data-slot="navigation-menu-list"
|
data-slot="navigation-menu-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuItem({
|
function NavigationMenuItem({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Item
|
<NavigationMenuPrimitive.Item
|
||||||
data-slot="navigation-menu-item"
|
data-slot="navigation-menu-item"
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
const navigationMenuTriggerStyle = cva(
|
||||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||||
)
|
);
|
||||||
|
|
||||||
function NavigationMenuTrigger({
|
function NavigationMenuTrigger({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Trigger
|
<NavigationMenuPrimitive.Trigger
|
||||||
data-slot="navigation-menu-trigger"
|
data-slot="navigation-menu-trigger"
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</NavigationMenuPrimitive.Trigger>
|
</NavigationMenuPrimitive.Trigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuContent({
|
function NavigationMenuContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Content
|
<NavigationMenuPrimitive.Content
|
||||||
data-slot="navigation-menu-content"
|
data-slot="navigation-menu-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuViewport({
|
function NavigationMenuViewport({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("absolute top-full left-0 isolate z-50 flex justify-center")}
|
||||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
>
|
||||||
)}
|
<NavigationMenuPrimitive.Viewport
|
||||||
>
|
data-slot="navigation-menu-viewport"
|
||||||
<NavigationMenuPrimitive.Viewport
|
className={cn(
|
||||||
data-slot="navigation-menu-viewport"
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
className={cn(
|
className,
|
||||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
)}
|
||||||
className
|
{...props}
|
||||||
)}
|
/>
|
||||||
{...props}
|
</div>
|
||||||
/>
|
);
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuLink({
|
function NavigationMenuLink({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Link
|
<NavigationMenuPrimitive.Link
|
||||||
data-slot="navigation-menu-link"
|
data-slot="navigation-menu-link"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationMenuIndicator({
|
function NavigationMenuIndicator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuPrimitive.Indicator
|
<NavigationMenuPrimitive.Indicator
|
||||||
data-slot="navigation-menu-indicator"
|
data-slot="navigation-menu-indicator"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
</NavigationMenuPrimitive.Indicator>
|
</NavigationMenuPrimitive.Indicator>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuContent,
|
NavigationMenuContent,
|
||||||
NavigationMenuTrigger,
|
NavigationMenuTrigger,
|
||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuIndicator,
|
NavigationMenuIndicator,
|
||||||
NavigationMenuViewport,
|
NavigationMenuViewport,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,127 +1,125 @@
|
|||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
import { cn } from "@/lib/utils"
|
import { type Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { buttonVariants, type Button } from "@/components/ui/button"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
role="navigation"
|
aria-label="pagination"
|
||||||
aria-label="pagination"
|
data-slot="pagination"
|
||||||
data-slot="pagination"
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
className={cn("mx-auto flex w-full justify-center", className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationContent({
|
function PaginationContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"ul">) {
|
}: React.ComponentProps<"ul">) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
data-slot="pagination-content"
|
data-slot="pagination-content"
|
||||||
className={cn("flex flex-row items-center gap-1", className)}
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
return <li data-slot="pagination-item" {...props} />
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
type PaginationLinkProps = {
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
React.ComponentProps<"a">
|
React.ComponentProps<"a">;
|
||||||
|
|
||||||
function PaginationLink({
|
function PaginationLink({
|
||||||
className,
|
className,
|
||||||
isActive,
|
isActive,
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: PaginationLinkProps) {
|
}: PaginationLinkProps) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
data-slot="pagination-link"
|
data-slot="pagination-link"
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: isActive ? "outline" : "ghost",
|
variant: isActive ? "outline" : "ghost",
|
||||||
size,
|
size,
|
||||||
}),
|
}),
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationPrevious({
|
function PaginationPrevious({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink>) {
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label="Go to previous page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
<span className="hidden sm:block">Previous</span>
|
<span className="hidden sm:block">Previous</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationNext({
|
function PaginationNext({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink>) {
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label="Go to next page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block">Next</span>
|
<span className="hidden sm:block">Next</span>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationEllipsis({
|
function PaginationEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
data-slot="pagination-ellipsis"
|
data-slot="pagination-ellipsis"
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
<span className="sr-only">More pages</span>
|
<span className="sr-only">More pages</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
PaginationLink,
|
PaginationLink,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Popover({
|
function Popover({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverTrigger({
|
function PopoverTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
className,
|
className,
|
||||||
align = "center",
|
align = "center",
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
data-slot="popover-content"
|
data-slot="popover-content"
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverAnchor({
|
function PopoverAnchor({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Progress({
|
function Progress({
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
data-slot="progress-indicator"
|
data-slot="progress-indicator"
|
||||||
className="bg-primary h-full w-full flex-1 transition-all"
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Progress }
|
export { Progress };
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import { CircleIcon } from "lucide-react";
|
||||||
import { CircleIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function RadioGroup({
|
function RadioGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
data-slot="radio-group"
|
data-slot="radio-group"
|
||||||
className={cn("grid gap-3", className)}
|
className={cn("grid gap-3", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadioGroupItem({
|
function RadioGroupItem({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
data-slot="radio-group-item"
|
data-slot="radio-group-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator
|
<RadioGroupPrimitive.Indicator
|
||||||
data-slot="radio-group-indicator"
|
data-slot="radio-group-indicator"
|
||||||
className="relative flex items-center justify-center"
|
className="relative flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|||||||
@@ -1,58 +1,57 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function ScrollArea({
|
function ScrollArea({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
data-slot="scroll-area"
|
data-slot="scroll-area"
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScrollBar({
|
function ScrollBar({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
data-slot="scroll-area-scrollbar"
|
data-slot="scroll-area-scrollbar"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex touch-none p-px transition-colors select-none",
|
"flex touch-none p-px transition-colors select-none",
|
||||||
orientation === "vertical" &&
|
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
|
||||||
"h-full w-2.5 border-l border-l-transparent",
|
orientation === "horizontal" &&
|
||||||
orientation === "horizontal" &&
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
"h-2.5 flex-col border-t border-t-transparent",
|
className,
|
||||||
className
|
)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
data-slot="scroll-area-thumb"
|
||||||
data-slot="scroll-area-thumb"
|
className="bg-border relative flex-1 rounded-full"
|
||||||
className="bg-border relative flex-1 rounded-full"
|
/>
|
||||||
/>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar };
|
||||||
|
|||||||
@@ -1,190 +1,190 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Select({
|
function Select({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGroup({
|
function SelectGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectValue({
|
function SelectValue({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectTrigger({
|
function SelectTrigger({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = "default",
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default"
|
size?: "sm" | "default";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectContent({
|
function SelectContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = "item-aligned",
|
position = "item-aligned",
|
||||||
align = "center",
|
align = "center",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
"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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
align={align}
|
align={align}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLabel({
|
function SelectLabel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectItem({
|
function SelectItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
data-slot="select-item-indicator"
|
data-slot="select-item-indicator"
|
||||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectSeparator({
|
function SelectSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
function SelectScrollUpButton({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon className="size-4" />
|
<ChevronUpIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
function SelectScrollDownButton({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4" />
|
<ChevronDownIcon className="size-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
decorative = true,
|
decorative = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
data-slot="separator"
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Separator }
|
export { Separator };
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import { XIcon } from "lucide-react";
|
||||||
import { XIcon } from "lucide-react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTrigger({
|
function SheetTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetClose({
|
function SheetClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetPortal({
|
function SheetPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetOverlay({
|
function SheetOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetContent({
|
function SheetContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
side = "right",
|
side = "right",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
side === "right" &&
|
side === "right" &&
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
side === "left" &&
|
side === "left" &&
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
side === "top" &&
|
side === "top" &&
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
side === "bottom" &&
|
side === "bottom" &&
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-header"
|
data-slot="sheet-header"
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-footer"
|
data-slot="sheet-footer"
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTitle({
|
function SheetTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
data-slot="sheet-title"
|
data-slot="sheet-title"
|
||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetDescription({
|
function SheetDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
data-slot="sheet-description"
|
data-slot="sheet-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="skeleton"
|
data-slot="skeleton"
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Skeleton }
|
export { Skeleton };
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Slider({
|
function Slider({
|
||||||
className,
|
className,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
value,
|
value,
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 100,
|
max = 100,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
const _values = React.useMemo(
|
const _values = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
Array.isArray(value)
|
Array.isArray(value)
|
||||||
? value
|
? value
|
||||||
: Array.isArray(defaultValue)
|
: Array.isArray(defaultValue)
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: [min, max],
|
: [min, max],
|
||||||
[value, defaultValue, min, max]
|
[value, defaultValue, min, max],
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
data-slot="slider"
|
data-slot="slider"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track
|
<SliderPrimitive.Track
|
||||||
data-slot="slider-track"
|
data-slot="slider-track"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Range
|
<SliderPrimitive.Range
|
||||||
data-slot="slider-range"
|
data-slot="slider-range"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
{Array.from({ length: _values.length }, (_, index) => (
|
{_values.map((val, index) => (
|
||||||
<SliderPrimitive.Thumb
|
<SliderPrimitive.Thumb
|
||||||
data-slot="slider-thumb"
|
data-slot="slider-thumb"
|
||||||
key={index}
|
key={`thumb-${index}-${val}`}
|
||||||
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Slider }
|
export { Slider };
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
OctagonXIcon,
|
OctagonXIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
info: <InfoIcon className="size-4" />,
|
info: <InfoIcon className="size-4" />,
|
||||||
warning: <TriangleAlertIcon className="size-4" />,
|
warning: <TriangleAlertIcon className="size-4" />,
|
||||||
error: <OctagonXIcon className="size-4" />,
|
error: <OctagonXIcon className="size-4" />,
|
||||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--popover)",
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"--normal-text": "var(--popover-foreground)",
|
||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--border)",
|
||||||
"--border-radius": "var(--radius)",
|
"--border-radius": "var(--radius)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster };
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Loader2Icon } from "lucide-react"
|
import { Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
return (
|
return (
|
||||||
<Loader2Icon
|
<Loader2Icon
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
className={cn("size-4 animate-spin", className)}
|
className={cn("size-4 animate-spin", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Spinner }
|
export { Spinner };
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Switch({
|
function Switch({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
data-slot="switch-thumb"
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
|||||||
@@ -1,116 +1,113 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
||||||
data-slot="table-container"
|
<table
|
||||||
className="relative w-full overflow-x-auto"
|
data-slot="table"
|
||||||
>
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
<table
|
{...props}
|
||||||
data-slot="table"
|
/>
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
</div>
|
||||||
{...props}
|
);
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
data-slot="table-header"
|
data-slot="table-header"
|
||||||
className={cn("[&_tr]:border-b", className)}
|
className={cn("[&_tr]:border-b", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
return (
|
return (
|
||||||
<tbody
|
<tbody
|
||||||
data-slot="table-body"
|
data-slot="table-body"
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
return (
|
return (
|
||||||
<tfoot
|
<tfoot
|
||||||
data-slot="table-footer"
|
data-slot="table-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
data-slot="table-row"
|
data-slot="table-row"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableCaption({
|
function TableCaption({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"caption">) {
|
}: React.ComponentProps<"caption">) {
|
||||||
return (
|
return (
|
||||||
<caption
|
<caption
|
||||||
data-slot="table-caption"
|
data-slot="table-caption"
|
||||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root
|
<TabsPrimitive.Root
|
||||||
data-slot="tabs"
|
data-slot="tabs"
|
||||||
className={cn("flex flex-col gap-2", className)}
|
className={cn("flex flex-col gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
data-slot="tabs-content"
|
data-slot="tabs-content"
|
||||||
className={cn("flex-1 outline-none", className)}
|
className={cn("flex-1 outline-none", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import * as React from "react"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|||||||
@@ -1,83 +1,82 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import { type VariantProps } from "class-variance-authority"
|
import * as React from "react";
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { toggleVariants } from "@/components/ui/toggle"
|
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext<
|
const ToggleGroupContext = React.createContext<
|
||||||
VariantProps<typeof toggleVariants> & {
|
VariantProps<typeof toggleVariants> & {
|
||||||
spacing?: number
|
spacing?: number;
|
||||||
}
|
}
|
||||||
>({
|
>({
|
||||||
size: "default",
|
size: "default",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
spacing: 0,
|
spacing: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
function ToggleGroup({
|
function ToggleGroup({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
spacing = 0,
|
spacing = 0,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
VariantProps<typeof toggleVariants> & {
|
VariantProps<typeof toggleVariants> & {
|
||||||
spacing?: number
|
spacing?: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Root
|
<ToggleGroupPrimitive.Root
|
||||||
data-slot="toggle-group"
|
data-slot="toggle-group"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
data-spacing={spacing}
|
data-spacing={spacing}
|
||||||
style={{ "--gap": spacing } as React.CSSProperties}
|
style={{ "--gap": spacing } as React.CSSProperties}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext.Provider>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleGroupItem({
|
function ToggleGroupItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
VariantProps<typeof toggleVariants>) {
|
VariantProps<typeof toggleVariants>) {
|
||||||
const context = React.useContext(ToggleGroupContext)
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Item
|
<ToggleGroupPrimitive.Item
|
||||||
data-slot="toggle-group-item"
|
data-slot="toggle-group-item"
|
||||||
data-variant={context.variant || variant}
|
data-variant={context.variant || variant}
|
||||||
data-size={context.size || size}
|
data-size={context.size || size}
|
||||||
data-spacing={context.spacing}
|
data-spacing={context.spacing}
|
||||||
className={cn(
|
className={cn(
|
||||||
toggleVariants({
|
toggleVariants({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
size: context.size || size,
|
||||||
}),
|
}),
|
||||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupPrimitive.Item>
|
</ToggleGroupPrimitive.Item>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-2 min-w-9",
|
default: "h-9 px-2 min-w-9",
|
||||||
sm: "h-8 px-1.5 min-w-8",
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
lg: "h-10 px-2.5 min-w-10",
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Toggle({
|
function Toggle({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
VariantProps<typeof toggleVariants>) {
|
VariantProps<typeof toggleVariants>) {
|
||||||
return (
|
return (
|
||||||
<TogglePrimitive.Root
|
<TogglePrimitive.Root
|
||||||
data-slot="toggle"
|
data-slot="toggle"
|
||||||
className={cn(toggleVariants({ variant, size, className }))}
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
export { Toggle, toggleVariants };
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function TooltipProvider({
|
function TooltipProvider({
|
||||||
delayDuration = 0,
|
delayDuration = 0,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
return (
|
return (
|
||||||
<TooltipPrimitive.Provider
|
<TooltipPrimitive.Provider
|
||||||
data-slot="tooltip-provider"
|
data-slot="tooltip-provider"
|
||||||
delayDuration={delayDuration}
|
delayDuration={delayDuration}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tooltip({
|
function Tooltip({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipTrigger({
|
function TooltipTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipContent({
|
function TooltipContent({
|
||||||
className,
|
className,
|
||||||
sideOffset = 0,
|
sideOffset = 0,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
</TooltipPrimitive.Content>
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
export function useIsMobile() {
|
export function useIsMobile() {
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
const onChange = () => {
|
const onChange = () => {
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
}
|
};
|
||||||
mql.addEventListener("change", onChange)
|
mql.addEventListener("change", onChange);
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
return () => mql.removeEventListener("change", onChange)
|
return () => mql.removeEventListener("change", onChange);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return !!isMobile
|
return !!isMobile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000",
|
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000",
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Système anti-spam rudimentaire pour les erreurs répétitives
|
// Système anti-spam rudimentaire pour les erreurs répétitives
|
||||||
@@ -13,26 +13,26 @@ const errorCache = new Map<string, number>();
|
|||||||
const SPAM_THRESHOLD_MS = 2000; // 2 secondes de silence après une erreur sur le même endpoint
|
const SPAM_THRESHOLD_MS = 2000; // 2 secondes de silence après une erreur sur le même endpoint
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
// Nettoyer le cache d'erreur en cas de succès sur cet endpoint
|
// Nettoyer le cache d'erreur en cas de succès sur cet endpoint
|
||||||
const url = response.config.url || "";
|
const url = response.config.url || "";
|
||||||
errorCache.delete(url);
|
errorCache.delete(url);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const url = error.config?.url || "unknown";
|
const url = error.config?.url || "unknown";
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastErrorTime = errorCache.get(url);
|
const lastErrorTime = errorCache.get(url);
|
||||||
|
|
||||||
if (lastErrorTime && now - lastErrorTime < SPAM_THRESHOLD_MS) {
|
if (lastErrorTime && now - lastErrorTime < SPAM_THRESHOLD_MS) {
|
||||||
// Ignorer l'erreur si elle se produit trop rapidement (déjà signalée)
|
// Ignorer l'erreur si elle se produit trop rapidement (déjà signalée)
|
||||||
// On retourne une promesse qui ne se résout jamais ou on rejette avec une marque spéciale
|
// On retourne une promesse qui ne se résout jamais ou on rejette avec une marque spéciale
|
||||||
return new Promise(() => {});
|
return new Promise(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
errorCache.set(url, now);
|
errorCache.set(url, now);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,132 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { UserService } from "@/services/user.service";
|
|
||||||
import { AuthService } from "@/services/auth.service";
|
|
||||||
import type { User } from "@/types/user";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import * as React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AuthService } from "@/services/auth.service";
|
||||||
|
import { UserService } from "@/services/user.service";
|
||||||
|
import type { RegisterPayload } from "@/types/auth";
|
||||||
|
import type { User } from "@/types/user";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (email: string, password: string) => Promise<void>;
|
||||||
register: (payload: any) => Promise<void>;
|
register: (payload: RegisterPayload) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = React.createContext<AuthContextType | null>(null);
|
const AuthContext = React.createContext<AuthContextType | null>(null);
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [user, setUser] = React.useState<User | null>(null);
|
const [user, setUser] = React.useState<User | null>(null);
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const refreshUser = React.useCallback(async () => {
|
const refreshUser = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const userData = await UserService.getMe();
|
const userData = await UserService.getMe();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
refreshUser();
|
refreshUser();
|
||||||
}, [refreshUser]);
|
}, [refreshUser]);
|
||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
await AuthService.login(email, password);
|
await AuthService.login(email, password);
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
toast.success("Connexion réussie !");
|
toast.success("Connexion réussie !");
|
||||||
router.push("/");
|
router.push("/");
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error.response?.data?.message || "Erreur de connexion");
|
let errorMessage = "Erreur de connexion";
|
||||||
throw error;
|
if (
|
||||||
}
|
error &&
|
||||||
};
|
typeof error === "object" &&
|
||||||
|
"response" in error &&
|
||||||
|
error.response &&
|
||||||
|
typeof error.response === "object" &&
|
||||||
|
"data" in error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"message" in error.response.data &&
|
||||||
|
typeof error.response.data.message === "string"
|
||||||
|
) {
|
||||||
|
errorMessage = error.response.data.message;
|
||||||
|
}
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const register = async (payload: any) => {
|
const register = async (payload: RegisterPayload) => {
|
||||||
try {
|
try {
|
||||||
await AuthService.register(payload);
|
await AuthService.register(payload);
|
||||||
toast.success("Inscription réussie ! Vous pouvez maintenant vous connecter.");
|
toast.success(
|
||||||
router.push("/login");
|
"Inscription réussie ! Vous pouvez maintenant vous connecter.",
|
||||||
} catch (error: any) {
|
);
|
||||||
toast.error(error.response?.data?.message || "Erreur d'inscription");
|
router.push("/login");
|
||||||
throw error;
|
} catch (error: unknown) {
|
||||||
}
|
let errorMessage = "Erreur d'inscription";
|
||||||
};
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
"response" in error &&
|
||||||
|
error.response &&
|
||||||
|
typeof error.response === "object" &&
|
||||||
|
"data" in error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"message" in error.response.data &&
|
||||||
|
typeof error.response.data.message === "string"
|
||||||
|
) {
|
||||||
|
errorMessage = error.response.data.message;
|
||||||
|
}
|
||||||
|
toast.error(errorMessage);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await AuthService.logout();
|
await AuthService.logout();
|
||||||
setUser(null);
|
setUser(null);
|
||||||
toast.success("Déconnexion réussie");
|
toast.success("Déconnexion réussie");
|
||||||
router.push("/");
|
router.push("/");
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
toast.error("Erreur lors de la déconnexion");
|
toast.error("Erreur lors de la déconnexion");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthenticated: !!user,
|
isAuthenticated: !!user,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
logout,
|
logout,
|
||||||
refreshUser,
|
refreshUser,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = React.useContext(AuthContext);
|
const context = React.useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useAuth must be used within an AuthProvider");
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import type { LoginResponse } from "@/types/auth";
|
import type { LoginResponse, RegisterPayload } from "@/types/auth";
|
||||||
|
|
||||||
export const AuthService = {
|
export const AuthService = {
|
||||||
async login(email: string, password: string): Promise<LoginResponse> {
|
async login(email: string, password: string): Promise<LoginResponse> {
|
||||||
const { data } = await api.post<LoginResponse>("/auth/login", { email, password });
|
const { data } = await api.post<LoginResponse>("/auth/login", {
|
||||||
return data;
|
email,
|
||||||
},
|
password,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async register(payload: any): Promise<any> {
|
async register(payload: RegisterPayload): Promise<void> {
|
||||||
const { data } = await api.post("/auth/register", payload);
|
await api.post("/auth/register", payload);
|
||||||
return data;
|
},
|
||||||
},
|
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
await api.post("/auth/logout");
|
await api.post("/auth/logout");
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async refresh(): Promise<void> {
|
||||||
await api.post("/auth/refresh");
|
await api.post("/auth/refresh");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import api from "@/lib/api";
|
|||||||
import type { Category } from "@/types/content";
|
import type { Category } from "@/types/content";
|
||||||
|
|
||||||
export const CategoryService = {
|
export const CategoryService = {
|
||||||
async getAll(): Promise<Category[]> {
|
async getAll(): Promise<Category[]> {
|
||||||
const { data } = await api.get<Category[]>("/categories");
|
const { data } = await api.get<Category[]>("/categories");
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getOne(id: string): Promise<Category> {
|
async getOne(id: string): Promise<Category> {
|
||||||
const { data } = await api.get<Category>(`/categories/${id}`);
|
const { data } = await api.get<Category>(`/categories/${id}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,54 +2,63 @@ import api from "@/lib/api";
|
|||||||
import type { Content, PaginatedResponse } from "@/types/content";
|
import type { Content, PaginatedResponse } from "@/types/content";
|
||||||
|
|
||||||
export const ContentService = {
|
export const ContentService = {
|
||||||
async getExplore(params: {
|
async getExplore(params: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
sort?: "trend" | "recent";
|
sort?: "trend" | "recent";
|
||||||
tag?: string;
|
tag?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
}): Promise<PaginatedResponse<Content>> {
|
}): Promise<PaginatedResponse<Content>> {
|
||||||
const { data } = await api.get<PaginatedResponse<Content>>("/contents/explore", {
|
const { data } = await api.get<PaginatedResponse<Content>>(
|
||||||
params,
|
"/contents/explore",
|
||||||
});
|
{
|
||||||
return data;
|
params,
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async getTrends(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
|
async getTrends(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
|
||||||
const { data } = await api.get<PaginatedResponse<Content>>("/contents/trends", {
|
const { data } = await api.get<PaginatedResponse<Content>>(
|
||||||
params: { limit, offset },
|
"/contents/trends",
|
||||||
});
|
{
|
||||||
return data;
|
params: { limit, offset },
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async getRecent(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
|
async getRecent(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
|
||||||
const { data } = await api.get<PaginatedResponse<Content>>("/contents/recent", {
|
const { data } = await api.get<PaginatedResponse<Content>>(
|
||||||
params: { limit, offset },
|
"/contents/recent",
|
||||||
});
|
{
|
||||||
return data;
|
params: { limit, offset },
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async getOne(idOrSlug: string): Promise<Content> {
|
async getOne(idOrSlug: string): Promise<Content> {
|
||||||
const { data } = await api.get<Content>(`/contents/${idOrSlug}`);
|
const { data } = await api.get<Content>(`/contents/${idOrSlug}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async incrementViews(id: string): Promise<void> {
|
async incrementViews(id: string): Promise<void> {
|
||||||
await api.post(`/contents/${id}/view`);
|
await api.post(`/contents/${id}/view`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async incrementUsage(id: string): Promise<void> {
|
async incrementUsage(id: string): Promise<void> {
|
||||||
await api.post(`/contents/${id}/use`);
|
await api.post(`/contents/${id}/use`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async upload(formData: FormData): Promise<Content> {
|
async upload(formData: FormData): Promise<Content> {
|
||||||
const { data } = await api.post<Content>("/contents/upload", formData, {
|
const { data } = await api.post<Content>("/contents/upload", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ import api from "@/lib/api";
|
|||||||
import type { Content, PaginatedResponse } from "@/types/content";
|
import type { Content, PaginatedResponse } from "@/types/content";
|
||||||
|
|
||||||
export const FavoriteService = {
|
export const FavoriteService = {
|
||||||
async add(contentId: string): Promise<void> {
|
async add(contentId: string): Promise<void> {
|
||||||
await api.post(`/favorites/${contentId}`);
|
await api.post(`/favorites/${contentId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(contentId: string): Promise<void> {
|
async remove(contentId: string): Promise<void> {
|
||||||
await api.delete(`/favorites/${contentId}`);
|
await api.delete(`/favorites/${contentId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async list(params: { limit: number; offset: number }): Promise<PaginatedResponse<Content>> {
|
async list(params: {
|
||||||
const { data } = await api.get<PaginatedResponse<Content>>("/favorites", { params });
|
limit: number;
|
||||||
return data;
|
offset: number;
|
||||||
},
|
}): Promise<PaginatedResponse<Content>> {
|
||||||
|
const { data } = await api.get<PaginatedResponse<Content>>("/favorites", {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import type { User, UserProfile } from "@/types/user";
|
import type { User } from "@/types/user";
|
||||||
|
|
||||||
export const UserService = {
|
export const UserService = {
|
||||||
async getMe(): Promise<User> {
|
async getMe(): Promise<User> {
|
||||||
const { data } = await api.get<User>("/users/me");
|
const { data } = await api.get<User>("/users/me");
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getProfile(username: string): Promise<User> {
|
async getProfile(username: string): Promise<User> {
|
||||||
const { data } = await api.get<User>(`/users/public/${username}`);
|
const { data } = await api.get<User>(`/users/public/${username}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateMe(update: Partial<User>): Promise<User> {
|
async updateMe(update: Partial<User>): Promise<User> {
|
||||||
const { data } = await api.patch<User>("/users/me", update);
|
const { data } = await api.patch<User>("/users/me", update);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
message: string;
|
message: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterPayload {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
displayName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthStatus {
|
export interface AuthStatus {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
user: null | {
|
user: null | {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
import type { User } from "./user";
|
import type { User } from "./user";
|
||||||
|
|
||||||
export interface Content {
|
export interface Content {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
url: string;
|
url: string;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
type: "image" | "video";
|
type: "image" | "video";
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
size: number;
|
size: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
views: number;
|
views: number;
|
||||||
usageCount: number;
|
usageCount: number;
|
||||||
favoritesCount: number;
|
favoritesCount: number;
|
||||||
tags: (string | Tag)[];
|
tags: (string | Tag)[];
|
||||||
category?: Category;
|
category?: Category;
|
||||||
authorId: string;
|
authorId: string;
|
||||||
author: User;
|
author: User;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
total: number;
|
total: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
role: "user" | "admin";
|
role: "user" | "admin";
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile extends User {
|
export interface UserProfile extends User {
|
||||||
bio?: string;
|
bio?: string;
|
||||||
favoritesCount: number;
|
favoritesCount: number;
|
||||||
uploadsCount: number;
|
uploadsCount: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user