Compare commits
2 Commits
4169e265ab
...
716e5cffb4
Author | SHA1 | Date |
---|---|---|
|
716e5cffb4 | 6 months ago |
|
f16bb5862e | 6 months ago |
@ -0,0 +1,137 @@ |
|||||||
|
import DataTable from "@/components/(dashboard)/common/DataTable/DataTable" |
||||||
|
import { Button } from "@/components/(dashboard)/ui/button" |
||||||
|
import { Avatar, AvatarImage } from "@/components/ui/avatar" |
||||||
|
import { Badge } from "@/components/ui/badge" |
||||||
|
import { routes } from "@/lib/routes" |
||||||
|
import { ColumnDef } from "@tanstack/react-table" |
||||||
|
import { ArrowUpDown } from "lucide-react" |
||||||
|
import Link from "next/link" |
||||||
|
|
||||||
|
const UserTable :React.FC<{ |
||||||
|
mutate : () => void |
||||||
|
userData : Array<any> |
||||||
|
isLoading : boolean |
||||||
|
}> = ({ |
||||||
|
mutate ,
|
||||||
|
userData ,
|
||||||
|
isLoading |
||||||
|
}) => { |
||||||
|
|
||||||
|
const columns: ColumnDef<any>[] = [ |
||||||
|
{ |
||||||
|
accessorKey: "sn", |
||||||
|
header: "SN", |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div className="capitalize">{row.index + 1}</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'name', |
||||||
|
accessorFn: (row: any) => row.original?.firstName, |
||||||
|
header: ({ column }) => ( |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
className="!px-0" |
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} |
||||||
|
> |
||||||
|
Name |
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div className="capitalize flex gap-2"> |
||||||
|
<Avatar className="h-8 w-8"> |
||||||
|
<AvatarImage
|
||||||
|
src={row.original?.profilePicture ?? 'no image path'}
|
||||||
|
alt={row.original?.firstName}
|
||||||
|
/> |
||||||
|
</Avatar> |
||||||
|
<p>{row?.original?.firstName}</p> |
||||||
|
</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'email', |
||||||
|
accessorFn: (row: any) => row.original?.email, |
||||||
|
header: ({ column }) => ( |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} |
||||||
|
className="!px-0" |
||||||
|
> |
||||||
|
Email |
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div>{row.original?.email}</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'dateOfBirth', |
||||||
|
accessorFn: (row: any) => row.original?.dateOfBirth, |
||||||
|
header: ({ column }) => ( |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
className="!px-0" |
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} |
||||||
|
> |
||||||
|
DateOfBirth |
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div className="capitalize">{row.original?.dateOfBirth}</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'isActivated', |
||||||
|
accessorFn: (row: any) => row.original?.isActivated, |
||||||
|
header: ({ column }) => ( |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
className="!px-0" |
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} |
||||||
|
> |
||||||
|
IsActive |
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div>{row.original?.isActivated ? <Badge variant={'success'}>Active</Badge> : <Badge variant={'destructive'}>Deactive</Badge>}</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 'last_online_at', |
||||||
|
accessorFn: (row: any) => row.original?.lastOnline, |
||||||
|
header: ({ column }) => ( |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
className="!px-0" |
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} |
||||||
|
> |
||||||
|
lastOnline |
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<div> |
||||||
|
{row.original?.lastOnline ?? '-'}
|
||||||
|
</div> |
||||||
|
), |
||||||
|
}, |
||||||
|
] |
||||||
|
return( |
||||||
|
<> |
||||||
|
<DataTable
|
||||||
|
data={userData} |
||||||
|
columns={columns} |
||||||
|
mutate={mutate} |
||||||
|
searchKey="username" |
||||||
|
isLoading={isLoading} |
||||||
|
/> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default UserTable |
@ -0,0 +1,56 @@ |
|||||||
|
'use client' |
||||||
|
import UserTabContent from "@/app/user/profile/_partials/UserTabContent" |
||||||
|
import BreadCrumbNav from "@/components/(dashboard)/common/BreadCumbNav/BreadCrumbNav" |
||||||
|
import DataTable from "@/components/(dashboard)/common/DataTable/DataTable" |
||||||
|
import ContentContainer from "@/components/(dashboard)/elements/ContentContainer" |
||||||
|
import { PageHeading } from "@/components/(dashboard)/ui/title" |
||||||
|
import CommonContainer from "@/components/elements/CommonContainer" |
||||||
|
import AppContextProvider from "@/helpers/context/AppContextProvider" |
||||||
|
import { defaultFetcher } from "@/helpers/fetch.helper" |
||||||
|
import { routes } from "@/lib/routes" |
||||||
|
import { APP_BASE_URL } from "@/utils/constants" |
||||||
|
import AdminView from "@/views/AdminView" |
||||||
|
import useSWR from "swr" |
||||||
|
import UserTable from "./_partials/UserTable" |
||||||
|
|
||||||
|
const UsersIndexPage = () => { |
||||||
|
const UserListURL = `${APP_BASE_URL}/api/admin/stats/userDetail` |
||||||
|
const { data : UsersList , mutate , isLoading} = useSWR(UserListURL , defaultFetcher); |
||||||
|
|
||||||
|
|
||||||
|
console.log(UsersList) |
||||||
|
|
||||||
|
return( |
||||||
|
<> |
||||||
|
<AppContextProvider> |
||||||
|
<AdminView> |
||||||
|
<CommonContainer> |
||||||
|
<PageHeading>Users</PageHeading> |
||||||
|
<BreadCrumbNav breadCrumbItems={[ |
||||||
|
{ |
||||||
|
title : 'Dashboard', |
||||||
|
href : routes.DASHBOARD_ROUTE |
||||||
|
}, |
||||||
|
{ |
||||||
|
title : 'Users', |
||||||
|
href : routes.USER_INDEX_PAGE |
||||||
|
}, |
||||||
|
]}/> |
||||||
|
<ContentContainer> |
||||||
|
<div> |
||||||
|
<UserTable
|
||||||
|
userData={UsersList?.users} |
||||||
|
mutate={mutate} |
||||||
|
isLoading={isLoading} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</ContentContainer> |
||||||
|
</CommonContainer> |
||||||
|
</AdminView> |
||||||
|
</AppContextProvider> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default UsersIndexPage |
||||||
|
|
@ -0,0 +1,179 @@ |
|||||||
|
import { useState, useEffect } from 'react'; |
||||||
|
import { Card, CardContent } from "@/components/ui/card"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { Upload, X, AlertCircle } from 'lucide-react'; |
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert"; |
||||||
|
|
||||||
|
interface BannerUploadProps { |
||||||
|
onImageChange?: (file: File | null) => void; |
||||||
|
defaultImage?: string; |
||||||
|
maxSizeMB?: number; |
||||||
|
} |
||||||
|
|
||||||
|
export default function BannerImageUpload({
|
||||||
|
onImageChange,
|
||||||
|
defaultImage, |
||||||
|
maxSizeMB = 5
|
||||||
|
}: BannerUploadProps) { |
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(defaultImage || null); |
||||||
|
const [error, setError] = useState<string | null>(null); |
||||||
|
const [isDragging, setIsDragging] = useState(false); |
||||||
|
|
||||||
|
const acceptedTypes = ['image/jpeg', 'image/png', 'image/webp']; |
||||||
|
const maxSize = maxSizeMB * 1024 * 1024; // Convert MB to bytes
|
||||||
|
|
||||||
|
const validateFile = (file: File): string | null => { |
||||||
|
if (!acceptedTypes.includes(file.type)) { |
||||||
|
return 'Please upload a valid image file (JPG, PNG, or WebP)'; |
||||||
|
} |
||||||
|
if (file.size > maxSize) { |
||||||
|
return `File size should be less than ${maxSizeMB}MB`; |
||||||
|
} |
||||||
|
return null; |
||||||
|
}; |
||||||
|
|
||||||
|
const handleFile = (file: File) => { |
||||||
|
const validationError = validateFile(file); |
||||||
|
if (validationError) { |
||||||
|
setError(validationError); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Clear any existing error
|
||||||
|
setError(null); |
||||||
|
|
||||||
|
// Create preview URL
|
||||||
|
const objectUrl = URL.createObjectURL(file); |
||||||
|
setPreviewUrl(objectUrl); |
||||||
|
|
||||||
|
// Notify parent component
|
||||||
|
if (onImageChange) { |
||||||
|
onImageChange(file); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const file = e.target.files?.[0]; |
||||||
|
if (file) { |
||||||
|
handleFile(file); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { |
||||||
|
e.preventDefault(); |
||||||
|
setIsDragging(false); |
||||||
|
|
||||||
|
const file = e.dataTransfer.files?.[0]; |
||||||
|
if (file) { |
||||||
|
handleFile(file); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { |
||||||
|
e.preventDefault(); |
||||||
|
setIsDragging(true); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { |
||||||
|
e.preventDefault(); |
||||||
|
setIsDragging(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const removeBanner = () => { |
||||||
|
if (previewUrl) { |
||||||
|
URL.revokeObjectURL(previewUrl); |
||||||
|
} |
||||||
|
setPreviewUrl(null); |
||||||
|
setError(null); |
||||||
|
if (onImageChange) { |
||||||
|
onImageChange(null); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Cleanup preview URL when component unmounts
|
||||||
|
useEffect(() => { |
||||||
|
return () => { |
||||||
|
if (previewUrl && previewUrl !== defaultImage) { |
||||||
|
URL.revokeObjectURL(previewUrl); |
||||||
|
} |
||||||
|
}; |
||||||
|
}, [previewUrl, defaultImage]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="space-y-4 col-span-2"> |
||||||
|
<label>Upload Course Banner</label> |
||||||
|
<div |
||||||
|
className={`relative w-full h-64 rounded-lg overflow-hidden border-2 border-dashed transition-colors
|
||||||
|
${isDragging ? 'border-purple-500 bg-purple-50' : 'border-gray-300 bg-gray-50'} |
||||||
|
${error ? 'border-red-300' : ''}`}
|
||||||
|
onDrop={handleDrop} |
||||||
|
onDragOver={handleDragOver} |
||||||
|
onDragLeave={handleDragLeave} |
||||||
|
> |
||||||
|
{previewUrl ? ( |
||||||
|
<> |
||||||
|
<img |
||||||
|
src={previewUrl} |
||||||
|
alt="Banner preview" |
||||||
|
className="w-full h-full object-cover" |
||||||
|
/> |
||||||
|
<div className="absolute inset-0 bg-black bg-opacity-40 opacity-0 hover:opacity-100 transition-opacity"> |
||||||
|
<div className="absolute inset-0 flex items-center justify-center space-x-4"> |
||||||
|
<Button |
||||||
|
type="button" |
||||||
|
variant="outline" |
||||||
|
className="bg-white hover:bg-gray-100" |
||||||
|
onClick={() => document.getElementById('banner-upload')?.click()} |
||||||
|
> |
||||||
|
<Upload className="h-5 w-5 mr-2" /> |
||||||
|
Change Banner |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
type="button" |
||||||
|
variant="destructive" |
||||||
|
onClick={removeBanner} |
||||||
|
> |
||||||
|
<X className="h-5 w-5 mr-2" /> |
||||||
|
Remove Banner |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center"> |
||||||
|
<Upload className="h-12 w-12 text-gray-400 mb-4" /> |
||||||
|
<div className="text-center"> |
||||||
|
<Button |
||||||
|
type="button" |
||||||
|
variant="outline" |
||||||
|
className="mb-2" |
||||||
|
onClick={() => document.getElementById('banner-upload')?.click()} |
||||||
|
> |
||||||
|
Choose File |
||||||
|
</Button> |
||||||
|
<p className="text-sm text-gray-500">or drag and drop your image here</p> |
||||||
|
<p className="text-xs text-gray-400 mt-2"> |
||||||
|
Supports: JPG, PNG, WebP (max {maxSizeMB}MB) |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
<input |
||||||
|
id="banner-upload" |
||||||
|
type="file" |
||||||
|
className="hidden" |
||||||
|
accept={acceptedTypes.join(',')} |
||||||
|
onChange={handleFileSelect} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
{error && ( |
||||||
|
<Alert variant="destructive"> |
||||||
|
<AlertCircle className="h-4 w-4" /> |
||||||
|
<AlertDescription>{error}</AlertDescription> |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
export const CourseCategoryOptionHelper = (data: Record<string, string>[]) : any => { |
||||||
|
return data?.length > 0
|
||||||
|
? data.map((item) => ({ |
||||||
|
value: item?.id?.toString() ? item?.id?.toString() : item?.idx, |
||||||
|
label: item?.title ? item.title : item?.name, |
||||||
|
})) |
||||||
|
: []; |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" |
||||||
|
import { defaultFetcher } from "@/helpers/fetch.helper" |
||||||
|
import { APP_BASE_URL } from "@/utils/constants" |
||||||
|
import Image from "next/image" |
||||||
|
import useSWR from "swr" |
||||||
|
|
||||||
|
const EnrolledCourseTabContent : React.FC = () => { |
||||||
|
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Card className='border border-purple-700/20 shadow shadow-purple-700/40'> |
||||||
|
<CardHeader> |
||||||
|
<CardTitle className="text-2xl text-purple-700">Enrolled Courses List </CardTitle> |
||||||
|
</CardHeader> |
||||||
|
<CardContent> |
||||||
|
{ |
||||||
|
data?.length && data.map((course : Record<string,any> , index : number) => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<Card |
||||||
|
key={course.id} |
||||||
|
className="hover:shadow-lg transition-shadow" |
||||||
|
> |
||||||
|
<CardContent className="p-6 flex gap-4"> |
||||||
|
<div > |
||||||
|
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div className="flex items-start justify-between"> |
||||||
|
<div className="space-y-2"> |
||||||
|
<h3 className="text-xl font-semibold text-purple-700"> |
||||||
|
{course.title} |
||||||
|
</h3> |
||||||
|
<p className="text-gray-600">{course.description}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500"> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Published:</span> |
||||||
|
<span className="ml-1">{course.publishedDate}</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Enrolled:</span> |
||||||
|
<span className="ml-1"> |
||||||
|
{course.enrolledStudent} students |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Active Discussions:</span> |
||||||
|
<span className="ml-1"> |
||||||
|
{course.activeDiscussionCount} |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Published by:</span> |
||||||
|
<span className="ml-1">{course.publishedBy}</span> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</> |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default EnrolledCourseTabContent |
@ -0,0 +1,74 @@ |
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" |
||||||
|
import { defaultFetcher } from "@/helpers/fetch.helper" |
||||||
|
import { APP_BASE_URL } from "@/utils/constants" |
||||||
|
import Image from "next/image" |
||||||
|
import useSWR from "swr" |
||||||
|
|
||||||
|
const MyCoursesListTabContent = () => { |
||||||
|
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Card className='border border-purple-700/20 shadow shadow-purple-700/40'> |
||||||
|
<CardHeader> |
||||||
|
<CardTitle className="text-2xl text-purple-700">My Courses</CardTitle> |
||||||
|
</CardHeader> |
||||||
|
<CardContent> |
||||||
|
{ |
||||||
|
data?.length && data.map((course : Record<string,any> , index : number) => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<Card |
||||||
|
key={course.id} |
||||||
|
className="hover:shadow-lg transition-shadow" |
||||||
|
> |
||||||
|
<CardContent className="p-6 flex gap-4"> |
||||||
|
<div > |
||||||
|
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div className="flex items-start justify-between"> |
||||||
|
<div className="space-y-2"> |
||||||
|
<h3 className="text-xl font-semibold text-purple-700"> |
||||||
|
{course.title} |
||||||
|
</h3> |
||||||
|
<p className="text-gray-600">{course.description}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500"> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Published:</span> |
||||||
|
<span className="ml-1">{course.publishedDate}</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Enrolled:</span> |
||||||
|
<span className="ml-1"> |
||||||
|
{course.enrolledStudent} students |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Active Discussions:</span> |
||||||
|
<span className="ml-1"> |
||||||
|
{course.activeDiscussionCount} |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
<span className="flex items-center"> |
||||||
|
<span className="font-medium">Published by:</span> |
||||||
|
<span className="ml-1">{course.publishedBy}</span> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</> |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default MyCoursesListTabContent |
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,59 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const alertVariants = cva( |
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: "bg-background text-foreground", |
||||||
|
destructive: |
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
const Alert = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> |
||||||
|
>(({ className, variant, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
role="alert" |
||||||
|
className={cn(alertVariants({ variant }), className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
Alert.displayName = "Alert" |
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef< |
||||||
|
HTMLParagraphElement, |
||||||
|
React.HTMLAttributes<HTMLHeadingElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<h5 |
||||||
|
ref={ref} |
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertTitle.displayName = "AlertTitle" |
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef< |
||||||
|
HTMLParagraphElement, |
||||||
|
React.HTMLAttributes<HTMLParagraphElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDescription.displayName = "AlertDescription" |
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription } |
Loading…
Reference in new issue