Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3bf575402 | ||
|
|
74046efebe |
@@ -1,17 +1,10 @@
|
|||||||
import { baiJamjureeFont } from '@/lib/fonts';
|
|
||||||
import { Card } from '@components';
|
import { Card } from '@components';
|
||||||
import { cn } from '@utils';
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { AnimatedBackground } from './_components/animated-background';
|
import { AnimatedBackground } from './_components/animated-background';
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4">
|
||||||
className={cn(
|
|
||||||
baiJamjureeFont.className,
|
|
||||||
'bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AnimatedBackground />
|
<AnimatedBackground />
|
||||||
<div className="z-10 w-full">
|
<div className="z-10 w-full">
|
||||||
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
|
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
|
||||||
|
|||||||
@@ -32,21 +32,21 @@ export default function ProjectsVote() {
|
|||||||
title: 'IMPHNEN Certificate',
|
title: 'IMPHNEN Certificate',
|
||||||
description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
|
description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'IMPHNEN Roadmap by Vote',
|
||||||
|
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const completedItems = [
|
const completedItems = [
|
||||||
{
|
{
|
||||||
title: 'IMPHNEN List Event',
|
title: 'IMPHNEN Events Page',
|
||||||
description:
|
description:
|
||||||
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
|
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'IMPHNEN Testimoni',
|
title: 'IMPHNEN Testimoni Page',
|
||||||
description: 'Berikan testimonial gokil buat komunitas IMPHNEN',
|
description: 'Berikan testimonial buat komunitas IMPHNEN',
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'IMPHNEN Roadmap by Vote',
|
|
||||||
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
Drawer,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Textarea,
|
||||||
|
} from '@components';
|
||||||
|
import { cn } from '@utils';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { LuArrowLeft, LuUpload, LuUser, LuX } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export function TestimonialPopup() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const isDesktop = useMediaQuery('(min-width: 768px)');
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>Tambah Testimoni</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader className="pb-2">
|
||||||
|
<DialogTitle>Tulis Testimoni</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<TestimonialForm setOpen={setOpen} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button>Tambah Testimoni</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent className="max-h-[85vh]">
|
||||||
|
<DrawerHeader className="pb-0">
|
||||||
|
<DrawerTitle>Tulis Testimoni</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<div className="overflow-y-auto px-4">
|
||||||
|
<TestimonialForm setOpen={setOpen} className="py-2" />
|
||||||
|
</div>
|
||||||
|
<DrawerFooter className="pt-2 pb-4">
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button variant="bordered">Batal</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestimonialFormProps extends React.ComponentProps<'div'> {
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestimonialForm({ className, setOpen }: TestimonialFormProps) {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [testimonialError, setTestimonialError] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
role: '',
|
||||||
|
testimonial: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('Hanya file gambar yang diizinkan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (event.target?.result) {
|
||||||
|
setAvatarPreview(event.target.result as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAvatar = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAvatarPreview(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateStep1 = () => {
|
||||||
|
return formData.name.trim() !== '' && formData.email.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
if (validateStep1()) setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setTestimonialError(false);
|
||||||
|
|
||||||
|
if (formData.testimonial.trim() === '') {
|
||||||
|
setTestimonialError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testimonial submitted:', formData);
|
||||||
|
setOpen(false);
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setStep(1);
|
||||||
|
setTestimonialError(false);
|
||||||
|
setAvatarPreview(null);
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
role: '',
|
||||||
|
testimonial: '',
|
||||||
|
});
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('w-full', className)}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="relative">
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Section header with step info aligned using justify-between */}
|
||||||
|
<div className="flex justify-between items-center pb-3 border-b mb-4">
|
||||||
|
<h3 className="text-base font-semibold">Lengkapi Data Diri</h3>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Langkah 1 dari 2
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col items-center gap-3 mb-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Avatar className="w-20 h-20 border-2 border-dashed rounded-full">
|
||||||
|
{avatarPreview ? (
|
||||||
|
<AvatarImage
|
||||||
|
src={avatarPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AvatarFallback className="bg-secondary">
|
||||||
|
<LuUser className="w-8 h-8 text-muted-foreground" />
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
{avatarPreview && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={removeAvatar}
|
||||||
|
className="absolute -top-1 -right-1 bg-background rounded-full p-1 shadow-md border"
|
||||||
|
aria-label="Hapus foto"
|
||||||
|
>
|
||||||
|
<LuX className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
aria-label="Unggah foto"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="bordered"
|
||||||
|
className="border-dashed h-8 px-3 text-xs"
|
||||||
|
onClick={triggerFileInput}
|
||||||
|
>
|
||||||
|
<LuUpload className="w-3 h-3 mr-1" />
|
||||||
|
{avatarPreview ? 'Ganti Foto' : 'Unggah Foto'}
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Maks. 2MB (JPEG, PNG)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
label: 'Nama',
|
||||||
|
placeholder: 'Nama lengkap',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
placeholder: 'email@contoh.com',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
label: 'Role',
|
||||||
|
placeholder: 'Backend Developer',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
].map((field) => (
|
||||||
|
<div key={field.id} className="space-y-1">
|
||||||
|
<Label htmlFor={field.id} className="text-sm">
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={field.id}
|
||||||
|
type={field.type || 'text'}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
value={formData[field.id as keyof typeof formData]}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(field.id, e.target.value)
|
||||||
|
}
|
||||||
|
required={field.required}
|
||||||
|
className="py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleNextStep}
|
||||||
|
disabled={!validateStep1()}
|
||||||
|
>
|
||||||
|
Lanjut
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Section header with step info aligned using justify-between */}
|
||||||
|
<div className="flex justify-between items-center pb-3 border-b mb-4">
|
||||||
|
<h3 className="text-base font-semibold">Ulasan</h3>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Langkah 2 dari 2
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label htmlFor="testimonial" className="text-sm">
|
||||||
|
Testimoni
|
||||||
|
</Label>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formData.testimonial.length}/500
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="testimonial"
|
||||||
|
placeholder="Bagaimana pengalaman Anda bersama IMPHNEN?"
|
||||||
|
className={cn(
|
||||||
|
'min-h-[140px]',
|
||||||
|
testimonialError && 'border-destructive'
|
||||||
|
)}
|
||||||
|
value={formData.testimonial}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange('testimonial', e.target.value)
|
||||||
|
}
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
{testimonialError && (
|
||||||
|
<div className="text-destructive text-xs mt-1">
|
||||||
|
Harap isi testimonial
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="bordered"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
>
|
||||||
|
<LuArrowLeft className="w-4 h-4 mr-1" />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" size="lg" className="flex-1">
|
||||||
|
Kirim
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
import TESTIMONIALS from '@/data/testimonials.json';
|
import TESTIMONIALS from '@/data/testimonials.json';
|
||||||
import { Button } from '@components';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
|
||||||
import { BsChatLeftQuote } from 'react-icons/bs';
|
import { BsChatLeftQuote } from 'react-icons/bs';
|
||||||
import { FaQuoteLeft } from 'react-icons/fa';
|
import { FaQuoteLeft } from 'react-icons/fa';
|
||||||
|
import { TestimonialPopup } from './_components/testimonials-popup';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<div className="py-16 px-4 text-center border-b border-border">
|
<div className="py-6 px-4 text-center border-b border-border">
|
||||||
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
|
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
|
||||||
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
|
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
|
||||||
<p className="max-w-2xl mx-auto text-lg mb-8 text-balance text-muted-foreground">
|
<p className="max-w-2xl mx-auto text-lg mb-4 text-balance text-muted-foreground">
|
||||||
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
|
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
|
||||||
komunitas kami
|
komunitas kami
|
||||||
</p>
|
</p>
|
||||||
<Link href="/testimonials/submit">
|
<TestimonialPopup />
|
||||||
<Button>Tulis Testimonialmu</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
|
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function Page() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { poppinsFont } from '@/lib/fonts';
|
import { baiJamjureeFont } from '@/lib/fonts';
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import { cn } from '@utils';
|
import { cn } from '@utils';
|
||||||
import { type Metadata } from 'next';
|
import { type Metadata } from 'next';
|
||||||
@@ -17,7 +17,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="id" suppressHydrationWarning>
|
<html lang="id" suppressHydrationWarning>
|
||||||
<body className={cn(poppinsFont.className, 'antialiased')}>
|
<body className={cn(baiJamjureeFont.className, 'antialiased')}>
|
||||||
<Providers
|
<Providers
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|||||||
Generated
+55
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
@@ -469,6 +470,33 @@
|
|||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-avatar": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -802,6 +830,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-is-hydrated": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
@@ -2100,6 +2146,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '../lib';
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn('aspect-square size-full', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarFallback, AvatarImage };
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './avatar';
|
||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './dialog';
|
export * from './dialog';
|
||||||
|
|||||||
Generated
+46
@@ -13,6 +13,7 @@
|
|||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
@@ -7598,6 +7599,33 @@
|
|||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-avatar": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -7931,6 +7959,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-is-hydrated": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user