Compare commits
2 Commits
a6d03971eb
...
057e9c9043
Author | SHA1 | Date |
---|---|---|
|
057e9c9043 | 6 months ago |
|
a80995b170 | 6 months ago |
@ -0,0 +1,20 @@ |
|||||||
|
'use client' |
||||||
|
import CommonContainer from "@/components/(dashboard)/elements/CommonContainer" |
||||||
|
import AppContextProvider from "@/helpers/context/AppContextProvider" |
||||||
|
import AdminView from "@/views/AdminView" |
||||||
|
|
||||||
|
const DashBoardIndexPage = () => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<AppContextProvider> |
||||||
|
<AdminView> |
||||||
|
<CommonContainer> |
||||||
|
<h2>hello</h2> |
||||||
|
</CommonContainer> |
||||||
|
</AdminView> |
||||||
|
</AppContextProvider> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default DashBoardIndexPage |
@ -0,0 +1,22 @@ |
|||||||
|
import { Toaster } from "@/components/(dashboard)/ui/toaster" |
||||||
|
import AppContextProvider from "@/helpers/context/AppContextProvider" |
||||||
|
import { Suspense } from "react" |
||||||
|
|
||||||
|
const RootLayout :React.FC<{ |
||||||
|
children :React.ReactNode |
||||||
|
}> = ({ |
||||||
|
children |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<Suspense> |
||||||
|
<AppContextProvider> |
||||||
|
{children} |
||||||
|
</AppContextProvider> |
||||||
|
<Toaster /> |
||||||
|
</Suspense> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default RootLayout |
@ -0,0 +1,195 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; |
||||||
|
import { Label } from "@/components/ui/label"; |
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"; |
||||||
|
import { Separator } from "@/components/ui/separator"; |
||||||
|
import { Check, X } from "lucide-react"; |
||||||
|
|
||||||
|
const QuizGenerator = () => { |
||||||
|
// Submitted questions history
|
||||||
|
const [submittedQuestions] = useState([ |
||||||
|
{ |
||||||
|
id: 1, |
||||||
|
question: "What is React's Virtual DOM?", |
||||||
|
options: [ |
||||||
|
"A complete DOM copy", |
||||||
|
"A lightweight copy of the DOM", |
||||||
|
"A browser feature", |
||||||
|
"A routing system" |
||||||
|
], |
||||||
|
selectedOption: 0, // User selected
|
||||||
|
correctOption: 1, // Correct answer
|
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 2, |
||||||
|
question: "What hook manages side effects?", |
||||||
|
options: [ |
||||||
|
"useState", |
||||||
|
"useReducer", |
||||||
|
"useEffect", |
||||||
|
"useContext" |
||||||
|
], |
||||||
|
selectedOption: 2, |
||||||
|
correctOption: 2, |
||||||
|
}, |
||||||
|
]); |
||||||
|
|
||||||
|
// Unsubmitted questions
|
||||||
|
const [pendingQuestions, setPendingQuestions] = useState([ |
||||||
|
{ |
||||||
|
id: 1, |
||||||
|
question: "Which of these is a state management library?", |
||||||
|
options: ["Redux", "Axios", "Lodash", "Moment"], |
||||||
|
selectedOption: null |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: 2, |
||||||
|
question: "What does CSS stand for?", |
||||||
|
options: [ |
||||||
|
"Computer Style Sheets", |
||||||
|
"Cascading Style Sheets", |
||||||
|
"Creative Style Sheets", |
||||||
|
"Colorful Style Sheets" |
||||||
|
], |
||||||
|
selectedOption: null |
||||||
|
}, |
||||||
|
]); |
||||||
|
|
||||||
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); |
||||||
|
|
||||||
|
const handleOptionSelect = (value) => { |
||||||
|
const updatedQuestions = [...pendingQuestions]; |
||||||
|
updatedQuestions[currentQuestionIndex].selectedOption = parseInt(value); |
||||||
|
setPendingQuestions(updatedQuestions); |
||||||
|
}; |
||||||
|
|
||||||
|
const generateQuiz = () => { |
||||||
|
// Reset selections and shuffle pending questions
|
||||||
|
const shuffledQuestions = [...pendingQuestions].map(q => ({ |
||||||
|
...q, |
||||||
|
selectedOption: null |
||||||
|
})); |
||||||
|
setPendingQuestions(shuffledQuestions); |
||||||
|
setCurrentQuestionIndex(0); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleNext = () => { |
||||||
|
if (currentQuestionIndex < pendingQuestions.length - 1) { |
||||||
|
setCurrentQuestionIndex(prev => prev + 1); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handlePrevious = () => { |
||||||
|
if (currentQuestionIndex > 0) { |
||||||
|
setCurrentQuestionIndex(prev => prev - 1); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="py-6 container mx- mt-8"> |
||||||
|
{/* Submitted Questions History */} |
||||||
|
<div className='grid grid-cols-3 space-x-6'> |
||||||
|
<Card className="w-full col-span-2"> |
||||||
|
<CardHeader> |
||||||
|
<CardTitle>Quiz History</CardTitle> |
||||||
|
</CardHeader> |
||||||
|
<CardContent> |
||||||
|
<ScrollArea className="h-[400px] pr-4"> |
||||||
|
{submittedQuestions.map((q, index) => ( |
||||||
|
<div key={q.id} className="mb-4"> |
||||||
|
<div className="p-4 border rounded-lg border-gray-200"> |
||||||
|
<h3 className="font-medium mb-2">{q.question}</h3> |
||||||
|
<div className="text-sm space-y-2"> |
||||||
|
{q.options.map((option, i) => ( |
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`p-2 rounded flex justify-between items-center ${ |
||||||
|
i === q.correctOption && i === q.selectedOption |
||||||
|
? 'bg-green-100' |
||||||
|
: i === q.correctOption |
||||||
|
? 'bg-green-50' |
||||||
|
: i === q.selectedOption |
||||||
|
? 'bg-red-50' |
||||||
|
: 'bg-gray-50' |
||||||
|
}`}
|
||||||
|
> |
||||||
|
<span>{option}</span> |
||||||
|
{i === q.correctOption && ( |
||||||
|
<Check className="w-4 h-4 text-green-600" /> |
||||||
|
)} |
||||||
|
{i === q.selectedOption && i !== q.correctOption && ( |
||||||
|
<X className="w-4 h-4 text-red-600" /> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{index < submittedQuestions.length - 1 && <Separator className="my-4" />} |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</ScrollArea> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
|
||||||
|
{/* Pending Questions Interface */} |
||||||
|
<div className="w-full space-y-6"> |
||||||
|
<Button
|
||||||
|
onClick={generateQuiz} |
||||||
|
className="w-full bg-purple-700" |
||||||
|
> |
||||||
|
Generate Quiz |
||||||
|
</Button> |
||||||
|
|
||||||
|
{pendingQuestions.length > 0 ? ( |
||||||
|
<Card> |
||||||
|
<CardHeader> |
||||||
|
<CardTitle> |
||||||
|
Question {currentQuestionIndex + 1} of {pendingQuestions.length} |
||||||
|
</CardTitle> |
||||||
|
</CardHeader> |
||||||
|
<CardContent> |
||||||
|
<div className="space-y-6"> |
||||||
|
<h3 className="text-lg font-medium"> |
||||||
|
{pendingQuestions[currentQuestionIndex].question} |
||||||
|
</h3> |
||||||
|
|
||||||
|
<RadioGroup |
||||||
|
value={pendingQuestions[currentQuestionIndex].selectedOption?.toString()} |
||||||
|
onValueChange={handleOptionSelect} |
||||||
|
> |
||||||
|
{pendingQuestions[currentQuestionIndex].options.map((option, i) => ( |
||||||
|
<div key={i} className="flex items-center space-x-2"> |
||||||
|
<RadioGroupItem value={i.toString()} id={`option-${i}`} /> |
||||||
|
<Label htmlFor={`option-${i}`}>{option}</Label> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</RadioGroup> |
||||||
|
|
||||||
|
<div className="flex justify-end mt-6"> |
||||||
|
<Button
|
||||||
|
onClick={handleNext} |
||||||
|
disabled={currentQuestionIndex === pendingQuestions.length - 1} |
||||||
|
className='bg-purple-700' |
||||||
|
> |
||||||
|
Submit |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
) : ( |
||||||
|
<Card> |
||||||
|
<CardContent className="p-6 text-center text-gray-500"> |
||||||
|
No pending questions available |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default QuizGenerator; |
@ -0,0 +1,169 @@ |
|||||||
|
import AsyncSelect from 'react-select/async'; |
||||||
|
import React, { useEffect, useRef } from 'react'; |
||||||
|
import { ChoiceType } from '@/helpers/commonSchema/common.schema'; |
||||||
|
import { Label } from '../ui/label'; |
||||||
|
import { StylesConfig } from 'node_modules/react-select/dist/declarations/src'; |
||||||
|
|
||||||
|
interface SelectFieldProps { |
||||||
|
className?: string; |
||||||
|
id?: string; |
||||||
|
label?: string; |
||||||
|
labelStyle: string; |
||||||
|
labelWidth?: string; |
||||||
|
name: string; |
||||||
|
defaultValue?: ChoiceType | null; |
||||||
|
value?: string | number | boolean; |
||||||
|
required?: boolean; |
||||||
|
wrapperClassName?: string; |
||||||
|
options?: { value: string | number; label: string }[]; |
||||||
|
fieldErrors?: Array<string>; |
||||||
|
clearSelectValue?: boolean; |
||||||
|
setClearSelectValue?: React.Dispatch<React.SetStateAction<boolean>>; |
||||||
|
isDisabled?: boolean; |
||||||
|
onChange?: (choice: ChoiceType) => void | any; |
||||||
|
loadOptions? : (inputValue: string, callback: (options: any[]) => void) => Promise<void> | any |
||||||
|
placeHolder? : string |
||||||
|
labelClassName? : string |
||||||
|
defaultOptions? : { value: string | number; label: string }[] |
||||||
|
isMulti? : boolean |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const AsyncSelectField: React.FC<SelectFieldProps> = ({ |
||||||
|
className = '', |
||||||
|
id, |
||||||
|
label, |
||||||
|
labelStyle, |
||||||
|
labelWidth, |
||||||
|
name, |
||||||
|
defaultValue, |
||||||
|
value, |
||||||
|
required, |
||||||
|
wrapperClassName = '', |
||||||
|
fieldErrors = [], |
||||||
|
clearSelectValue = false, |
||||||
|
setClearSelectValue, |
||||||
|
isDisabled, |
||||||
|
onChange, |
||||||
|
loadOptions, |
||||||
|
placeHolder, |
||||||
|
labelClassName , |
||||||
|
defaultOptions , |
||||||
|
isMulti = false , |
||||||
|
}) => { |
||||||
|
if (labelStyle === 'label-left') { |
||||||
|
wrapperClassName = wrapperClassName.concat( |
||||||
|
' ', |
||||||
|
' flex justify-between items-center gap-4 ' |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const selectRef = useRef<any>(null); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (clearSelectValue) { |
||||||
|
selectRef?.current?.clearValue?.(); |
||||||
|
setClearSelectValue && setClearSelectValue(false); |
||||||
|
} |
||||||
|
}, [clearSelectValue, setClearSelectValue]); |
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // Auto-select the first option if available
|
||||||
|
// if (!selectedValue && defaultOptions && defaultOptions.length > 0) {
|
||||||
|
// const firstOption = defaultOptions[0];
|
||||||
|
// setSelectedValue(firstOption);
|
||||||
|
// onChange && onChange(firstOption);
|
||||||
|
// }
|
||||||
|
// }, [defaultOptions, selectedValue, onChange]);
|
||||||
|
|
||||||
|
const styles = { |
||||||
|
menuList: (base: any , state :any) => ({ |
||||||
|
...base, |
||||||
|
borderColor : state?.isFocused && 'bg-[var(--primary)]', |
||||||
|
colourStyles : '#fff', |
||||||
|
'::-webkit-scrollbar': { |
||||||
|
width: '5px', |
||||||
|
}, |
||||||
|
'::-webkit-scrollbar-track': { |
||||||
|
background: '#f1f1f1', |
||||||
|
}, |
||||||
|
'::-webkit-scrollbar-thumb': { |
||||||
|
background: '#d1d1d1', |
||||||
|
}, |
||||||
|
'::-webkit-scrollbar-thumb:hover': { |
||||||
|
background: '#555', |
||||||
|
}, |
||||||
|
}), |
||||||
|
input: (base:any, state : any) => ({ |
||||||
|
...base, |
||||||
|
'[type="text"]': { |
||||||
|
fontSize: 13, |
||||||
|
fontWeight: 900, |
||||||
|
color: 'green' |
||||||
|
} |
||||||
|
}) |
||||||
|
}; |
||||||
|
|
||||||
|
const colourStyles: StylesConfig<any> = { |
||||||
|
control: (styles) => ({ ...styles, backgroundColor: 'white' }), |
||||||
|
option: (styles, { data, isDisabled, isFocused, isSelected }) => { |
||||||
|
return { |
||||||
|
...styles, |
||||||
|
backgroundColor:'hsl(var(--background))', |
||||||
|
color:'#fff' |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
// input: (styles) => ({ ...styles, ...dot() }),
|
||||||
|
// placeholder: (styles) => ({ ...styles, ...dot('#ccc') }),
|
||||||
|
// singleValue: (styles, { data }) => ({ ...styles, ...dot(data.color) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={`${wrapperClassName}`}> |
||||||
|
{label && ( |
||||||
|
<Label className={`${labelClassName} ${fieldErrors?.length > 0 && 'text-destructive'}`}> |
||||||
|
{label} |
||||||
|
{required ? <span className="text-red-400"> *</span> : ''} |
||||||
|
</Label> |
||||||
|
)} |
||||||
|
<div className="w-full mt-2"> |
||||||
|
<AsyncSelect |
||||||
|
cacheOptions |
||||||
|
isMulti={isMulti} |
||||||
|
loadOptions={loadOptions} |
||||||
|
defaultOptions={defaultOptions} |
||||||
|
ref={selectRef} |
||||||
|
styles={styles} |
||||||
|
isDisabled={isDisabled} |
||||||
|
onChange={onChange as any} |
||||||
|
className={className + 'bg-[var(--primary)]' + ' !text-sm '+ (fieldErrors?.length > 0 && 'border border-destructive')} |
||||||
|
theme={(theme) => ({ |
||||||
|
...theme, |
||||||
|
borderRadius: 0, |
||||||
|
colors: { |
||||||
|
...theme.colors, |
||||||
|
backgroundColor: 'hsl(var(--background))' , |
||||||
|
primary25: 'hsl(var(--secondary))', |
||||||
|
primary: 'hsl(var(--secondary))', |
||||||
|
color : '#fff' |
||||||
|
}, |
||||||
|
})} |
||||||
|
|
||||||
|
id={id} |
||||||
|
name={name} |
||||||
|
defaultValue={defaultValue} |
||||||
|
placeholder={placeHolder} |
||||||
|
|
||||||
|
/> |
||||||
|
<span className="block text-destructive text-xs my-1"> |
||||||
|
{fieldErrors.map((item: string, index: any) => ( |
||||||
|
<span key={index}>{item}</span> |
||||||
|
))} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AsyncSelectField; |
@ -0,0 +1,174 @@ |
|||||||
|
import React, { useCallback, useState, useEffect } from 'react'; |
||||||
|
import { useDropzone } from 'react-dropzone'; |
||||||
|
import { Label } from '../ui/label'; |
||||||
|
import { FolderInput, Trash2 } from 'lucide-react'; |
||||||
|
|
||||||
|
interface FileWithPreview extends File { |
||||||
|
preview: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface InitialFileState { |
||||||
|
file: FileWithPreview | null; |
||||||
|
path: string | null; |
||||||
|
} |
||||||
|
|
||||||
|
interface DropZonePropsInterface { |
||||||
|
title?: string; |
||||||
|
handleFileChange: (key: string, file: File | null) => void; |
||||||
|
name: string; |
||||||
|
initialItem?: File | string | null; |
||||||
|
currentFile?: File | null; // Add this prop
|
||||||
|
} |
||||||
|
|
||||||
|
const DropZone: React.FC<DropZonePropsInterface> = ({ |
||||||
|
title = 'Upload File', |
||||||
|
handleFileChange, |
||||||
|
name, |
||||||
|
initialItem = null, |
||||||
|
currentFile = null, // Add this prop
|
||||||
|
}) => { |
||||||
|
const [fileState, setFileState] = useState<InitialFileState>({ |
||||||
|
file: null, |
||||||
|
path: typeof initialItem === 'string' ? initialItem : null |
||||||
|
}); |
||||||
|
|
||||||
|
// Effect to handle initial setup and updates
|
||||||
|
useEffect(() => { |
||||||
|
if (currentFile) { |
||||||
|
// If there's a current file in form state, create preview
|
||||||
|
const fileWithPreview = Object.assign(currentFile, { |
||||||
|
preview: URL.createObjectURL(currentFile) |
||||||
|
}) as FileWithPreview; |
||||||
|
setFileState({ file: fileWithPreview, path: null }); |
||||||
|
} else if (initialItem && typeof initialItem === 'string') { |
||||||
|
// Handle path-based initialItem
|
||||||
|
setFileState(prev => ({ ...prev, path: initialItem })); |
||||||
|
} else if (initialItem instanceof File) { |
||||||
|
// Handle File-based initialItem
|
||||||
|
const fileWithPreview = Object.assign(initialItem, { |
||||||
|
preview: URL.createObjectURL(initialItem) |
||||||
|
}) as FileWithPreview; |
||||||
|
setFileState({ file: fileWithPreview, path: null }); |
||||||
|
} else if (!initialItem && !currentFile) { |
||||||
|
// Reset state if no file
|
||||||
|
setFileState({ file: null, path: null }); |
||||||
|
} |
||||||
|
}, [initialItem, currentFile]); |
||||||
|
|
||||||
|
const onDrop = useCallback( |
||||||
|
(acceptedFiles: File[]) => { |
||||||
|
if (acceptedFiles.length > 0) { |
||||||
|
const fileWithPreview = Object.assign(acceptedFiles[0], { |
||||||
|
preview: URL.createObjectURL(acceptedFiles[0]), |
||||||
|
}) as FileWithPreview; |
||||||
|
|
||||||
|
setFileState({ file: fileWithPreview, path: null }); |
||||||
|
handleFileChange(name, fileWithPreview); |
||||||
|
} |
||||||
|
}, |
||||||
|
[name, handleFileChange] |
||||||
|
); |
||||||
|
|
||||||
|
const removeFile = useCallback((e: React.MouseEvent) => { |
||||||
|
e.stopPropagation(); |
||||||
|
setFileState({ file: null, path: null }); |
||||||
|
handleFileChange(name, null); |
||||||
|
}, [name, handleFileChange]); |
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ |
||||||
|
onDrop, |
||||||
|
accept: { |
||||||
|
'image/jpeg': ['.jpeg', '.jpg'], |
||||||
|
'image/png': ['.png'], |
||||||
|
'application/pdf': ['.pdf'], |
||||||
|
'application/msword': ['.doc', '.docx'] |
||||||
|
}, |
||||||
|
multiple: false |
||||||
|
}); |
||||||
|
|
||||||
|
const renderPreview = () => { |
||||||
|
const currentFile = fileState.file; |
||||||
|
const currentPath = fileState.path; |
||||||
|
|
||||||
|
if (currentFile) { |
||||||
|
if (currentFile.type.startsWith('image/')) { |
||||||
|
return ( |
||||||
|
<div className="mt-4 flex flex-col items-center gap-2"> |
||||||
|
<img |
||||||
|
src={currentFile.preview} |
||||||
|
alt={currentFile.name} |
||||||
|
className="max-h-32 border rounded" |
||||||
|
/> |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<FolderInput className="w-4 h-4" /> |
||||||
|
<span className="text-sm">{currentFile.name}</span> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={removeFile} |
||||||
|
className="hover:bg-gray-200 p-1 rounded-full" |
||||||
|
aria-label="Remove file" |
||||||
|
> |
||||||
|
<Trash2 className="w-4 h-4 text-red-500" /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
} else if (currentPath) { |
||||||
|
if (currentPath.match(/\.(jpg|jpeg|png)$/i)) { |
||||||
|
return ( |
||||||
|
<div className="mt-4 flex flex-col items-center gap-2"> |
||||||
|
<img |
||||||
|
src={currentPath} |
||||||
|
alt="Uploaded file" |
||||||
|
className="max-h-32 border rounded" |
||||||
|
/> |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<FolderInput className="w-4 h-4" /> |
||||||
|
<span className="text-sm">{currentPath.split('/').pop()}</span> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={removeFile} |
||||||
|
className="hover:bg-gray-200 p-1 rounded-full" |
||||||
|
aria-label="Remove file" |
||||||
|
> |
||||||
|
<Trash2 className="w-4 h-4 text-red-500" /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
}; |
||||||
|
|
||||||
|
// Cleanup preview URLs on unmount
|
||||||
|
useEffect(() => { |
||||||
|
return () => { |
||||||
|
if (fileState.file?.preview) { |
||||||
|
URL.revokeObjectURL(fileState.file.preview); |
||||||
|
} |
||||||
|
}; |
||||||
|
}, [fileState.file]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex flex-col gap-4"> |
||||||
|
<Label>{title}</Label> |
||||||
|
<div |
||||||
|
{...getRootProps()} |
||||||
|
className="bg-sky-50 dark:bg-background border-gray-400 border border-dashed px-4 py-8 rounded cursor-pointer" |
||||||
|
> |
||||||
|
<input {...getInputProps()} /> |
||||||
|
<div className="text-center"> |
||||||
|
<p className="text-sm">Drag and drop a file here, or click to select a file</p> |
||||||
|
<em className="text-sm block mt-1">(*.jpeg, *.png, *.pdf, *.doc files accepted)</em> |
||||||
|
</div> |
||||||
|
|
||||||
|
{renderPreview()} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default DropZone; |
@ -0,0 +1,121 @@ |
|||||||
|
import React, { useContext } from "react"; |
||||||
|
import { Label } from "@/components/ui/label"; |
||||||
|
import { Input } from "@/components/ui/input"; |
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { useTheme } from "@/hooks/use-theme"; |
||||||
|
|
||||||
|
interface FormInputProps extends React.InputHTMLAttributes<HTMLInputElement> { |
||||||
|
label?: string; |
||||||
|
error?: []; |
||||||
|
labelClassName?: string; |
||||||
|
containerClassName?: string; |
||||||
|
helperText?: string; |
||||||
|
labelPosition?: "top" | "left"; |
||||||
|
required? : boolean ,
|
||||||
|
name : string |
||||||
|
type?: |
||||||
|
| "textarea" |
||||||
|
| "text" |
||||||
|
| "file" |
||||||
|
| "email" |
||||||
|
| "tel" |
||||||
|
| "password" |
||||||
|
| "number" |
||||||
|
| "search" |
||||||
|
| "color" |
||||||
|
| "date" |
||||||
|
| "time" |
||||||
|
| "range" |
||||||
|
| "datetime-local" |
||||||
|
| "checkbox"; |
||||||
|
} |
||||||
|
|
||||||
|
const FormInput = React.forwardRef<HTMLInputElement, FormInputProps>( |
||||||
|
( |
||||||
|
{ |
||||||
|
label, |
||||||
|
error, |
||||||
|
name , |
||||||
|
className, |
||||||
|
labelClassName, |
||||||
|
containerClassName, |
||||||
|
helperText, |
||||||
|
labelPosition = "top", |
||||||
|
id, |
||||||
|
type = "text" ,
|
||||||
|
required = false , |
||||||
|
...props |
||||||
|
}, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-"); |
||||||
|
const {theme} = useTheme() |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={cn( |
||||||
|
"w-full", |
||||||
|
labelPosition === "left" && "grid grid-cols-4 gap-4 items-center", |
||||||
|
containerClassName |
||||||
|
)}> |
||||||
|
{label && ( |
||||||
|
<Label |
||||||
|
htmlFor={inputId} |
||||||
|
className={cn( |
||||||
|
labelPosition === "left" && "text-right", |
||||||
|
error && "text-destructive", |
||||||
|
labelClassName, |
||||||
|
'font-medium' |
||||||
|
)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
{required ? <span className="text-red-400"> *</span> : ""} |
||||||
|
</Label> |
||||||
|
)} |
||||||
|
<div className={cn( |
||||||
|
"flex flex-col gap-1.5", |
||||||
|
labelPosition === "left" && "col-span-3", |
||||||
|
labelPosition !== "left" && 'mt-2' |
||||||
|
)}> |
||||||
|
<Input |
||||||
|
id={inputId} |
||||||
|
ref={ref} |
||||||
|
name={name} |
||||||
|
type={type} |
||||||
|
className={cn( |
||||||
|
error && "border-destructive", |
||||||
|
className ,
|
||||||
|
type === 'date' && theme, |
||||||
|
'py-[20px]' |
||||||
|
)} |
||||||
|
aria-describedby={ |
||||||
|
error ? `${inputId}-error` :
|
||||||
|
helperText ? `${inputId}-helper` :
|
||||||
|
undefined |
||||||
|
} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
{helperText && ( |
||||||
|
<p |
||||||
|
id={`${inputId}-helper`} |
||||||
|
className="text-sm text-muted-foreground" |
||||||
|
> |
||||||
|
{helperText} |
||||||
|
</p> |
||||||
|
)} |
||||||
|
{error?.map((item: string, index: any) => ( |
||||||
|
<span key={index} className="text-sm text-destructive"> |
||||||
|
{item} |
||||||
|
<br></br> |
||||||
|
</span> |
||||||
|
))} |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
FormInput.displayName = "FormInput"; |
||||||
|
|
||||||
|
export default FormInput; |
@ -0,0 +1,47 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import React from "react"; |
||||||
|
import Stepper from "@/components/ui/stepper"; |
||||||
|
import { Step } from "@/helpers/commonSchema/steps.schema"; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function MultiStepForm({steps , renderContent , className} :
|
||||||
|
{steps : Step[] , renderContent : ( step : number , changeNext : () => void , changePrev : () => void ) => React.ReactNode , className? : string} |
||||||
|
) { |
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = React.useState(1); |
||||||
|
|
||||||
|
const handleNext = () => { |
||||||
|
if (currentStep < steps.length) { |
||||||
|
setCurrentStep((prev) => prev + 1); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handlePrev = () => { |
||||||
|
if (currentStep > 1) { |
||||||
|
setCurrentStep((prev) => prev - 1); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleStepClick = (stepNumber: number) => { |
||||||
|
setCurrentStep(stepNumber); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={`max-w-[70vw] w-full mx-auto ${className}`}> |
||||||
|
<div className=" space-y-6"> |
||||||
|
<Stepper |
||||||
|
steps={steps} |
||||||
|
currentStep={currentStep} |
||||||
|
onStepClick={handleStepClick} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
{ |
||||||
|
renderContent(currentStep , handleNext , handlePrev) |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
import Calendar from "@sbmdkl/nepali-datepicker-reactjs"; |
||||||
|
import '@sbmdkl/nepali-datepicker-reactjs/dist/index.css'; |
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react"; |
||||||
|
import NepaliDate from "nepali-date-converter"; |
||||||
|
import { AdToBs, getTomorrowEnglishDate, getYesterdayEnglishDate } from "@/helpers/date.helper" |
||||||
|
import { Label } from "../ui/label"; |
||||||
|
|
||||||
|
interface NepaliDateFieldProps { |
||||||
|
children?: React.ReactNode; |
||||||
|
wrapperClassName?: string; |
||||||
|
id?: string; |
||||||
|
className?: string; |
||||||
|
label?: string; |
||||||
|
labelStyle?: "label-left" | 'label-top'; |
||||||
|
labelWidth?: String; |
||||||
|
name?: string; |
||||||
|
status?: string; |
||||||
|
disabled?: boolean; |
||||||
|
value?: any; |
||||||
|
defaultValue?: any; |
||||||
|
formGroup?: string; |
||||||
|
hasIcon?: string; |
||||||
|
size?: string; |
||||||
|
placeholder?: string; |
||||||
|
rows?: number; |
||||||
|
hint?: string; |
||||||
|
required?: boolean; |
||||||
|
hidden?: boolean; |
||||||
|
fieldErrors?: Array<string>; |
||||||
|
helpText?: String; |
||||||
|
onChange?: (e: any) => void; |
||||||
|
step?: string; |
||||||
|
disableFutureDate?: boolean; |
||||||
|
position?: "bottom" | "left"; |
||||||
|
disablePastDate?: boolean; |
||||||
|
isToday? : boolean |
||||||
|
} |
||||||
|
|
||||||
|
const NepaliDateField: React.FC<NepaliDateFieldProps> = ({ |
||||||
|
className = "", |
||||||
|
id, |
||||||
|
wrapperClassName = "", |
||||||
|
children, |
||||||
|
label, |
||||||
|
labelStyle, |
||||||
|
labelWidth, |
||||||
|
name, |
||||||
|
status, |
||||||
|
disabled, |
||||||
|
value, |
||||||
|
defaultValue, |
||||||
|
formGroup, |
||||||
|
hasIcon, |
||||||
|
size, |
||||||
|
placeholder, |
||||||
|
rows, |
||||||
|
hint, |
||||||
|
required, |
||||||
|
fieldErrors = [], |
||||||
|
helpText, |
||||||
|
hidden = false, |
||||||
|
onChange, |
||||||
|
disableFutureDate, |
||||||
|
position = "bottom", |
||||||
|
disablePastDate, |
||||||
|
isToday = false , |
||||||
|
...props |
||||||
|
}) => { |
||||||
|
const today = new NepaliDate().format("YYYY-MM-DD"); |
||||||
|
const tomorrow = getTomorrowEnglishDate(); |
||||||
|
const previousDay = getYesterdayEnglishDate(); |
||||||
|
const currentNepaliDate = new NepaliDate().format("YYYY-MM-DD"); |
||||||
|
const tomorrowNepaliDate = AdToBs(tomorrow); |
||||||
|
const yesterdayNepaliDate = AdToBs(previousDay); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (disabled) { |
||||||
|
document.querySelector('._1oYOS')?.setAttribute('disabled', 'true'); |
||||||
|
} else { |
||||||
|
document.querySelector('._1oYOS')?.removeAttribute('disabled'); |
||||||
|
} |
||||||
|
}, [disabled]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={`w-full flex-col ${wrapperClassName} ${ |
||||||
|
position === "left" ? "inputLeft" : "" |
||||||
|
}`}
|
||||||
|
> |
||||||
|
{label && ( |
||||||
|
<Label id={id && id}> |
||||||
|
{label} |
||||||
|
</Label> |
||||||
|
)} |
||||||
|
<div className={label && 'w-full mt-2'}> |
||||||
|
<Calendar |
||||||
|
onChange={onChange} |
||||||
|
hideDefaultValue={defaultValue ? false : (isToday ? false : true)} |
||||||
|
defaultDate={defaultValue ?? (isToday ? today : undefined)} |
||||||
|
placeholder={"YYYY-MM-DD"} |
||||||
|
language={"en"} |
||||||
|
value={value ?? ''} |
||||||
|
className={`border rounded-md py-1.5 px-1.5 w-48 ${className} text-xs `} |
||||||
|
maxDate={disableFutureDate ? tomorrowNepaliDate : undefined} |
||||||
|
minDate={disablePastDate ? yesterdayNepaliDate : undefined} |
||||||
|
disabled={disabled} |
||||||
|
/> |
||||||
|
{children} |
||||||
|
{helpText && ( |
||||||
|
<div className="text-gray-800 text-xs mt-1">{helpText}</div> |
||||||
|
)} |
||||||
|
<span className="text-error-500 text-xs"> |
||||||
|
{fieldErrors.map((item: string, index: any) => ( |
||||||
|
<span key={index}> |
||||||
|
{item} |
||||||
|
<br></br> |
||||||
|
</span> |
||||||
|
))} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default NepaliDateField; |
@ -0,0 +1,125 @@ |
|||||||
|
import React, { useEffect, useRef } from "react"; |
||||||
|
import { |
||||||
|
Select, |
||||||
|
SelectContent, |
||||||
|
SelectItem, |
||||||
|
SelectTrigger, |
||||||
|
SelectValue, |
||||||
|
} from "@/components/ui/select"; |
||||||
|
import { Label } from "@/components/ui/label"; |
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { ChoiceType } from "@/helpers/commonSchema/common.schema"; |
||||||
|
|
||||||
|
interface SelectFieldProps { |
||||||
|
className?: string; |
||||||
|
id?: string; |
||||||
|
label?: string; |
||||||
|
labelStyle?: string; |
||||||
|
labelWidth?: string; |
||||||
|
name: string; |
||||||
|
labelClassName?: string; |
||||||
|
placeholder?: string; |
||||||
|
defaultValue?: string | number | boolean; |
||||||
|
value?: string | number | boolean; |
||||||
|
required?: boolean; |
||||||
|
wrapperClassName?: string; |
||||||
|
options: { value: string | number; label: string }[]; |
||||||
|
fieldErrors?: Array<string>; |
||||||
|
clearSelectValue?: boolean; |
||||||
|
setClearSelectValue?: React.Dispatch<React.SetStateAction<boolean>>; |
||||||
|
isDisabled?: boolean; |
||||||
|
onChange?: (value: ChoiceType | any) => void; |
||||||
|
} |
||||||
|
|
||||||
|
const SelectField: React.FC<SelectFieldProps> = ({ |
||||||
|
className = "", |
||||||
|
id, |
||||||
|
label, |
||||||
|
labelStyle, |
||||||
|
name, |
||||||
|
defaultValue, |
||||||
|
value, |
||||||
|
placeholder = `Select ${label}`, |
||||||
|
required, |
||||||
|
options, |
||||||
|
wrapperClassName = "", |
||||||
|
fieldErrors = [], |
||||||
|
clearSelectValue = false, |
||||||
|
setClearSelectValue, |
||||||
|
isDisabled, |
||||||
|
onChange, |
||||||
|
labelClassName |
||||||
|
}) => { |
||||||
|
const selectRef = useRef<HTMLButtonElement>(null); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (clearSelectValue === true) { |
||||||
|
onChange?.(""); |
||||||
|
setClearSelectValue?.(false); |
||||||
|
} |
||||||
|
}, [clearSelectValue, setClearSelectValue, onChange]); |
||||||
|
|
||||||
|
const wrapperClasses = cn( |
||||||
|
wrapperClassName, |
||||||
|
labelStyle === "label-left" && "flex justify-between items-center gap-2" |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={wrapperClasses}> |
||||||
|
{label && ( |
||||||
|
<Label
|
||||||
|
htmlFor={id ?? name}
|
||||||
|
className={cn(labelClassName)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
{required && <span className="text-destructive ml-1">*</span>} |
||||||
|
</Label> |
||||||
|
)} |
||||||
|
|
||||||
|
<div className={cn( |
||||||
|
"w-full", |
||||||
|
labelStyle !== "label-left" && "mt-2" |
||||||
|
)}> |
||||||
|
<Select |
||||||
|
defaultValue={defaultValue?.toString()} |
||||||
|
value={value?.toString()} |
||||||
|
onValueChange={onChange} |
||||||
|
disabled={isDisabled} |
||||||
|
|
||||||
|
> |
||||||
|
<SelectTrigger
|
||||||
|
ref={selectRef} |
||||||
|
className={cn( |
||||||
|
"w-full font-normal", |
||||||
|
className |
||||||
|
)} |
||||||
|
id={id ?? name} |
||||||
|
> |
||||||
|
<SelectValue placeholder={placeholder} /> |
||||||
|
</SelectTrigger> |
||||||
|
<SelectContent className="max-h-[200px]"> |
||||||
|
{options.map((option) => ( |
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value.toString()} |
||||||
|
> |
||||||
|
{option.label} |
||||||
|
</SelectItem> |
||||||
|
))} |
||||||
|
</SelectContent> |
||||||
|
</Select> |
||||||
|
|
||||||
|
{fieldErrors.map((error, index) => ( |
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="block text-destructive text-xs mt-1" |
||||||
|
> |
||||||
|
{error} |
||||||
|
</span> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SelectField; |
@ -0,0 +1,54 @@ |
|||||||
|
import React from "react" |
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator
|
||||||
|
|
||||||
|
} from "@/components/ui/breadcrumb" |
||||||
|
import { Link } from "lucide-react" |
||||||
|
|
||||||
|
interface BreadCrumbItemInterface { |
||||||
|
title : string |
||||||
|
href : string |
||||||
|
|
||||||
|
} |
||||||
|
interface BreadCrumbNavPropsInterface { |
||||||
|
breadCrumbItems : BreadCrumbItemInterface[] |
||||||
|
} |
||||||
|
|
||||||
|
const BreadCrumbNav : React.FC<BreadCrumbNavPropsInterface> = ({ |
||||||
|
breadCrumbItems |
||||||
|
}) => { |
||||||
|
|
||||||
|
return( |
||||||
|
<Breadcrumb> |
||||||
|
<BreadcrumbList> |
||||||
|
{ |
||||||
|
breadCrumbItems?.length > 1 && |
||||||
|
breadCrumbItems.map((item: BreadCrumbItemInterface, index: number) => { |
||||||
|
const isLastItem = index === breadCrumbItems.length - 1; |
||||||
|
return ( |
||||||
|
<React.Fragment key={index}> |
||||||
|
<BreadcrumbItem> |
||||||
|
{isLastItem ? ( |
||||||
|
<BreadcrumbPage>{item.title}</BreadcrumbPage> |
||||||
|
) : ( |
||||||
|
<BreadcrumbLink href={item.href}> |
||||||
|
{item.title} |
||||||
|
</BreadcrumbLink> |
||||||
|
)} |
||||||
|
</BreadcrumbItem> |
||||||
|
{!isLastItem && <BreadcrumbSeparator />} |
||||||
|
</React.Fragment> |
||||||
|
); |
||||||
|
}) |
||||||
|
} |
||||||
|
</BreadcrumbList> |
||||||
|
</Breadcrumb> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default BreadCrumbNav |
@ -0,0 +1,377 @@ |
|||||||
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from "react" |
||||||
|
import { useSearchParams, useRouter } from "next/navigation" |
||||||
|
import { ChevronDown, Cross, Loader2, MoreHorizontal, X } from "lucide-react" |
||||||
|
import { |
||||||
|
ColumnDef, |
||||||
|
ColumnFiltersState, |
||||||
|
SortingState, |
||||||
|
VisibilityState, |
||||||
|
flexRender, |
||||||
|
getCoreRowModel, |
||||||
|
getFilteredRowModel, |
||||||
|
getPaginationRowModel, |
||||||
|
getSortedRowModel, |
||||||
|
useReactTable, |
||||||
|
} from "@tanstack/react-table" |
||||||
|
import { Button } from "@/components/ui/button" |
||||||
|
import { Checkbox } from "@/components/ui/checkbox" |
||||||
|
import { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuCheckboxItem, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuTrigger, |
||||||
|
} from "@/components/ui/dropdown-menu" |
||||||
|
import { Input } from "@/components/ui/input" |
||||||
|
import { |
||||||
|
Table, |
||||||
|
TableBody, |
||||||
|
TableCell, |
||||||
|
TableHead, |
||||||
|
TableHeader, |
||||||
|
TableRow, |
||||||
|
} from "@/components/ui/table" |
||||||
|
import { DeleteInstances } from "../DeleteInstances/DeleteInstances" |
||||||
|
import BulkUpload from "../../elements/BulkUpload" |
||||||
|
import Download from "../../elements/Download" |
||||||
|
import TableSkeleton from "../../elements/TableSkeleton" |
||||||
|
|
||||||
|
const useDebounce = (callback: Function, delay: number) => { |
||||||
|
const timeoutRef = React.useRef<NodeJS.Timeout>(); |
||||||
|
|
||||||
|
React.useEffect(() => { |
||||||
|
return () => { |
||||||
|
if (timeoutRef.current) { |
||||||
|
clearTimeout(timeoutRef.current); |
||||||
|
} |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
return useCallback((...args: any[]) => { |
||||||
|
if (timeoutRef.current) { |
||||||
|
clearTimeout(timeoutRef.current); |
||||||
|
} |
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => { |
||||||
|
callback(...args); |
||||||
|
}, delay); |
||||||
|
}, [callback, delay]); |
||||||
|
}; |
||||||
|
|
||||||
|
interface DataTableProps<TData> { |
||||||
|
data: TData[] |
||||||
|
columns: ColumnDef<TData>[] |
||||||
|
searchKey?: string |
||||||
|
showCheckbox?: boolean |
||||||
|
actionDropdown?: any |
||||||
|
sn?: number |
||||||
|
customFilter?: React.ReactNode |
||||||
|
mutate: () => void |
||||||
|
isLoading?: boolean |
||||||
|
uploadURL?: string |
||||||
|
deleteURL?: string |
||||||
|
downloadSampleURL? : string |
||||||
|
downloadBtnLable? : string |
||||||
|
} |
||||||
|
|
||||||
|
export function DataTable<TData>({ |
||||||
|
data = [], |
||||||
|
columns: userColumns, |
||||||
|
searchKey, |
||||||
|
showCheckbox = false, |
||||||
|
actionDropdown, |
||||||
|
customFilter, |
||||||
|
mutate, |
||||||
|
uploadURL, |
||||||
|
isLoading = false, |
||||||
|
deleteURL, |
||||||
|
downloadSampleURL, |
||||||
|
downloadBtnLable |
||||||
|
}: DataTableProps<TData>) { |
||||||
|
const searchParams = useSearchParams() |
||||||
|
const router = useRouter() |
||||||
|
const [isSearching, setIsSearching] = useState(false) |
||||||
|
const inputRef = useRef<HTMLInputElement>(null) |
||||||
|
const shouldFocusRef = useRef(false) |
||||||
|
|
||||||
|
// Initialize states with proper types
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]) |
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) |
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) |
||||||
|
const [rowSelection, setRowSelection] = useState({}) |
||||||
|
const [deleteIds, setDeleteIds] = useState([]) |
||||||
|
|
||||||
|
// Initialize filter state from URL
|
||||||
|
const [localFilter, setLocalFilter] = useState(searchParams.get(searchKey ?? 'name') || "") |
||||||
|
|
||||||
|
// Memoize the columns configuration
|
||||||
|
const finalColumns = useMemo(() => { |
||||||
|
let cols: ColumnDef<TData>[] = [] |
||||||
|
|
||||||
|
if (showCheckbox) { |
||||||
|
cols.push({ |
||||||
|
id: "select", |
||||||
|
header: ({ table }) => ( |
||||||
|
<Checkbox |
||||||
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} |
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} |
||||||
|
aria-label="Select all" |
||||||
|
disabled={isLoading} |
||||||
|
/> |
||||||
|
), |
||||||
|
cell: ({ row }) => ( |
||||||
|
<Checkbox |
||||||
|
checked={row.getIsSelected()} |
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)} |
||||||
|
aria-label="Select row" |
||||||
|
disabled={isLoading} |
||||||
|
/> |
||||||
|
), |
||||||
|
enableSorting: false, |
||||||
|
enableHiding: false, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
cols = [...cols, ...userColumns] |
||||||
|
|
||||||
|
if (actionDropdown) { |
||||||
|
cols.push({ |
||||||
|
...actionDropdown, |
||||||
|
header: "Actions", |
||||||
|
enableSorting: false, |
||||||
|
enableHiding: false, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return cols |
||||||
|
}, [userColumns, showCheckbox, actionDropdown, isLoading]) |
||||||
|
|
||||||
|
const tableData = useMemo(() => { |
||||||
|
if (!data) return []; |
||||||
|
return data.map((item: any) => ({ |
||||||
|
...item, |
||||||
|
})); |
||||||
|
}, [data]); |
||||||
|
|
||||||
|
// Create table instance
|
||||||
|
const table = useReactTable({ |
||||||
|
data: tableData, |
||||||
|
columns: finalColumns, |
||||||
|
onSortingChange: setSorting, |
||||||
|
onColumnFiltersChange: setColumnFilters, |
||||||
|
getCoreRowModel: getCoreRowModel(), |
||||||
|
getPaginationRowModel: getPaginationRowModel(), |
||||||
|
getSortedRowModel: getSortedRowModel(), |
||||||
|
getFilteredRowModel: getFilteredRowModel(), |
||||||
|
onColumnVisibilityChange: setColumnVisibility, |
||||||
|
onRowSelectionChange: setRowSelection, |
||||||
|
state: { |
||||||
|
sorting, |
||||||
|
columnFilters, |
||||||
|
columnVisibility, |
||||||
|
rowSelection, |
||||||
|
}, |
||||||
|
enableFilters: true, |
||||||
|
manualFiltering: false, |
||||||
|
}) |
||||||
|
|
||||||
|
// Debounced URL update
|
||||||
|
const debouncedUpdateURL = useCallback( |
||||||
|
useDebounce(async (value: string) => { |
||||||
|
try { |
||||||
|
const currentParams = new URLSearchParams(searchParams?.toString() || ''); |
||||||
|
currentParams.set(searchKey ?? 'name', value); |
||||||
|
router.replace(`?${currentParams.toString()}`); |
||||||
|
if (typeof mutate === 'function') { |
||||||
|
await mutate(); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('Error updating search:', error); |
||||||
|
} finally { |
||||||
|
setIsSearching(false); |
||||||
|
shouldFocusRef.current = true; |
||||||
|
} |
||||||
|
}, 500), |
||||||
|
[searchParams, router, mutate, searchKey] |
||||||
|
); |
||||||
|
|
||||||
|
// Clear search parameters
|
||||||
|
const clearSearchParams = () => { |
||||||
|
const column = table.getColumn(searchKey as string); |
||||||
|
if (column) { |
||||||
|
column.setFilterValue(""); |
||||||
|
} |
||||||
|
setLocalFilter(""); |
||||||
|
setIsSearching(false); |
||||||
|
router.replace(window.location.pathname); |
||||||
|
if (typeof mutate === 'function') { |
||||||
|
mutate(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Handle input filter change
|
||||||
|
const handleFilterChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const value = event.target.value; |
||||||
|
setLocalFilter(value); |
||||||
|
setIsSearching(true); |
||||||
|
shouldFocusRef.current = true; |
||||||
|
debouncedUpdateURL(value); |
||||||
|
}, [debouncedUpdateURL]); |
||||||
|
|
||||||
|
// Handle focus after loading/searching states change
|
||||||
|
useEffect(() => { |
||||||
|
if (!isLoading && !isSearching && shouldFocusRef.current && inputRef.current) { |
||||||
|
inputRef.current.focus(); |
||||||
|
shouldFocusRef.current = false; |
||||||
|
} |
||||||
|
}, [isLoading, isSearching]); |
||||||
|
|
||||||
|
// Initialize filter from URL
|
||||||
|
useEffect(() => { |
||||||
|
const urlFilter = searchParams?.get(searchKey ?? 'name'); |
||||||
|
if (searchKey && urlFilter) { |
||||||
|
setLocalFilter(urlFilter); |
||||||
|
} |
||||||
|
}, [searchParams, searchKey]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="w-full"> |
||||||
|
<div className="flex items-center py-4 justify-between"> |
||||||
|
<div className="flex gap-2"> |
||||||
|
{searchKey && ( |
||||||
|
<div className="relative"> |
||||||
|
<Input |
||||||
|
placeholder={`Filter ${searchKey}...`} |
||||||
|
value={localFilter} |
||||||
|
onChange={handleFilterChange} |
||||||
|
className="max-w-sm" |
||||||
|
disabled={isLoading} |
||||||
|
ref={inputRef} |
||||||
|
/> |
||||||
|
{(isLoading || isSearching) ? ( |
||||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2"> |
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> |
||||||
|
</div> |
||||||
|
) : searchParams?.get(searchKey) ? ( |
||||||
|
<button
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2" |
||||||
|
onClick={clearSearchParams} |
||||||
|
> |
||||||
|
<X className="h-4 w-4 text-muted-foreground" /> |
||||||
|
</button> |
||||||
|
) : null} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{customFilter} |
||||||
|
</div> |
||||||
|
<div className="w-fit flex gap-4"> |
||||||
|
{table.getSelectedRowModel().rows.map(row => row.original?.id)?.length > 0 && deleteURL &&
|
||||||
|
<DeleteInstances
|
||||||
|
url={deleteURL} |
||||||
|
mutate={mutate} |
||||||
|
ids={table.getSelectedRowModel().rows.map(row => row.original?.id)} |
||||||
|
variant={'outline'} |
||||||
|
/> |
||||||
|
} |
||||||
|
<DropdownMenu> |
||||||
|
<DropdownMenuTrigger asChild> |
||||||
|
<Button variant="outline" className="ml-auto" disabled={isLoading}> |
||||||
|
Columns <ChevronDown className="ml-2 h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
<DropdownMenuContent align="end"> |
||||||
|
{table |
||||||
|
.getAllColumns() |
||||||
|
.filter((column) => column.getCanHide()) |
||||||
|
.map((column) => ( |
||||||
|
<DropdownMenuCheckboxItem |
||||||
|
key={column.id} |
||||||
|
className="capitalize" |
||||||
|
checked={column.getIsVisible()} |
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)} |
||||||
|
> |
||||||
|
{column.id} |
||||||
|
</DropdownMenuCheckboxItem> |
||||||
|
))} |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
{(uploadURL || downloadSampleURL) &&
|
||||||
|
<DropdownMenu> |
||||||
|
<DropdownMenuTrigger asChild className=""> |
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0"> |
||||||
|
<span className="sr-only">Open menu</span> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
<DropdownMenuContent align="end"> |
||||||
|
{uploadURL &&
|
||||||
|
<DropdownMenuItem asChild> |
||||||
|
<BulkUpload submitUrl={uploadURL} mutate={mutate}/> |
||||||
|
</DropdownMenuItem> |
||||||
|
} |
||||||
|
{ |
||||||
|
downloadSampleURL &&
|
||||||
|
<DropdownMenuItem asChild className="mt-2"> |
||||||
|
<Download url={downloadSampleURL} label={downloadBtnLable}/> |
||||||
|
</DropdownMenuItem> |
||||||
|
} |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="rounded-md border"> |
||||||
|
<Table className="custom-scrollbar" key={data?.length}> |
||||||
|
<TableHeader> |
||||||
|
{table.getHeaderGroups().map((headerGroup) => ( |
||||||
|
<TableRow key={headerGroup.id}> |
||||||
|
{headerGroup.headers.map((header) => ( |
||||||
|
<TableHead key={header.id}> |
||||||
|
{header.isPlaceholder |
||||||
|
? null |
||||||
|
: flexRender( |
||||||
|
header.column.columnDef.header, |
||||||
|
header.getContext() |
||||||
|
)} |
||||||
|
</TableHead> |
||||||
|
))} |
||||||
|
</TableRow> |
||||||
|
))} |
||||||
|
</TableHeader> |
||||||
|
<TableBody> |
||||||
|
{isLoading ? ( |
||||||
|
<TableSkeleton columns={finalColumns.length} rows={5}/> |
||||||
|
) : table.getRowModel().rows.length > 0 ? ( |
||||||
|
table.getRowModel().rows.map((row) => ( |
||||||
|
<TableRow |
||||||
|
key={row.id} |
||||||
|
data-state={row.getIsSelected() && "selected"} |
||||||
|
> |
||||||
|
{row.getVisibleCells().map((cell) => ( |
||||||
|
<TableCell key={cell.id}> |
||||||
|
{flexRender( |
||||||
|
cell.column.columnDef.cell, |
||||||
|
cell.getContext() |
||||||
|
)} |
||||||
|
</TableCell> |
||||||
|
))} |
||||||
|
</TableRow> |
||||||
|
)) |
||||||
|
) : ( |
||||||
|
<TableRow> |
||||||
|
<TableCell |
||||||
|
colSpan={finalColumns.length} |
||||||
|
className="h-24 text-center" |
||||||
|
> |
||||||
|
No results. |
||||||
|
</TableCell> |
||||||
|
</TableRow> |
||||||
|
)} |
||||||
|
</TableBody> |
||||||
|
</Table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default DataTable; |
@ -0,0 +1,117 @@ |
|||||||
|
import { |
||||||
|
AlertDialog, |
||||||
|
AlertDialogAction, |
||||||
|
AlertDialogCancel, |
||||||
|
AlertDialogContent, |
||||||
|
AlertDialogDescription, |
||||||
|
AlertDialogFooter, |
||||||
|
AlertDialogHeader, |
||||||
|
AlertDialogTitle, |
||||||
|
AlertDialogTrigger, |
||||||
|
} from "@/components/ui/alert-dialog" |
||||||
|
import { Button } from "@/components/ui/button" |
||||||
|
import { fetchHeader } from "@/helpers/fetch.helper"; |
||||||
|
import { useToast } from "@/hooks/use-toast"; |
||||||
|
import { Loader, Trash2 } from "lucide-react"; |
||||||
|
import { useState } from "react" |
||||||
|
|
||||||
|
export function DeleteInstances({ |
||||||
|
url, |
||||||
|
mutate, |
||||||
|
ids, |
||||||
|
variant = "ghost" , |
||||||
|
// open,
|
||||||
|
// onOpenChange,
|
||||||
|
showTitle = true , |
||||||
|
className |
||||||
|
}: { |
||||||
|
url: string; |
||||||
|
mutate: () => void; |
||||||
|
ids: Array<string>; |
||||||
|
variant? : "ghost" | "link" | "default" | "destructive" | "outline" | "secondary" | null | undefined |
||||||
|
className? : string |
||||||
|
showTitle? : boolean |
||||||
|
// open: boolean;
|
||||||
|
// onOpenChange: (value: boolean) => void;
|
||||||
|
}) { |
||||||
|
const [showModal , setShowModal] = useState<boolean>(false)
|
||||||
|
const [loading, setLoading] = useState<boolean>(false); |
||||||
|
const { toast } = useToast(); |
||||||
|
|
||||||
|
const handleDelete = async (e: React.MouseEvent): Promise<void> => { |
||||||
|
|
||||||
|
const formData = new FormData(); |
||||||
|
ids.forEach((id : string , index : number) => { |
||||||
|
formData.append(`ids[${index}]` , id) |
||||||
|
}) |
||||||
|
|
||||||
|
try { |
||||||
|
setLoading(true); |
||||||
|
const res = await fetch(url, { |
||||||
|
headers: fetchHeader(), |
||||||
|
method: "POST", |
||||||
|
body: formData, |
||||||
|
}); |
||||||
|
const data = await res.json(); |
||||||
|
if (data?.success) { |
||||||
|
toast({ |
||||||
|
title: "Success", |
||||||
|
description: data?.message, |
||||||
|
variant: "success", |
||||||
|
}); |
||||||
|
mutate(); |
||||||
|
setShowModal(false); // Close the dialog on success
|
||||||
|
}else{ |
||||||
|
toast({ |
||||||
|
variant: "destructive", |
||||||
|
title: "Error", |
||||||
|
description: data?.message, |
||||||
|
}); |
||||||
|
} |
||||||
|
} catch (error: any) { |
||||||
|
toast({ |
||||||
|
variant: "destructive", |
||||||
|
title: "Error", |
||||||
|
description: error?.message, |
||||||
|
}); |
||||||
|
} finally { |
||||||
|
setLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<AlertDialog open={showModal} onOpenChange={setShowModal}> |
||||||
|
<AlertDialogTrigger asChild> |
||||||
|
<Button |
||||||
|
variant={variant} |
||||||
|
size="sm" |
||||||
|
className={`text-red-500 hover:text-red-700 !w-full text-left flex !px-2 h-auto min-h-[36px]
|
||||||
|
${variant == 'outline' ? 'justify-center' : 'justify-start'} ${className}`}
|
||||||
|
> |
||||||
|
<Trash2 className="h-4 w-4" /> |
||||||
|
{showTitle && 'Delete'} |
||||||
|
{loading ? <Loader /> : null} |
||||||
|
</Button> |
||||||
|
</AlertDialogTrigger> |
||||||
|
<AlertDialogContent> |
||||||
|
<AlertDialogHeader> |
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> |
||||||
|
<AlertDialogDescription> |
||||||
|
This action cannot be undone. This will delete your data. |
||||||
|
</AlertDialogDescription> |
||||||
|
</AlertDialogHeader> |
||||||
|
<AlertDialogFooter> |
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel> |
||||||
|
<AlertDialogAction |
||||||
|
className="bg-red-500 hover:bg-red-300" |
||||||
|
onClick={(e) => handleDelete(e)} |
||||||
|
> |
||||||
|
Continue |
||||||
|
</AlertDialogAction> |
||||||
|
</AlertDialogFooter> |
||||||
|
</AlertDialogContent> |
||||||
|
</AlertDialog> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@ |
|||||||
|
import Link from "next/link"; |
||||||
|
|
||||||
|
export default function AppFooter() { |
||||||
|
return ( |
||||||
|
<div className="z-20 w-full bg-background/95 shadow backdrop-blur supports-[backdrop-filter]:bg-background/60"> |
||||||
|
<div className="mx-4 md:mx-8 flex h-14 items-center"> |
||||||
|
<p className="text-xs md:text-sm leading-loose text-muted-foreground text-left"> |
||||||
|
©copyright{" "} |
||||||
|
<Link |
||||||
|
href="www.kumarijob.com" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
className="font-medium underline underline-offset-4" |
||||||
|
> |
||||||
|
Kumari Job |
||||||
|
</Link> |
||||||
|
. All rights reserved |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import { SearchIcon } from "lucide-react"; |
||||||
|
import { ModeToggle } from "./_partials/modeToggle"; |
||||||
|
import { SheetMenu } from "./_partials/sheetMenu"; |
||||||
|
import { UserNav } from "./_partials/userNav"; |
||||||
|
import UserNotifications from "./_partials/userNotifications"; |
||||||
|
import { Input } from "@/components/ui/input"; |
||||||
|
|
||||||
|
|
||||||
|
const AppHeader : React.FC = () => { |
||||||
|
return ( |
||||||
|
<header className="sticky top-0 z-10 w-full bg-background/95 shadow backdrop-blur
|
||||||
|
supports-[backdrop-filter]:bg-background/60
|
||||||
|
dark:shadow-secondary"> |
||||||
|
<div className="flex h-14 items-center container py-4 px-4 sm:px-8 mx-auto"> |
||||||
|
<div className="flex items-center space-x-4 lg:space-x-0 mr-4"> |
||||||
|
<SheetMenu /> |
||||||
|
</div> |
||||||
|
<div className="relative flex-1 max-w-md md:max-w-lg sm:ml-0 ml-2"> |
||||||
|
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> |
||||||
|
<Input |
||||||
|
type="search" |
||||||
|
placeholder="Search..." |
||||||
|
className="w-full rounded-lg bg-muted pl-8 pr-4 py-2 text-sm focus:outline-none focus:ring-1
|
||||||
|
focus:ring-primary focus:border-primary" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div className="flex flex-1 items-center gap-3 justify-end"> |
||||||
|
<UserNotifications /> |
||||||
|
<ModeToggle /> |
||||||
|
<UserNav /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default AppHeader |
||||||
|
|
||||||
|
|
@ -0,0 +1,66 @@ |
|||||||
|
import React, { useContext } from 'react'; |
||||||
|
import { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuTrigger, |
||||||
|
} from "@/components/ui/dropdown-menu"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { GraduationCap, Grid3x3, Headset, UserRoundCog, UserSearch, Warehouse } from "lucide-react"; |
||||||
|
import Link from 'next/link'; |
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; |
||||||
|
import { AuthContext } from '@/helpers/context/AuthProvider'; |
||||||
|
|
||||||
|
const apps = [ |
||||||
|
{ icon: UserRoundCog, name: 'PIMS', color: 'text-red-600' , link : `${process.env.NEXT_PUBLIC_HRMS_FRONTEND}`}, |
||||||
|
{ icon: Warehouse, name: 'Asset', color: 'text-orange-600' , link : `${process.env.NEXT_PUBLIC_HRMS_FRONTEND}`}, |
||||||
|
{ icon: Headset, name: 'CRMS', color: 'text-blue-500' , link : `${process.env.NEXT_PUBLIC_CRM_FRONTEND}`}, |
||||||
|
{ icon: GraduationCap, name: 'TMS', color: 'text-yellow-500' , link : `${process.env.NEXT_PUBLIC_TMS_FRONTEND}`}, |
||||||
|
{ icon: UserSearch, name: 'Job Board', color: 'text-green-500' , link : `${process.env.NEXT_PUBLIC_EMPLOYEE_FRONTEND}`}, |
||||||
|
]; |
||||||
|
|
||||||
|
const ApplicationSwitch : React.FC = () => { |
||||||
|
|
||||||
|
const { user } = useContext(AuthContext)
|
||||||
|
return ( |
||||||
|
<DropdownMenu> |
||||||
|
<DropdownMenuTrigger asChild> |
||||||
|
<Button |
||||||
|
variant="outline" |
||||||
|
className="relative h-8 w-8 rounded-full" |
||||||
|
> |
||||||
|
<Grid3x3 className="w-[1.2rem] h-[1.2rem]"/> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
<DropdownMenuContent align="end" className="w-60 p-2"> |
||||||
|
<div className="grid grid-cols-3 gap-4 p-2"> |
||||||
|
<DropdownMenuItem> |
||||||
|
<Link href={`${process.env.IDP_FRONTEND}`} className=''> |
||||||
|
<Avatar className="h-8 w-8"> |
||||||
|
{ |
||||||
|
user?.pfpName ?
|
||||||
|
<AvatarImage src={user?.pfpName} alt={user.firstName} /> :
|
||||||
|
<AvatarFallback className="bg-transparent">uk</AvatarFallback> |
||||||
|
} |
||||||
|
</Avatar> |
||||||
|
<span>Profile</span> |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
{apps.map((app) => ( |
||||||
|
<DropdownMenuItem |
||||||
|
key={app.name} |
||||||
|
className="flex flex-col items-center justify-center p-2 focus:bg-accent cursor-pointer" |
||||||
|
> |
||||||
|
<Link href={app.link} className={`hover:text-${app.color}`}> |
||||||
|
<app.icon className={`h-6 w-6 mb-1 ${app.color}`} /> |
||||||
|
<span className="text-xs">{app.name}</span> |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default ApplicationSwitch; |
@ -0,0 +1,40 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import * as React from "react"; |
||||||
|
// import { useTheme } from "next-themes";
|
||||||
|
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; |
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { |
||||||
|
Tooltip, |
||||||
|
TooltipContent, |
||||||
|
TooltipTrigger, |
||||||
|
TooltipProvider |
||||||
|
} from "@/components/ui/tooltip"; |
||||||
|
import { useTheme } from "@/hooks/use-theme"; |
||||||
|
|
||||||
|
export function ModeToggle() { |
||||||
|
|
||||||
|
const { toggleTheme, theme } = useTheme(); |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<TooltipProvider disableHoverableContent> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger asChild> |
||||||
|
<Button |
||||||
|
className="rounded-full w-8 h-8 bg-background" |
||||||
|
variant="outline" |
||||||
|
size="icon" |
||||||
|
onClick={() => toggleTheme()} |
||||||
|
> |
||||||
|
<SunIcon className="w-[1.2rem] h-[1.2rem] rotate-90 scale-0 transition-transform ease-in-out duration-500 dark:rotate-0 dark:scale-100" /> |
||||||
|
<MoonIcon className="absolute w-[1.2rem] h-[1.2rem] rotate-0 scale-1000 transition-transform ease-in-out duration-500 dark:-rotate-90 dark:scale-0" /> |
||||||
|
<span className="sr-only">Switch Theme</span> |
||||||
|
</Button> |
||||||
|
</TooltipTrigger> |
||||||
|
<TooltipContent side="bottom">Switch Theme</TooltipContent> |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import Link from "next/link"; |
||||||
|
import { MenuIcon, PanelsTopLeft } from "lucide-react"; |
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { |
||||||
|
Sheet, |
||||||
|
SheetHeader, |
||||||
|
SheetContent, |
||||||
|
SheetTrigger, |
||||||
|
SheetTitle |
||||||
|
} from "@/components/ui/sheet"; |
||||||
|
import { Menu } from "../../Sidebar/_partials/menu"; |
||||||
|
|
||||||
|
export function SheetMenu() { |
||||||
|
return ( |
||||||
|
<Sheet> |
||||||
|
<SheetTrigger className="lg:hidden" asChild> |
||||||
|
<Button className="h-8" variant="outline" size="icon"> |
||||||
|
<MenuIcon size={20} /> |
||||||
|
</Button> |
||||||
|
</SheetTrigger> |
||||||
|
<SheetContent className="sm:w-72 px-3 h-full flex flex-col" side="left"> |
||||||
|
<SheetHeader> |
||||||
|
<Button |
||||||
|
className="flex justify-center items-center pb-2 pt-1" |
||||||
|
variant="link" |
||||||
|
asChild |
||||||
|
> |
||||||
|
<Link href="/dashboard" className="flex items-center gap-2"> |
||||||
|
<PanelsTopLeft className="w-6 h-6 mr-1" /> |
||||||
|
<SheetTitle className="font-bold text-lg">Brand</SheetTitle> |
||||||
|
</Link> |
||||||
|
</Button> |
||||||
|
</SheetHeader> |
||||||
|
<Menu isOpen /> |
||||||
|
</SheetContent> |
||||||
|
</Sheet> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import Link from "next/link"; |
||||||
|
import { LayoutGrid, LogOut, RefreshCcw, User } from "lucide-react"; |
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; |
||||||
|
|
||||||
|
import { |
||||||
|
Tooltip, |
||||||
|
TooltipContent, |
||||||
|
TooltipTrigger, |
||||||
|
TooltipProvider |
||||||
|
} from "@/components/ui/tooltip"; |
||||||
|
import { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuGroup, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuLabel, |
||||||
|
DropdownMenuSeparator, |
||||||
|
DropdownMenuTrigger |
||||||
|
} from "@/components/ui/dropdown-menu"; |
||||||
|
import { useContext } from "react"; |
||||||
|
import { AuthContext } from "@/helpers/context/AuthProvider"; |
||||||
|
|
||||||
|
export function UserNav() { |
||||||
|
|
||||||
|
const { user , logout } = useContext(AuthContext) |
||||||
|
|
||||||
|
return ( |
||||||
|
<DropdownMenu> |
||||||
|
<TooltipProvider disableHoverableContent> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger asChild> |
||||||
|
<DropdownMenuTrigger asChild> |
||||||
|
<Button |
||||||
|
variant="outline" |
||||||
|
className="relative h-8 w-8 rounded-full" |
||||||
|
> |
||||||
|
<Avatar className="h-8 w-8"> |
||||||
|
{ |
||||||
|
user?.pfpName ?
|
||||||
|
<AvatarImage src={user?.pfpName} alt={user.firstName} /> :
|
||||||
|
<AvatarFallback className="bg-transparent">uk</AvatarFallback> |
||||||
|
} |
||||||
|
</Avatar> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
</TooltipTrigger> |
||||||
|
<TooltipContent side="bottom">Profile</TooltipContent> |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
|
||||||
|
<DropdownMenuContent className="w-56" align="end" forceMount> |
||||||
|
<DropdownMenuLabel className="font-normal"> |
||||||
|
<div className="flex flex-col space-y-1"> |
||||||
|
<p className="text-sm font-medium leading-none">{user?.firstName}</p> |
||||||
|
<p className="text-xs leading-none text-muted-foreground"> |
||||||
|
{user?.email} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</DropdownMenuLabel> |
||||||
|
<DropdownMenuSeparator /> |
||||||
|
<DropdownMenuGroup> |
||||||
|
<DropdownMenuItem className="hover:cursor-pointer" asChild> |
||||||
|
<Link href="/dashboard" className="flex items-center"> |
||||||
|
<LayoutGrid className="w-4 h-4 mr-3 text-muted-foreground" /> |
||||||
|
Dashboard |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
<DropdownMenuItem className="hover:cursor-pointer" asChild> |
||||||
|
<Link href="/account" className="flex items-center"> |
||||||
|
<User className="w-4 h-4 mr-3 text-muted-foreground" /> |
||||||
|
Account |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
<DropdownMenuItem className="hover:cursor-pointer" asChild> |
||||||
|
<Link href="/account" className="flex items-center"> |
||||||
|
<RefreshCcw className="w-4 h-4 mr-3 text-muted-foreground" /> |
||||||
|
Switch Company |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
</DropdownMenuGroup> |
||||||
|
<DropdownMenuSeparator /> |
||||||
|
<DropdownMenuItem className="hover:cursor-pointer" onClick={() => logout()}> |
||||||
|
<LogOut className="w-4 h-4 mr-3 text-muted-foreground" /> |
||||||
|
Sign out |
||||||
|
</DropdownMenuItem> |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
import { Button } from "@/components/ui/button" |
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@/components/ui/dropdown-menu" |
||||||
|
import { BellIcon } from "lucide-react" |
||||||
|
import Image from "next/image" |
||||||
|
import Link from "next/link" |
||||||
|
|
||||||
|
const UserNotifications : React.FC = () => { |
||||||
|
return( |
||||||
|
<DropdownMenu> |
||||||
|
<DropdownMenuTrigger asChild> |
||||||
|
<Button |
||||||
|
variant="outline" |
||||||
|
className="relative h-8 w-8 rounded-full" |
||||||
|
> |
||||||
|
<BellIcon className="w-[1.2rem] h-[1.2rem]"/> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
|
||||||
|
<DropdownMenuContent className="w-80" align="end" forceMount> |
||||||
|
<DropdownMenuLabel className="font-normal"> |
||||||
|
<div className="flex flex-col space-y-1"> |
||||||
|
<p className="text-sm font-medium leading-none">Notifications</p> |
||||||
|
</div> |
||||||
|
</DropdownMenuLabel> |
||||||
|
<DropdownMenuSeparator /> |
||||||
|
<DropdownMenuGroup> |
||||||
|
<DropdownMenuItem className="hover:cursor-pointer" asChild> |
||||||
|
<p>Notification</p> |
||||||
|
</DropdownMenuItem> |
||||||
|
<DropdownMenuItem> |
||||||
|
<Link href="https://smarthr.dreamstechnologies.com/laravel/template/public/activity"> |
||||||
|
<div className="flex gap-2"> |
||||||
|
<div className="w-[35px] h-[35px] relative"> |
||||||
|
<Image src="https://smarthr.dreamstechnologies.com/laravel/template/public/build/img/profiles/avatar-27.jpg"
|
||||||
|
alt="Profile" fill className="aspect-square rounded-full"/> |
||||||
|
</div> |
||||||
|
<div className="flex-1"> |
||||||
|
<p className="mb-1"><span className="text-dark font-semibold">Shawn </span> |
||||||
|
performance in Math is below the threshold. |
||||||
|
</p> |
||||||
|
<span className="text-xs text-right block">Just Now</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
|
||||||
|
</DropdownMenuGroup> |
||||||
|
|
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
{/* <div class="d-flex flex-column"> |
||||||
|
<div class="border-bottom mb-3 pb-3"> |
||||||
|
<a href="https://smarthr.dreamstechnologies.com/laravel/template/public/activity"> |
||||||
|
<div class="d-flex"> |
||||||
|
<span class="avatar avatar-lg me-2 flex-shrink-0"> |
||||||
|
<img src="https://smarthr.dreamstechnologies.com/laravel/template/public/build/img/profiles/avatar-27.jpg" alt="Profile"> |
||||||
|
</span> |
||||||
|
<div class="flex-grow-1"> |
||||||
|
<p class="mb-1"><span class="text-dark fw-semibold">Shawn</span> |
||||||
|
performance in Math is below the threshold.</p> |
||||||
|
<span>Just Now</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="border-bottom mb-3 pb-3"> |
||||||
|
<a href="https://smarthr.dreamstechnologies.com/laravel/template/public/activity" class="pb-0"> |
||||||
|
<div class="d-flex"> |
||||||
|
<span class="avatar avatar-lg me-2 flex-shrink-0"> |
||||||
|
<img src="https://smarthr.dreamstechnologies.com/laravel/template/public/build/img/profiles/avatar-23.jpg" alt="Profile"> |
||||||
|
</span> |
||||||
|
<div class="flex-grow-1"> |
||||||
|
<p class="mb-1"><span class="text-dark fw-semibold">Sylvia</span> added |
||||||
|
appointment on 02:00 PM</p> |
||||||
|
<span>10 mins ago</span> |
||||||
|
<div class="d-flex justify-content-start align-items-center mt-1"> |
||||||
|
<span class="btn btn-light btn-sm me-2">Deny</span> |
||||||
|
<span class="btn btn-primary btn-sm">Approve</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="border-bottom mb-3 pb-3"> |
||||||
|
<a href="https://smarthr.dreamstechnologies.com/laravel/template/public/activity"> |
||||||
|
<div class="d-flex"> |
||||||
|
<span class="avatar avatar-lg me-2 flex-shrink-0"> |
||||||
|
<img src="https://smarthr.dreamstechnologies.com/laravel/template/public/build/img/profiles/avatar-25.jpg" alt="Profile"> |
||||||
|
</span> |
||||||
|
<div class="flex-grow-1"> |
||||||
|
<p class="mb-1">New student record <span class="text-dark fw-semibold"> George</span> is created by <span class="text-dark fw-semibold">Teressa</span></p> |
||||||
|
<span>2 hrs ago</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="border-0 mb-3 pb-0"> |
||||||
|
<a href="https://smarthr.dreamstechnologies.com/laravel/template/public/activity"> |
||||||
|
<div class="d-flex"> |
||||||
|
<span class="avatar avatar-lg me-2 flex-shrink-0"> |
||||||
|
<img src="https://smarthr.dreamstechnologies.com/laravel/template/public/build/img/profiles/avatar-01.jpg" alt="Profile"> |
||||||
|
</span> |
||||||
|
<div class="flex-grow-1"> |
||||||
|
<p class="mb-1">A new teacher record for <span class="text-dark fw-semibold">Elisa</span> </p> |
||||||
|
<span>09:45 AM</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> */} |
||||||
|
|
||||||
|
export default UserNotifications |
@ -0,0 +1,116 @@ |
|||||||
|
import { PaginationEllipsis, PaginationItem, PaginationLink } from "@/components/ui/pagination"; |
||||||
|
|
||||||
|
export const generatePaginationLinks = ( |
||||||
|
currentPage: number, |
||||||
|
totalPages: number, |
||||||
|
onPageChange: (page: number) => void |
||||||
|
) => { |
||||||
|
const pages: JSX.Element[] = []; |
||||||
|
|
||||||
|
// Show all pages if total pages is 6 or less
|
||||||
|
if (totalPages <= 6) { |
||||||
|
for (let i = 1; i <= totalPages; i++) { |
||||||
|
pages.push( |
||||||
|
<PaginationItem key={i}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(i)} |
||||||
|
isActive={i === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
{i} |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
} |
||||||
|
return pages; |
||||||
|
} |
||||||
|
|
||||||
|
// Always show first page
|
||||||
|
pages.push( |
||||||
|
<PaginationItem key={1}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(1)} |
||||||
|
isActive={1 === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
1 |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
|
||||||
|
// Calculate range for middle section
|
||||||
|
let startPage = Math.max(2, currentPage - 1); |
||||||
|
let endPage = Math.min(totalPages - 1, currentPage + 1); |
||||||
|
|
||||||
|
// Show ellipsis after first page if needed
|
||||||
|
if (startPage > 2) { |
||||||
|
if (startPage > 3) { |
||||||
|
pages.push(<PaginationEllipsis key="ellipsis-1" />); |
||||||
|
} |
||||||
|
// Show page 2 if we're not starting from it
|
||||||
|
if (startPage > 2) { |
||||||
|
pages.push( |
||||||
|
<PaginationItem key={2}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(2)} |
||||||
|
isActive={2 === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
2 |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Add middle pages
|
||||||
|
for (let i = startPage; i <= endPage; i++) { |
||||||
|
pages.push( |
||||||
|
<PaginationItem key={i}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(i)} |
||||||
|
isActive={i === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
{i} |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// Show ellipsis before last page if needed
|
||||||
|
if (endPage < totalPages - 1) { |
||||||
|
// Show second to last page if we're not ending on it
|
||||||
|
if (endPage < totalPages - 2) { |
||||||
|
pages.push( |
||||||
|
<PaginationItem key={totalPages - 1}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(totalPages - 1)} |
||||||
|
isActive={totalPages - 1 === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
{totalPages - 1} |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
} |
||||||
|
if (endPage < totalPages - 2) { |
||||||
|
pages.push(<PaginationEllipsis key="ellipsis-2" />); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
pages.push( |
||||||
|
<PaginationItem key={totalPages}> |
||||||
|
<PaginationLink |
||||||
|
onClick={() => onPageChange(totalPages)} |
||||||
|
isActive={totalPages === currentPage} |
||||||
|
className="cursor-pointer" |
||||||
|
> |
||||||
|
{totalPages} |
||||||
|
</PaginationLink> |
||||||
|
</PaginationItem> |
||||||
|
); |
||||||
|
|
||||||
|
return pages; |
||||||
|
}; |
@ -0,0 +1,121 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" |
||||||
|
|
||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button" |
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( |
||||||
|
<nav |
||||||
|
role="navigation" |
||||||
|
aria-label="pagination" |
||||||
|
className={cn("mx-auto flex w-full justify-center", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef< |
||||||
|
HTMLUListElement, |
||||||
|
React.ComponentProps<"ul"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ul |
||||||
|
ref={ref} |
||||||
|
className={cn("flex flex-row items-center gap-1", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
PaginationContent.displayName = "PaginationContent" |
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentProps<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li ref={ref} className={cn("hover:cursor-pointer", className)} {...props} /> |
||||||
|
)) |
||||||
|
PaginationItem.displayName = "PaginationItem" |
||||||
|
|
||||||
|
type PaginationLinkProps = { |
||||||
|
isActive?: boolean; |
||||||
|
disabled?: boolean; |
||||||
|
} & Pick<ButtonProps, "size"> & |
||||||
|
React.ComponentProps<"a"> |
||||||
|
|
||||||
|
const PaginationLink = ({ |
||||||
|
className, |
||||||
|
isActive, |
||||||
|
size = "icon", |
||||||
|
disabled = false, |
||||||
|
...props |
||||||
|
}: PaginationLinkProps) => ( |
||||||
|
<PaginationItem> |
||||||
|
<a |
||||||
|
aria-current={isActive ? "page" : undefined} |
||||||
|
role="link" |
||||||
|
aria-disabled={disabled} |
||||||
|
className={cn( |
||||||
|
buttonVariants({ |
||||||
|
variant: isActive ? "default" : "ghost", |
||||||
|
size, |
||||||
|
}), |
||||||
|
disabled && "cursor-not-allowed pointer-events-none", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</PaginationItem> |
||||||
|
) |
||||||
|
PaginationLink.displayName = "PaginationLink" |
||||||
|
|
||||||
|
const PaginationPrevious = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to previous page" |
||||||
|
size="default" |
||||||
|
className={cn("gap-1 pl-2.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronLeft className="h-4 w-4" /> |
||||||
|
<span>Previous</span> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
PaginationPrevious.displayName = "PaginationPrevious" |
||||||
|
|
||||||
|
const PaginationNext = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to next page" |
||||||
|
size="default" |
||||||
|
className={cn("gap-1 pr-2.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<span>Next</span> |
||||||
|
<ChevronRight className="h-4 w-4" /> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
|
||||||
|
const PaginationEllipsis = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"span">) => ( |
||||||
|
<span |
||||||
|
aria-hidden |
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
<span className="sr-only">More pages</span> |
||||||
|
</span> |
||||||
|
) |
||||||
|
|
||||||
|
export { |
||||||
|
Pagination, |
||||||
|
PaginationContent, |
||||||
|
PaginationEllipsis, |
||||||
|
PaginationItem, |
||||||
|
PaginationLink, |
||||||
|
PaginationNext, |
||||||
|
PaginationPrevious, |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"; |
||||||
|
import { generatePaginationLinks } from "./GeneratePages"; |
||||||
|
import { useRouter, useSearchParams } from "next/navigation"; |
||||||
|
|
||||||
|
type PaginatorProps = { |
||||||
|
currentPage: number; |
||||||
|
totalPages: number; |
||||||
|
showPreviousNext: boolean; |
||||||
|
mutate : () => void |
||||||
|
} |
||||||
|
|
||||||
|
export default function Paginator({ |
||||||
|
currentPage, |
||||||
|
totalPages, |
||||||
|
showPreviousNext, |
||||||
|
mutate |
||||||
|
}: PaginatorProps) { |
||||||
|
|
||||||
|
const searchParams = useSearchParams() |
||||||
|
const router = useRouter() |
||||||
|
|
||||||
|
const onPageChange = (page : number) : void => { |
||||||
|
const currentParams = new URLSearchParams(searchParams.toString()) |
||||||
|
currentParams.set( 'page' , page.toString()) |
||||||
|
router.replace(`?${currentParams.toString()}`) |
||||||
|
mutate() |
||||||
|
} |
||||||
|
return ( |
||||||
|
<div className="w-fit ml-auto mt-4"> |
||||||
|
<Pagination> |
||||||
|
<PaginationContent> |
||||||
|
{showPreviousNext && totalPages ? ( |
||||||
|
<PaginationItem> |
||||||
|
<PaginationPrevious |
||||||
|
onClick={() => onPageChange(currentPage - 1)} |
||||||
|
disabled={currentPage - 1 < 1} |
||||||
|
className="cursor-pointer" |
||||||
|
/> |
||||||
|
</PaginationItem> |
||||||
|
) : null} |
||||||
|
{generatePaginationLinks(currentPage, totalPages, onPageChange)} |
||||||
|
{showPreviousNext && totalPages ? ( |
||||||
|
<PaginationItem> |
||||||
|
<PaginationNext |
||||||
|
onClick={() => onPageChange(currentPage + 1)} |
||||||
|
disabled={currentPage > totalPages - 1} |
||||||
|
className="cursor-pointer" |
||||||
|
/> |
||||||
|
</PaginationItem> |
||||||
|
): null} |
||||||
|
</PaginationContent> |
||||||
|
</Pagination> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
"use client"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { SidebarContext } from "@/hooks/use-sidebar"; |
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import Image from "next/image"; |
||||||
|
import Link from "next/link"; |
||||||
|
import { useContext } from "react"; |
||||||
|
import { SidebarToggle } from "./_partials/sidebarToggle"; |
||||||
|
import { Menu } from "./_partials/menu"; |
||||||
|
|
||||||
|
export default function AppSideBar() { |
||||||
|
|
||||||
|
const sidebar = useContext(SidebarContext) |
||||||
|
if (!sidebar) return <>No Sidebar provider</>; |
||||||
|
const { isOpen, toggleOpen, getOpenState, setIsHover, settings } = sidebar; |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<aside |
||||||
|
className={cn( |
||||||
|
"fixed top-0 left-0 z-20 h-screen -translate-x-full lg:translate-x-0 transition-[width] ease-in-out duration-300", |
||||||
|
!getOpenState() ? "w-[90px]" : "w-72", |
||||||
|
settings.disabled && "hidden" |
||||||
|
)} |
||||||
|
> |
||||||
|
<SidebarToggle isOpen={isOpen} setIsOpen={toggleOpen} /> |
||||||
|
<div |
||||||
|
onMouseEnter={() => setIsHover(true)} |
||||||
|
onMouseLeave={() => setIsHover(false)} |
||||||
|
className="relative h-full flex flex-col px-3 py-4 overflow-y-auto shadow-md dark:shadow-zinc-800" |
||||||
|
> |
||||||
|
<Button |
||||||
|
className={cn( |
||||||
|
"transition-transform ease-in-out duration-300 mb-1", |
||||||
|
!getOpenState() ? "translate-x-1" : "translate-x-0" |
||||||
|
)} |
||||||
|
variant="link" |
||||||
|
asChild |
||||||
|
> |
||||||
|
</Button> |
||||||
|
<Link href="/dashboard" className="block py-1"> |
||||||
|
<Image src={'https://www.kumarijob.com/soft-assets/images/logo.svg'} width={150} height={300} alt="brand-logo" className="mx-auto block"/> |
||||||
|
</Link> |
||||||
|
<Menu isOpen={getOpenState()} /> |
||||||
|
</div> |
||||||
|
</aside> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,189 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import Link from "next/link"; |
||||||
|
import { useState } from "react"; |
||||||
|
import { ChevronDown, Dot, LucideIcon } from "lucide-react"; |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"; |
||||||
|
import { |
||||||
|
Collapsible, |
||||||
|
CollapsibleContent, |
||||||
|
CollapsibleTrigger |
||||||
|
} from "@/components/ui/collapsible"; |
||||||
|
import { |
||||||
|
Tooltip, |
||||||
|
TooltipTrigger, |
||||||
|
TooltipContent, |
||||||
|
TooltipProvider |
||||||
|
} from "@/components/ui/tooltip"; |
||||||
|
import { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuLabel, |
||||||
|
DropdownMenuTrigger, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuSeparator |
||||||
|
} from "@/components/ui/dropdown-menu"; |
||||||
|
import { usePathname } from "next/navigation"; |
||||||
|
|
||||||
|
type Submenu = { |
||||||
|
href: string; |
||||||
|
label: string; |
||||||
|
active?: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
interface CollapseMenuButtonProps { |
||||||
|
icon: LucideIcon; |
||||||
|
label: string; |
||||||
|
active: boolean; |
||||||
|
submenus: Submenu[]; |
||||||
|
isOpen: boolean | undefined; |
||||||
|
} |
||||||
|
|
||||||
|
export function CollapseMenuButton({ |
||||||
|
icon: Icon, |
||||||
|
label, |
||||||
|
submenus, |
||||||
|
isOpen |
||||||
|
}: CollapseMenuButtonProps) { |
||||||
|
const pathname = usePathname(); |
||||||
|
const isSubmenuActive = submenus.some((submenu) => |
||||||
|
submenu.active === undefined ? submenu.href === pathname : submenu.active |
||||||
|
); |
||||||
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(isSubmenuActive); |
||||||
|
|
||||||
|
return isOpen ? ( |
||||||
|
<Collapsible |
||||||
|
open={isCollapsed} |
||||||
|
onOpenChange={setIsCollapsed} |
||||||
|
className="w-full" |
||||||
|
> |
||||||
|
<CollapsibleTrigger |
||||||
|
className="[&[data-state=open]>div>div>svg]:rotate-180 mb-1" |
||||||
|
asChild |
||||||
|
> |
||||||
|
<Button |
||||||
|
variant={isSubmenuActive ? "secondary" : "ghost"} |
||||||
|
className="w-full justify-start h-10" |
||||||
|
> |
||||||
|
<div className="w-full items-center flex justify-between"> |
||||||
|
<div className="flex items-center"> |
||||||
|
<span className="mr-4"> |
||||||
|
<Icon size={18} /> |
||||||
|
</span> |
||||||
|
<p |
||||||
|
className={cn( |
||||||
|
"max-w-[150px] truncate", |
||||||
|
isOpen |
||||||
|
? "translate-x-0 opacity-100" |
||||||
|
: "-translate-x-96 opacity-0" |
||||||
|
)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"whitespace-nowrap", |
||||||
|
isOpen |
||||||
|
? "translate-x-0 opacity-100" |
||||||
|
: "-translate-x-96 opacity-0" |
||||||
|
)} |
||||||
|
> |
||||||
|
<ChevronDown |
||||||
|
size={18} |
||||||
|
className="transition-transform duration-200" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Button> |
||||||
|
</CollapsibleTrigger> |
||||||
|
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"> |
||||||
|
{submenus.map(({ href, label, active }, index) => ( |
||||||
|
<Button |
||||||
|
key={index} |
||||||
|
variant={ |
||||||
|
(active === undefined && pathname === href) || active |
||||||
|
? "secondary" |
||||||
|
: "ghost" |
||||||
|
} |
||||||
|
className="w-full justify-start h-10 mb-1" |
||||||
|
asChild |
||||||
|
> |
||||||
|
<Link href={href}> |
||||||
|
<span className="mr-4 ml-2"> |
||||||
|
<Dot size={18} /> |
||||||
|
</span> |
||||||
|
<p |
||||||
|
className={cn( |
||||||
|
"max-w-[170px] truncate", |
||||||
|
isOpen |
||||||
|
? "translate-x-0 opacity-100" |
||||||
|
: "-translate-x-96 opacity-0" |
||||||
|
)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
</Link> |
||||||
|
</Button> |
||||||
|
))} |
||||||
|
</CollapsibleContent> |
||||||
|
</Collapsible> |
||||||
|
) : ( |
||||||
|
<DropdownMenu> |
||||||
|
<TooltipProvider disableHoverableContent> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger asChild> |
||||||
|
<DropdownMenuTrigger asChild> |
||||||
|
<Button |
||||||
|
variant={isSubmenuActive ? "secondary" : "ghost"} |
||||||
|
className="w-full justify-start h-10 mb-1" |
||||||
|
> |
||||||
|
<div className="w-full items-center flex justify-between"> |
||||||
|
<div className="flex items-center"> |
||||||
|
<span className={cn(isOpen === false ? "" : "mr-4")}> |
||||||
|
<Icon size={18} /> |
||||||
|
</span> |
||||||
|
<p |
||||||
|
className={cn( |
||||||
|
"max-w-[200px] truncate", |
||||||
|
isOpen === false ? "opacity-0" : "opacity-100" |
||||||
|
)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Button> |
||||||
|
</DropdownMenuTrigger> |
||||||
|
</TooltipTrigger> |
||||||
|
<TooltipContent side="right" align="start" alignOffset={2}> |
||||||
|
{label} |
||||||
|
</TooltipContent> |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
<DropdownMenuContent side="right" sideOffset={25} align="start"> |
||||||
|
<DropdownMenuLabel className="max-w-[190px] truncate"> |
||||||
|
{label} |
||||||
|
</DropdownMenuLabel> |
||||||
|
<DropdownMenuSeparator /> |
||||||
|
{submenus.map(({ href, label, active }, index) => ( |
||||||
|
<DropdownMenuItem key={index} asChild> |
||||||
|
<Link |
||||||
|
className={`cursor-pointer ${ |
||||||
|
((active === undefined && pathname === href) || active) && |
||||||
|
"bg-secondary" |
||||||
|
}`}
|
||||||
|
href={href} |
||||||
|
> |
||||||
|
<p className="max-w-[180px] truncate">{label}</p> |
||||||
|
</Link> |
||||||
|
</DropdownMenuItem> |
||||||
|
))} |
||||||
|
<DropdownMenuArrow className="fill-border" /> |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,152 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import Link from "next/link"; |
||||||
|
import { Ellipsis, LogOut } from "lucide-react"; |
||||||
|
import { usePathname } from "next/navigation"; |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
|
||||||
|
import { |
||||||
|
Tooltip, |
||||||
|
TooltipTrigger, |
||||||
|
TooltipContent, |
||||||
|
TooltipProvider |
||||||
|
} from "@/components/ui/tooltip"; |
||||||
|
import { useContext } from "react"; |
||||||
|
import { AuthContext } from "@/helpers/context/AuthProvider"; |
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"; |
||||||
|
import { CollapseMenuButton } from "./collapseMenuButton"; |
||||||
|
import { getMenuList } from "@/utils/menu-list"; |
||||||
|
|
||||||
|
interface MenuProps { |
||||||
|
isOpen: boolean | undefined; |
||||||
|
} |
||||||
|
|
||||||
|
export function Menu({ isOpen }: MenuProps) { |
||||||
|
|
||||||
|
const pathname = usePathname(); |
||||||
|
const menuList = getMenuList(); |
||||||
|
const { logout } = useContext(AuthContext) |
||||||
|
|
||||||
|
return ( |
||||||
|
<ScrollArea className="[&>div>div[style]]:!block"> |
||||||
|
<nav className="mt-8 h-full w-full"> |
||||||
|
<ul className="flex flex-col min-h-[calc(100vh-48px-36px-16px-32px)] lg:min-h-[calc(100vh-32px-60px-32px)] items-start space-y-1 px-2"> |
||||||
|
{menuList.map(({ groupLabel, menus }, index) => ( |
||||||
|
<li className={cn("w-full", groupLabel ? "pt-3" : "")} key={index}> |
||||||
|
{(isOpen && groupLabel) || isOpen === undefined ? ( |
||||||
|
<p className="text-sm font-medium text-muted-foreground px-4 pb-2 max-w-[248px] truncate"> |
||||||
|
{groupLabel} |
||||||
|
</p> |
||||||
|
) : !isOpen && isOpen !== undefined && groupLabel ? ( |
||||||
|
<TooltipProvider> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger className="w-full"> |
||||||
|
<div className="w-full flex justify-center items-center"> |
||||||
|
<Ellipsis className="h-5 w-5" /> |
||||||
|
</div> |
||||||
|
</TooltipTrigger> |
||||||
|
<TooltipContent side="right"> |
||||||
|
<p>{groupLabel}</p> |
||||||
|
</TooltipContent> |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
) : null} |
||||||
|
{menus.map( |
||||||
|
({ href, label, icon: Icon, active, submenus }, index) => |
||||||
|
!submenus || submenus.length === 0 ? ( |
||||||
|
<div className="w-full" key={index}> |
||||||
|
<TooltipProvider disableHoverableContent> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger asChild> |
||||||
|
<Button |
||||||
|
variant={ |
||||||
|
(active === undefined && |
||||||
|
pathname.startsWith(href)) || |
||||||
|
active |
||||||
|
? "secondary" |
||||||
|
: "ghost" |
||||||
|
} |
||||||
|
className="w-full justify-start h-10 mb-1" |
||||||
|
asChild |
||||||
|
> |
||||||
|
<Link href={href}> |
||||||
|
<span |
||||||
|
className={cn(isOpen === false ? "" : "mr-4")} |
||||||
|
> |
||||||
|
<Icon size={18} /> |
||||||
|
</span> |
||||||
|
<p |
||||||
|
className={cn( |
||||||
|
"max-w-[200px] truncate", |
||||||
|
isOpen === false |
||||||
|
? "-translate-x-96 opacity-0" |
||||||
|
: "translate-x-0 opacity-100" |
||||||
|
)} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</p> |
||||||
|
</Link> |
||||||
|
</Button> |
||||||
|
</TooltipTrigger> |
||||||
|
{isOpen === false && ( |
||||||
|
<TooltipContent side="right"> |
||||||
|
{label} |
||||||
|
</TooltipContent> |
||||||
|
)} |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
</div> |
||||||
|
) : ( |
||||||
|
<div className="w-full" key={index}> |
||||||
|
{/* working */} |
||||||
|
<CollapseMenuButton |
||||||
|
icon={Icon} |
||||||
|
label={label} |
||||||
|
active={ |
||||||
|
active === undefined |
||||||
|
? pathname.startsWith(href) |
||||||
|
: active |
||||||
|
} |
||||||
|
submenus={submenus} |
||||||
|
isOpen={isOpen} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
)} |
||||||
|
</li> |
||||||
|
))} |
||||||
|
<li className="w-full grow flex items-end"> |
||||||
|
<TooltipProvider disableHoverableContent> |
||||||
|
<Tooltip delayDuration={100}> |
||||||
|
<TooltipTrigger asChild> |
||||||
|
<Button |
||||||
|
onClick={() => {logout()}} |
||||||
|
variant="outline" |
||||||
|
className="w-full justify-center h-10 mt-5 text-red-500 hover:text-red-700" |
||||||
|
> |
||||||
|
<span className={cn(isOpen === false ? "" : "mr-2")}> |
||||||
|
<LogOut size={18} className=""/> |
||||||
|
</span> |
||||||
|
<p |
||||||
|
className={cn( |
||||||
|
"whitespace-nowrap", |
||||||
|
isOpen === false ? "opacity-0 hidden" : "opacity-100" |
||||||
|
)} |
||||||
|
> |
||||||
|
Sign out |
||||||
|
</p> |
||||||
|
</Button> |
||||||
|
</TooltipTrigger> |
||||||
|
{isOpen === false && ( |
||||||
|
<TooltipContent side="right" onClick={() => logout()}>Sign out</TooltipContent> |
||||||
|
)} |
||||||
|
</Tooltip> |
||||||
|
</TooltipProvider> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</nav> |
||||||
|
</ScrollArea> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import { ChevronLeft } from "lucide-react"; |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
|
||||||
|
interface SidebarToggleProps { |
||||||
|
isOpen: boolean | undefined; |
||||||
|
setIsOpen?: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) { |
||||||
|
return ( |
||||||
|
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20"> |
||||||
|
<Button |
||||||
|
onClick={() => setIsOpen?.()} |
||||||
|
className="rounded-md w-8 h-8" |
||||||
|
variant="outline" |
||||||
|
size="icon" |
||||||
|
> |
||||||
|
<ChevronLeft |
||||||
|
className={cn( |
||||||
|
"h-4 w-4 transition-transform ease-in-out duration-700", |
||||||
|
isOpen === false ? "rotate-180" : "rotate-0" |
||||||
|
)} |
||||||
|
/> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
const AdminCommonContainer: React.FC<{children : React.ReactNode , className? :string , title? : string}> = ({ |
||||||
|
children ,
|
||||||
|
className ,
|
||||||
|
title |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<title>{title ?? 'HRMS'}</title> |
||||||
|
<div className={`container py-4 px-4 sm:px-8 mx-auto ${className}`}>{children}</div> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default AdminCommonContainer |
@ -0,0 +1,17 @@ |
|||||||
|
const AdminContentContainer : React.FC<{ |
||||||
|
children : React.ReactNode ,
|
||||||
|
className? : string |
||||||
|
}> = ({ |
||||||
|
children ,
|
||||||
|
className |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<div className={`border bg-card text-card-foreground shadow rounded-lg border-none mt-6 min-h-[50vh] p-6 mb-16 ${className}`}> |
||||||
|
{ |
||||||
|
children |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default AdminContentContainer |
@ -0,0 +1,125 @@ |
|||||||
|
import React, { useState, useRef, ChangeEvent } from 'react'; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { |
||||||
|
Dialog, |
||||||
|
DialogContent, |
||||||
|
DialogHeader, |
||||||
|
DialogTitle, |
||||||
|
DialogTrigger, |
||||||
|
} from "@/components/ui/dialog"; |
||||||
|
import { Input } from "@/components/ui/input"; |
||||||
|
import { Label } from "@/components/ui/label"; |
||||||
|
import { Upload, Loader2 } from "lucide-react"; |
||||||
|
import { fetchHeader } from '@/helpers/fetch.helper'; |
||||||
|
import { useToast } from '@/hooks/use-toast'; |
||||||
|
|
||||||
|
const BulkUpload = ({ |
||||||
|
submitUrl ,
|
||||||
|
mutate |
||||||
|
} : { |
||||||
|
submitUrl : string |
||||||
|
mutate : () => void |
||||||
|
}) => { |
||||||
|
const { toast } = useToast() |
||||||
|
const [isLoading, setIsLoading] = useState(false); |
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null); |
||||||
|
const [open, setOpen] = useState(false); |
||||||
|
const fileInputRef = useRef<any>(null); |
||||||
|
|
||||||
|
const handleFileChange = (event : ChangeEvent<HTMLInputElement>) => { |
||||||
|
const file = event.target.files?.[0]; |
||||||
|
if (file) { |
||||||
|
setSelectedFile(file); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleSubmit = async () => { |
||||||
|
if (!selectedFile) return; |
||||||
|
|
||||||
|
try { |
||||||
|
setIsLoading(true); |
||||||
|
|
||||||
|
const formData = new FormData(); |
||||||
|
formData.append('file', selectedFile); |
||||||
|
|
||||||
|
const response = await fetch(submitUrl, { |
||||||
|
method: 'POST', |
||||||
|
body: formData, |
||||||
|
headers : fetchHeader() |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error('Upload failed'); |
||||||
|
} |
||||||
|
const data = await response.json() |
||||||
|
|
||||||
|
// Reset and close on success
|
||||||
|
setSelectedFile(null); |
||||||
|
setOpen(false); |
||||||
|
if (fileInputRef.current) { |
||||||
|
fileInputRef.current.value = ''; |
||||||
|
} |
||||||
|
toast({ |
||||||
|
title : 'Sucessfully Uploaded !', |
||||||
|
description : data?.message, |
||||||
|
variant : 'success' |
||||||
|
}) |
||||||
|
|
||||||
|
// You might want to trigger a refresh of the employee list here
|
||||||
|
|
||||||
|
} catch (error : any) { |
||||||
|
toast({ |
||||||
|
title : 'Upload failed', |
||||||
|
description : error?.message, |
||||||
|
variant : 'destructive' |
||||||
|
}) |
||||||
|
// You might want to show an error message to the user here
|
||||||
|
} finally { |
||||||
|
setIsLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Dialog open={open} onOpenChange={setOpen}> |
||||||
|
<DialogTrigger asChild> |
||||||
|
<Button variant={'outline'} className='justify-center'> |
||||||
|
<Upload className="mr-2 h-4 w-4" /> |
||||||
|
Import Employees |
||||||
|
</Button> |
||||||
|
</DialogTrigger> |
||||||
|
<DialogContent> |
||||||
|
<DialogHeader> |
||||||
|
<DialogTitle>Upload Employee Data</DialogTitle> |
||||||
|
</DialogHeader> |
||||||
|
<div className="grid gap-4 py-4"> |
||||||
|
<div className="space-y-2"> |
||||||
|
<Label htmlFor="file">Select File</Label> |
||||||
|
|
||||||
|
<Input |
||||||
|
id="file" |
||||||
|
type="file" |
||||||
|
accept=".csv,.xlsx,.xls" |
||||||
|
onChange={handleFileChange} |
||||||
|
ref={fileInputRef} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
{selectedFile && ( |
||||||
|
<p className="text-sm text-muted-foreground"> |
||||||
|
Selected: {selectedFile.name} |
||||||
|
</p> |
||||||
|
)} |
||||||
|
<Button
|
||||||
|
onClick={handleSubmit} |
||||||
|
disabled={!selectedFile || isLoading} |
||||||
|
className="w-full" |
||||||
|
> |
||||||
|
Upload |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</DialogContent> |
||||||
|
</Dialog> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default BulkUpload; |
@ -0,0 +1,14 @@ |
|||||||
|
const CommonContainer: React.FC<{children : React.ReactNode , className? :string , title? : string}> = ({ |
||||||
|
children ,
|
||||||
|
className ,
|
||||||
|
title |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<> |
||||||
|
<title>{title ?? 'HRMS'}</title> |
||||||
|
<div className={`container py-4 px-4 sm:px-8 mx-auto ${className}`}>{children}</div> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default CommonContainer |
@ -0,0 +1,17 @@ |
|||||||
|
const ContentContainer : React.FC<{ |
||||||
|
children : React.ReactNode ,
|
||||||
|
className? : string |
||||||
|
}> = ({ |
||||||
|
children ,
|
||||||
|
className |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<div className={`border bg-card text-card-foreground shadow rounded-lg border-none mt-6 min-h-[50vh] p-6 mb-16 ${className}`}> |
||||||
|
{ |
||||||
|
children |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default ContentContainer |
@ -0,0 +1,86 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { Button } from '../ui/button'; |
||||||
|
import { DownloadIcon } from 'lucide-react'; |
||||||
|
import { getEduConnectAccessToken } from '@/helpers/token.helper'; |
||||||
|
|
||||||
|
interface DownloadProps { |
||||||
|
url: string; |
||||||
|
filename?: string; |
||||||
|
className?: string; |
||||||
|
label?: string; |
||||||
|
onSuccess?: () => void; |
||||||
|
onError?: (error: Error) => void; |
||||||
|
headers?: Record<string, string>; |
||||||
|
} |
||||||
|
|
||||||
|
const Download = ({ |
||||||
|
url, |
||||||
|
filename = 'download', |
||||||
|
className = '', |
||||||
|
label = 'Download', |
||||||
|
onSuccess, |
||||||
|
onError, |
||||||
|
headers = {}, |
||||||
|
}: DownloadProps) => { |
||||||
|
const [isLoading, setIsLoading] = useState(false); |
||||||
|
|
||||||
|
const handleDownload = async () => { |
||||||
|
setIsLoading(true); |
||||||
|
try { |
||||||
|
const response = await fetch(url, { |
||||||
|
method: 'GET', |
||||||
|
headers: { |
||||||
|
Authorization : `Bearer ${getEduConnectAccessToken()}`, |
||||||
|
Accept : 'Application/json' |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`HTTP error! status: ${response.status}`); |
||||||
|
} |
||||||
|
|
||||||
|
// Get filename from Content-Disposition header if available
|
||||||
|
const contentDisposition = response.headers.get('content-disposition'); |
||||||
|
const serverFilename = contentDisposition |
||||||
|
?.split(';') |
||||||
|
?.find(n => n.includes('filename=')) |
||||||
|
?.replace('filename=', '') |
||||||
|
?.trim(); |
||||||
|
|
||||||
|
const blob = await response.blob(); |
||||||
|
const downloadUrl = window.URL.createObjectURL(blob); |
||||||
|
const link = document.createElement('a'); |
||||||
|
link.href = downloadUrl; |
||||||
|
link.download = serverFilename || filename; |
||||||
|
|
||||||
|
// Append to document, click, and cleanup
|
||||||
|
document.body.appendChild(link); |
||||||
|
link.click(); |
||||||
|
document.body.removeChild(link); |
||||||
|
|
||||||
|
// Cleanup the URL object
|
||||||
|
window.URL.revokeObjectURL(downloadUrl); |
||||||
|
|
||||||
|
onSuccess?.(); |
||||||
|
} catch (error) { |
||||||
|
console.error('Download failed:', error); |
||||||
|
onError?.(error as Error); |
||||||
|
} finally { |
||||||
|
setIsLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Button |
||||||
|
onClick={handleDownload} |
||||||
|
disabled={isLoading} |
||||||
|
variant={'outline'} |
||||||
|
className={`${className} w-full`} |
||||||
|
> |
||||||
|
<DownloadIcon className="mr-2 h-4 w-4" /> |
||||||
|
{label} |
||||||
|
</Button> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Download; |
@ -0,0 +1,72 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { Building2, Briefcase, Calendar, Edit, Trash2 } from 'lucide-react'; |
||||||
|
// import { EmployeeExperienceAPIInterface } from '@/helpers/apiSchema/employeeExperience.schema';
|
||||||
|
|
||||||
|
interface EmployeeExperienceListProps { |
||||||
|
experiences: any[]; |
||||||
|
onEdit: (experience: any) => void; |
||||||
|
onDelete: (experienceId: number) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const EmployeeExperienceList: React.FC<EmployeeExperienceListProps> = ({
|
||||||
|
experiences,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<ul className="employee-experience__list space-y-4"> |
||||||
|
{experiences?.map((experience) => ( |
||||||
|
<li key={experience.id} className="relative"> |
||||||
|
<div className="bg-white shadow-md rounded-lg p-4 border border-gray-200 hover:shadow-lg transition-shadow"> |
||||||
|
<div className="flex items-center mb-2"> |
||||||
|
<Building2 className="mr-3 text-blue-600" size={24} /> |
||||||
|
<h3 className="text-lg font-semibold text-gray-900"> |
||||||
|
{experience.company} |
||||||
|
</h3> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex items-center text-foreground"> |
||||||
|
<Briefcase className="mr-3" size={16} /> |
||||||
|
<span className="text-sm">{experience.designation}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex items-center text-gray-600"> |
||||||
|
<Calendar className="mr-3 text-gray-500" size={16} /> |
||||||
|
<span className="text-foreground text-sm"> |
||||||
|
{new Date(experience.from_date).toLocaleDateString()} -
|
||||||
|
{new Date(experience.to_date).toLocaleDateString()} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="mt-4 text-sm text-gray-500"> |
||||||
|
<span className="mr-2 bg-blue-100 text-blue-800 px-2 py-1 rounded"> |
||||||
|
{experience.industry} |
||||||
|
</span> |
||||||
|
<span className="bg-green-100 text-green-800 px-2 py-1 rounded"> |
||||||
|
Level {experience.job_level} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Action Buttons */} |
||||||
|
<div className="absolute top-4 right-4 flex space-x-2"> |
||||||
|
<button
|
||||||
|
onClick={() => onEdit(experience)} |
||||||
|
className="text-yellow-500 hover:text-yellow-700" |
||||||
|
title="Edit Experience" |
||||||
|
> |
||||||
|
<Edit size={20} /> |
||||||
|
</button> |
||||||
|
<button
|
||||||
|
onClick={() => onDelete(experience.id)} |
||||||
|
className="text-red-500 hover:text-red-700" |
||||||
|
title="Delete Experience" |
||||||
|
> |
||||||
|
<Trash2 size={20} /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,11 @@ |
|||||||
|
const FormSection = ({ children , title } : {children : React.ReactNode , title : string}) => ( |
||||||
|
<div className="space-y-4 w-full"> |
||||||
|
<h3 className="text-lg font-semibold">{title}</h3> |
||||||
|
<div className="space-y-4"> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
|
||||||
|
export default FormSection |
||||||
|
|
@ -0,0 +1,9 @@ |
|||||||
|
import { Loader2 } from "lucide-react"; |
||||||
|
|
||||||
|
const Loader: React.FC = () => { |
||||||
|
return ( |
||||||
|
<Loader2 /> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Loader |
@ -0,0 +1,32 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { Skeleton } from '../ui/skeleton' |
||||||
|
import { |
||||||
|
TableRow, |
||||||
|
TableCell, |
||||||
|
} from "@/components/ui/table" |
||||||
|
|
||||||
|
interface TableSkeletonProps { |
||||||
|
columns?: number |
||||||
|
rows?: number |
||||||
|
} |
||||||
|
|
||||||
|
export function TableSkeleton({
|
||||||
|
columns = 5, |
||||||
|
rows = 5 |
||||||
|
}: TableSkeletonProps) { |
||||||
|
return ( |
||||||
|
<> |
||||||
|
{Array.from({ length: rows }).map((_, rowIndex) => ( |
||||||
|
<TableRow key={rowIndex} className="hover:bg-transparent"> |
||||||
|
{Array.from({ length: columns }).map((_, colIndex) => ( |
||||||
|
<TableCell key={colIndex} className="p-4"> |
||||||
|
<Skeleton className="h-4 w-[80%] bg-slate-200" /> |
||||||
|
</TableCell> |
||||||
|
))} |
||||||
|
</TableRow> |
||||||
|
))} |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default TableSkeleton; |
@ -0,0 +1,57 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion" |
||||||
|
import { ChevronDown } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root |
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AccordionPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn("border-b rounded-lg", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AccordionItem.displayName = "AccordionItem" |
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef< |
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<AccordionPrimitive.Header className="flex"> |
||||||
|
<AccordionPrimitive.Trigger |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" /> |
||||||
|
</AccordionPrimitive.Trigger> |
||||||
|
</AccordionPrimitive.Header> |
||||||
|
)) |
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName |
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<AccordionPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div> |
||||||
|
</AccordionPrimitive.Content> |
||||||
|
)) |
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName |
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } |
@ -0,0 +1,141 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { buttonVariants } from "@/components/ui/button" |
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root |
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger |
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal |
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Overlay |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPortal> |
||||||
|
<AlertDialogOverlay /> |
||||||
|
<AlertDialogPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</AlertDialogPortal> |
||||||
|
)) |
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName |
||||||
|
|
||||||
|
const AlertDialogHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-2 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader" |
||||||
|
|
||||||
|
const AlertDialogFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter" |
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-lg font-semibold", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName |
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogDescription.displayName = |
||||||
|
AlertDialogPrimitive.Description.displayName |
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Action |
||||||
|
ref={ref} |
||||||
|
className={cn(buttonVariants(), className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName |
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Cancel |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
buttonVariants({ variant: "outline" }), |
||||||
|
"mt-2 sm:mt-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
AlertDialog, |
||||||
|
AlertDialogPortal, |
||||||
|
AlertDialogOverlay, |
||||||
|
AlertDialogTrigger, |
||||||
|
AlertDialogContent, |
||||||
|
AlertDialogHeader, |
||||||
|
AlertDialogFooter, |
||||||
|
AlertDialogTitle, |
||||||
|
AlertDialogDescription, |
||||||
|
AlertDialogAction, |
||||||
|
AlertDialogCancel, |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar" |
||||||
|
import { Dialog, DialogContent } from "@/components/ui/dialog" |
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
const Avatar = React.forwardRef< |
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AvatarPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full cursor-pointer", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName |
||||||
|
|
||||||
|
interface EnhancedAvatarImageProps extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> { |
||||||
|
viewerClassName?: string |
||||||
|
} |
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef< |
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>, |
||||||
|
EnhancedAvatarImageProps |
||||||
|
>(({ className, viewerClassName, src, alt, ...props }, ref) => { |
||||||
|
const [isOpen, setIsOpen] = React.useState(false) |
||||||
|
|
||||||
|
console.log(isOpen) |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<AvatarPrimitive.Image |
||||||
|
ref={ref} |
||||||
|
src={src} |
||||||
|
alt={alt} |
||||||
|
className={cn("aspect-square h-full w-full", className)} |
||||||
|
onClick={() => setIsOpen(true)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}> |
||||||
|
<DialogContent className="max-w-[90vw] max-h-[90vh] p-0 bg-transparent border-none" hideCloseButton> |
||||||
|
<div className="relative w-full h-full flex items-center justify-center rounded-lg overflow-hidden"> |
||||||
|
{/* Blurred background using the same image */} |
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat blur-xl opacity-90 scale-110" |
||||||
|
style={{ backgroundImage: `url(${src})` }} |
||||||
|
/> |
||||||
|
{/* Semi-transparent overlay to improve contrast */} |
||||||
|
<div className="absolute inset-0 bg-white/10 backdrop-blur-xl" /> |
||||||
|
|
||||||
|
<button |
||||||
|
onClick={() => setIsOpen(false)} |
||||||
|
className="absolute top-4 right-4 text-white hover:text-gray-300 focus:outline-none z-10" |
||||||
|
> |
||||||
|
<X className="h-6 w-6" /> |
||||||
|
</button> |
||||||
|
|
||||||
|
<img |
||||||
|
src={src} |
||||||
|
alt={alt} |
||||||
|
className={cn( |
||||||
|
"relative z-10 max-w-full max-h-[80vh] object-contain", |
||||||
|
viewerClassName |
||||||
|
)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</DialogContent> |
||||||
|
</Dialog> |
||||||
|
</> |
||||||
|
) |
||||||
|
}) |
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName |
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef< |
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AvatarPrimitive.Fallback |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName |
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback } |
@ -0,0 +1,38 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const badgeVariants = cva( |
||||||
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs cursor-pointer font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: |
||||||
|
"border-transparent bg-primary text-primary-foreground shadow hover:opacity-[0.95]", |
||||||
|
secondary: |
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:opacity-[0.95]", |
||||||
|
destructive: |
||||||
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:opacity-[0.95]", |
||||||
|
success :
|
||||||
|
"border-green-300 bg-green-500 text-destructive-foreground shadow hover:opacity-[0.95]", |
||||||
|
outline: "text-foreground", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
export interface BadgeProps |
||||||
|
extends React.HTMLAttributes<HTMLDivElement>, |
||||||
|
VariantProps<typeof badgeVariants> {} |
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) { |
||||||
|
return ( |
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} /> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { Badge, badgeVariants } |
@ -0,0 +1,115 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { Slot } from "@radix-ui/react-slot" |
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef< |
||||||
|
HTMLElement, |
||||||
|
React.ComponentPropsWithoutRef<"nav"> & { |
||||||
|
separator?: React.ReactNode |
||||||
|
} |
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) |
||||||
|
Breadcrumb.displayName = "Breadcrumb" |
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef< |
||||||
|
HTMLOListElement, |
||||||
|
React.ComponentPropsWithoutRef<"ol"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ol |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbList.displayName = "BreadcrumbList" |
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentPropsWithoutRef<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li |
||||||
|
ref={ref} |
||||||
|
className={cn("inline-flex items-center gap-1.5", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem" |
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef< |
||||||
|
HTMLAnchorElement, |
||||||
|
React.ComponentPropsWithoutRef<"a"> & { |
||||||
|
asChild?: boolean |
||||||
|
} |
||||||
|
>(({ asChild, className, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "a" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
className={cn("transition-colors hover:text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink" |
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef< |
||||||
|
HTMLSpanElement, |
||||||
|
React.ComponentPropsWithoutRef<"span"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<span |
||||||
|
ref={ref} |
||||||
|
role="link" |
||||||
|
aria-disabled="true" |
||||||
|
aria-current="page" |
||||||
|
className={cn("font-normal text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage" |
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({ |
||||||
|
children, |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"li">) => ( |
||||||
|
<li |
||||||
|
role="presentation" |
||||||
|
aria-hidden="true" |
||||||
|
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children ?? <ChevronRight />} |
||||||
|
</li> |
||||||
|
) |
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" |
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"span">) => ( |
||||||
|
<span |
||||||
|
role="presentation" |
||||||
|
aria-hidden="true" |
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
<span className="sr-only">More</span> |
||||||
|
</span> |
||||||
|
) |
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" |
||||||
|
|
||||||
|
export { |
||||||
|
Breadcrumb, |
||||||
|
BreadcrumbList, |
||||||
|
BreadcrumbItem, |
||||||
|
BreadcrumbLink, |
||||||
|
BreadcrumbPage, |
||||||
|
BreadcrumbSeparator, |
||||||
|
BreadcrumbEllipsis, |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { Slot } from "@radix-ui/react-slot" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
import { Loader, Loader2 } from "lucide-react" |
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const buttonVariants = cva( |
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: |
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90", |
||||||
|
destructive: |
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", |
||||||
|
outline: |
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", |
||||||
|
secondary: |
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", |
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground", |
||||||
|
link: "text-primary underline-offset-4 hover:underline", |
||||||
|
}, |
||||||
|
size: { |
||||||
|
default: "h-9 px-4 py-2", |
||||||
|
sm: "h-8 rounded-md px-3 text-xs", |
||||||
|
lg: "h-10 rounded-md px-8", |
||||||
|
icon: "h-9 w-9", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
size: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
export interface ButtonProps |
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>, |
||||||
|
VariantProps<typeof buttonVariants> { |
||||||
|
asChild?: boolean |
||||||
|
isLoading?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( |
||||||
|
({ className, variant, size, isLoading = false, asChild = false, children, disabled, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "button" |
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
className={cn(buttonVariants({ variant, size, className }))} |
||||||
|
ref={ref} |
||||||
|
disabled={isLoading || disabled} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{isLoading ? ( |
||||||
|
<> |
||||||
|
<Loader className="animate-spin" /> |
||||||
|
{children} |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
children |
||||||
|
)} |
||||||
|
</Comp> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
Button.displayName = "Button" |
||||||
|
|
||||||
|
export { Button, buttonVariants } |
@ -0,0 +1,76 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react" |
||||||
|
import { DayPicker } from "react-day-picker" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { buttonVariants } from "@/components/ui/button" |
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker> |
||||||
|
|
||||||
|
function Calendar({ |
||||||
|
className, |
||||||
|
classNames, |
||||||
|
showOutsideDays = true, |
||||||
|
...props |
||||||
|
}: CalendarProps) { |
||||||
|
return ( |
||||||
|
<DayPicker |
||||||
|
showOutsideDays={showOutsideDays} |
||||||
|
className={cn("p-3", className)} |
||||||
|
classNames={{ |
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", |
||||||
|
month: "space-y-4", |
||||||
|
caption: "flex justify-center pt-1 relative items-center", |
||||||
|
caption_label: "text-sm font-medium", |
||||||
|
nav: "space-x-1 flex items-center", |
||||||
|
nav_button: cn( |
||||||
|
buttonVariants({ variant: "outline" }), |
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" |
||||||
|
), |
||||||
|
nav_button_previous: "absolute left-1", |
||||||
|
nav_button_next: "absolute right-1", |
||||||
|
table: "w-full border-collapse space-y-1", |
||||||
|
head_row: "flex", |
||||||
|
head_cell: |
||||||
|
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", |
||||||
|
row: "flex w-full mt-2", |
||||||
|
cell: cn( |
||||||
|
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", |
||||||
|
props.mode === "range" |
||||||
|
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" |
||||||
|
: "[&:has([aria-selected])]:rounded-md" |
||||||
|
), |
||||||
|
day: cn( |
||||||
|
buttonVariants({ variant: "ghost" }), |
||||||
|
"h-8 w-8 p-0 font-normal aria-selected:opacity-100" |
||||||
|
), |
||||||
|
day_range_start: "day-range-start", |
||||||
|
day_range_end: "day-range-end", |
||||||
|
day_selected: |
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", |
||||||
|
day_today: "bg-accent text-accent-foreground", |
||||||
|
day_outside: |
||||||
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", |
||||||
|
day_disabled: "text-muted-foreground opacity-50", |
||||||
|
day_range_middle: |
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground", |
||||||
|
day_hidden: "invisible", |
||||||
|
...classNames, |
||||||
|
}} |
||||||
|
components={{ |
||||||
|
IconLeft: ({ className, ...props }) => ( |
||||||
|
<ChevronLeft className={cn("h-4 w-4", className)} {...props} /> |
||||||
|
), |
||||||
|
IconRight: ({ className, ...props }) => ( |
||||||
|
<ChevronRight className={cn("h-4 w-4", className)} {...props} /> |
||||||
|
), |
||||||
|
}} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
Calendar.displayName = "Calendar" |
||||||
|
|
||||||
|
export { Calendar } |
@ -0,0 +1,76 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Card = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"rounded-xl border bg-card text-card-foreground shadow", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
Card.displayName = "Card" |
||||||
|
|
||||||
|
const CardHeader = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
CardHeader.displayName = "CardHeader" |
||||||
|
|
||||||
|
const CardTitle = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn("font-semibold leading-none tracking-tight", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
CardTitle.displayName = "CardTitle" |
||||||
|
|
||||||
|
const CardDescription = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
CardDescription.displayName = "CardDescription" |
||||||
|
|
||||||
|
const CardContent = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> |
||||||
|
)) |
||||||
|
CardContent.displayName = "CardContent" |
||||||
|
|
||||||
|
const CardFooter = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn("flex items-center p-6 pt-0", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
CardFooter.displayName = "CardFooter" |
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } |
@ -0,0 +1,365 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as RechartsPrimitive from "recharts" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const |
||||||
|
|
||||||
|
export type ChartConfig = { |
||||||
|
[k in string]: { |
||||||
|
label?: React.ReactNode |
||||||
|
icon?: React.ComponentType |
||||||
|
} & ( |
||||||
|
| { color?: string; theme?: never } |
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> } |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
type ChartContextProps = { |
||||||
|
config: ChartConfig |
||||||
|
} |
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null) |
||||||
|
|
||||||
|
function useChart() { |
||||||
|
const context = React.useContext(ChartContext) |
||||||
|
|
||||||
|
if (!context) { |
||||||
|
throw new Error("useChart must be used within a <ChartContainer />") |
||||||
|
} |
||||||
|
|
||||||
|
return context |
||||||
|
} |
||||||
|
|
||||||
|
const ChartContainer = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & { |
||||||
|
config: ChartConfig |
||||||
|
children: React.ComponentProps< |
||||||
|
typeof RechartsPrimitive.ResponsiveContainer |
||||||
|
>["children"] |
||||||
|
} |
||||||
|
>(({ id, className, children, config, ...props }, ref) => { |
||||||
|
const uniqueId = React.useId() |
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` |
||||||
|
|
||||||
|
return ( |
||||||
|
<ChartContext.Provider value={{ config }}> |
||||||
|
<div |
||||||
|
data-chart={chartId} |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.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 [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChartStyle id={chartId} config={config} /> |
||||||
|
<RechartsPrimitive.ResponsiveContainer> |
||||||
|
{children} |
||||||
|
</RechartsPrimitive.ResponsiveContainer> |
||||||
|
</div> |
||||||
|
</ChartContext.Provider> |
||||||
|
) |
||||||
|
}) |
||||||
|
ChartContainer.displayName = "Chart" |
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { |
||||||
|
const colorConfig = Object.entries(config).filter( |
||||||
|
([, config]) => config.theme || config.color |
||||||
|
) |
||||||
|
|
||||||
|
if (!colorConfig.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<style |
||||||
|
dangerouslySetInnerHTML={{ |
||||||
|
__html: Object.entries(THEMES) |
||||||
|
.map( |
||||||
|
([theme, prefix]) => ` |
||||||
|
${prefix} [data-chart=${id}] { |
||||||
|
${colorConfig |
||||||
|
.map(([key, itemConfig]) => { |
||||||
|
const color = |
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || |
||||||
|
itemConfig.color |
||||||
|
return color ? ` --color-${key}: ${color};` : null |
||||||
|
}) |
||||||
|
.join("\n")} |
||||||
|
} |
||||||
|
` |
||||||
|
) |
||||||
|
.join("\n"), |
||||||
|
}} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip |
||||||
|
|
||||||
|
const ChartTooltipContent = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & |
||||||
|
React.ComponentProps<"div"> & { |
||||||
|
hideLabel?: boolean |
||||||
|
hideIndicator?: boolean |
||||||
|
indicator?: "line" | "dot" | "dashed" |
||||||
|
nameKey?: string |
||||||
|
labelKey?: string |
||||||
|
} |
||||||
|
>( |
||||||
|
( |
||||||
|
{ |
||||||
|
active, |
||||||
|
payload, |
||||||
|
className, |
||||||
|
indicator = "dot", |
||||||
|
hideLabel = false, |
||||||
|
hideIndicator = false, |
||||||
|
label, |
||||||
|
labelFormatter, |
||||||
|
labelClassName, |
||||||
|
formatter, |
||||||
|
color, |
||||||
|
nameKey, |
||||||
|
labelKey, |
||||||
|
}, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const { config } = useChart() |
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => { |
||||||
|
if (hideLabel || !payload?.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
const [item] = payload |
||||||
|
const key = `${labelKey || item.dataKey || item.name || "value"}` |
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key) |
||||||
|
const value = |
||||||
|
!labelKey && typeof label === "string" |
||||||
|
? config[label as keyof typeof config]?.label || label |
||||||
|
: itemConfig?.label |
||||||
|
|
||||||
|
if (labelFormatter) { |
||||||
|
return ( |
||||||
|
<div className={cn("font-medium", labelClassName)}> |
||||||
|
{labelFormatter(value, payload)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (!value) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div> |
||||||
|
}, [ |
||||||
|
label, |
||||||
|
labelFormatter, |
||||||
|
payload, |
||||||
|
hideLabel, |
||||||
|
labelClassName, |
||||||
|
config, |
||||||
|
labelKey, |
||||||
|
]) |
||||||
|
|
||||||
|
if (!active || !payload?.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot" |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", |
||||||
|
className |
||||||
|
)} |
||||||
|
> |
||||||
|
{!nestLabel ? tooltipLabel : null} |
||||||
|
<div className="grid gap-1.5"> |
||||||
|
{payload.map((item, index) => { |
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}` |
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key) |
||||||
|
const indicatorColor = color || item.payload.fill || item.color |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
key={item.dataKey} |
||||||
|
className={cn( |
||||||
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", |
||||||
|
indicator === "dot" && "items-center" |
||||||
|
)} |
||||||
|
> |
||||||
|
{formatter && item?.value !== undefined && item.name ? ( |
||||||
|
formatter(item.value, item.name, item, index, item.payload) |
||||||
|
) : ( |
||||||
|
<> |
||||||
|
{itemConfig?.icon ? ( |
||||||
|
<itemConfig.icon /> |
||||||
|
) : ( |
||||||
|
!hideIndicator && ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", |
||||||
|
{ |
||||||
|
"h-2.5 w-2.5": indicator === "dot", |
||||||
|
"w-1": indicator === "line", |
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent": |
||||||
|
indicator === "dashed", |
||||||
|
"my-0.5": nestLabel && indicator === "dashed", |
||||||
|
} |
||||||
|
)} |
||||||
|
style={ |
||||||
|
{ |
||||||
|
"--color-bg": indicatorColor, |
||||||
|
"--color-border": indicatorColor, |
||||||
|
} as React.CSSProperties |
||||||
|
} |
||||||
|
/> |
||||||
|
) |
||||||
|
)} |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-1 justify-between leading-none", |
||||||
|
nestLabel ? "items-end" : "items-center" |
||||||
|
)} |
||||||
|
> |
||||||
|
<div className="grid gap-1.5"> |
||||||
|
{nestLabel ? tooltipLabel : null} |
||||||
|
<span className="text-muted-foreground"> |
||||||
|
{itemConfig?.label || item.name} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
{item.value && ( |
||||||
|
<span className="font-mono font-medium tabular-nums text-foreground"> |
||||||
|
{item.value.toLocaleString()} |
||||||
|
</span> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
})} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
ChartTooltipContent.displayName = "ChartTooltip" |
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend |
||||||
|
|
||||||
|
const ChartLegendContent = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & |
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { |
||||||
|
hideIcon?: boolean |
||||||
|
nameKey?: string |
||||||
|
} |
||||||
|
>( |
||||||
|
( |
||||||
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const { config } = useChart() |
||||||
|
|
||||||
|
if (!payload?.length) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex items-center justify-center gap-4", |
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3", |
||||||
|
className |
||||||
|
)} |
||||||
|
> |
||||||
|
{payload.map((item) => { |
||||||
|
const key = `${nameKey || item.dataKey || "value"}` |
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
key={item.value} |
||||||
|
className={cn( |
||||||
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" |
||||||
|
)} |
||||||
|
> |
||||||
|
{itemConfig?.icon && !hideIcon ? ( |
||||||
|
<itemConfig.icon /> |
||||||
|
) : ( |
||||||
|
<div |
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]" |
||||||
|
style={{ |
||||||
|
backgroundColor: item.color, |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
{itemConfig?.label} |
||||||
|
</div> |
||||||
|
) |
||||||
|
})} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
ChartLegendContent.displayName = "ChartLegend" |
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload( |
||||||
|
config: ChartConfig, |
||||||
|
payload: unknown, |
||||||
|
key: string |
||||||
|
) { |
||||||
|
if (typeof payload !== "object" || payload === null) { |
||||||
|
return undefined |
||||||
|
} |
||||||
|
|
||||||
|
const payloadPayload = |
||||||
|
"payload" in payload && |
||||||
|
typeof payload.payload === "object" && |
||||||
|
payload.payload !== null |
||||||
|
? payload.payload |
||||||
|
: undefined |
||||||
|
|
||||||
|
let configLabelKey: string = key |
||||||
|
|
||||||
|
if ( |
||||||
|
key in payload && |
||||||
|
typeof payload[key as keyof typeof payload] === "string" |
||||||
|
) { |
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string |
||||||
|
} else if ( |
||||||
|
payloadPayload && |
||||||
|
key in payloadPayload && |
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string" |
||||||
|
) { |
||||||
|
configLabelKey = payloadPayload[ |
||||||
|
key as keyof typeof payloadPayload |
||||||
|
] as string |
||||||
|
} |
||||||
|
|
||||||
|
return configLabelKey in config |
||||||
|
? config[configLabelKey] |
||||||
|
: config[key as keyof typeof config] |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
ChartContainer, |
||||||
|
ChartTooltip, |
||||||
|
ChartTooltipContent, |
||||||
|
ChartLegend, |
||||||
|
ChartLegendContent, |
||||||
|
ChartStyle, |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" |
||||||
|
import { Check } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Checkbox = React.forwardRef< |
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CheckboxPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-gray-300 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<CheckboxPrimitive.Indicator |
||||||
|
className={cn("flex items-center justify-center text-current")} |
||||||
|
> |
||||||
|
<Check className="h-4 w-4" /> |
||||||
|
</CheckboxPrimitive.Indicator> |
||||||
|
</CheckboxPrimitive.Root> |
||||||
|
)) |
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName |
||||||
|
|
||||||
|
export { Checkbox } |
@ -0,0 +1,11 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" |
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root |
||||||
|
|
||||||
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger |
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent |
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent } |
@ -0,0 +1,153 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import { type DialogProps } from "@radix-ui/react-dialog" |
||||||
|
import { Command as CommandPrimitive } from "cmdk" |
||||||
|
import { Search } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { Dialog, DialogContent } from "@/components/ui/dialog" |
||||||
|
|
||||||
|
const Command = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CommandPrimitive |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
Command.displayName = CommandPrimitive.displayName |
||||||
|
|
||||||
|
const CommandDialog = ({ children, ...props }: DialogProps) => { |
||||||
|
return ( |
||||||
|
<Dialog {...props}> |
||||||
|
<DialogContent className="overflow-hidden p-0"> |
||||||
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[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} |
||||||
|
</Command> |
||||||
|
</DialogContent> |
||||||
|
</Dialog> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const CommandInput = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> |
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> |
||||||
|
<CommandPrimitive.Input |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
)) |
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName |
||||||
|
|
||||||
|
const CommandList = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.List>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CommandPrimitive.List |
||||||
|
ref={ref} |
||||||
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName |
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> |
||||||
|
>((props, ref) => ( |
||||||
|
<CommandPrimitive.Empty |
||||||
|
ref={ref} |
||||||
|
className="py-6 text-center text-sm" |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName |
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CommandPrimitive.Group |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName |
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CommandPrimitive.Separator |
||||||
|
ref={ref} |
||||||
|
className={cn("-mx-1 h-px bg-border", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName |
||||||
|
|
||||||
|
const CommandItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CommandPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName |
||||||
|
|
||||||
|
const CommandShortcut = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => { |
||||||
|
return ( |
||||||
|
<span |
||||||
|
className={cn( |
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
CommandShortcut.displayName = "CommandShortcut" |
||||||
|
|
||||||
|
export { |
||||||
|
Command, |
||||||
|
CommandDialog, |
||||||
|
CommandInput, |
||||||
|
CommandList, |
||||||
|
CommandEmpty, |
||||||
|
CommandGroup, |
||||||
|
CommandItem, |
||||||
|
CommandShortcut, |
||||||
|
CommandSeparator, |
||||||
|
} |
@ -0,0 +1,139 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root |
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger |
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal |
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close |
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Overlay |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
interface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> { |
||||||
|
hideCloseButton?: boolean |
||||||
|
disableClose?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
const DialogContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>, |
||||||
|
DialogContentProps |
||||||
|
>(({ className, children, hideCloseButton = false, disableClose = false, ...props }, ref) => ( |
||||||
|
<DialogPortal> |
||||||
|
<DialogOverlay className={disableClose ? "pointer-events-none" : ""} /> |
||||||
|
<DialogPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
onEscapeKeyDown={(event) => { |
||||||
|
if (disableClose) { |
||||||
|
event.preventDefault() |
||||||
|
} |
||||||
|
}} |
||||||
|
onPointerDownOutside={(event) => { |
||||||
|
if (disableClose) { |
||||||
|
event.preventDefault() |
||||||
|
} |
||||||
|
}} |
||||||
|
className={cn( |
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
{!hideCloseButton && !disableClose && ( |
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
<span className="sr-only">Close</span> |
||||||
|
</DialogPrimitive.Close> |
||||||
|
)} |
||||||
|
</DialogPrimitive.Content> |
||||||
|
</DialogPortal> |
||||||
|
)) |
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName |
||||||
|
|
||||||
|
const DialogHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
DialogHeader.displayName = "DialogHeader" |
||||||
|
|
||||||
|
const DialogFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
DialogFooter.displayName = "DialogFooter" |
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"text-lg font-semibold leading-none tracking-tight", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName |
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
Dialog, |
||||||
|
DialogPortal, |
||||||
|
DialogOverlay, |
||||||
|
DialogTrigger, |
||||||
|
DialogClose, |
||||||
|
DialogContent, |
||||||
|
DialogHeader, |
||||||
|
DialogFooter, |
||||||
|
DialogTitle, |
||||||
|
DialogDescription, |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" |
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root |
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger |
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group |
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal |
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub |
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup |
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { |
||||||
|
inset?: boolean |
||||||
|
} |
||||||
|
>(({ className, inset, children, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.SubTrigger |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", |
||||||
|
inset && "pl-8", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
<ChevronRight className="ml-auto" /> |
||||||
|
</DropdownMenuPrimitive.SubTrigger> |
||||||
|
)) |
||||||
|
DropdownMenuSubTrigger.displayName = |
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName |
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.SubContent |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DropdownMenuSubContent.displayName = |
||||||
|
DropdownMenuPrimitive.SubContent.displayName |
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> |
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.Portal> |
||||||
|
<DropdownMenuPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
sideOffset={sideOffset} |
||||||
|
className={cn( |
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", |
||||||
|
"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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</DropdownMenuPrimitive.Portal> |
||||||
|
)) |
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName |
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { |
||||||
|
inset?: boolean |
||||||
|
} |
||||||
|
>(({ className, inset, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", |
||||||
|
inset && "pl-8", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName |
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> |
||||||
|
>(({ className, children, checked, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.CheckboxItem |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
checked={checked} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> |
||||||
|
<DropdownMenuPrimitive.ItemIndicator> |
||||||
|
<Check className="h-4 w-4" /> |
||||||
|
</DropdownMenuPrimitive.ItemIndicator> |
||||||
|
</span> |
||||||
|
{children} |
||||||
|
</DropdownMenuPrimitive.CheckboxItem> |
||||||
|
)) |
||||||
|
DropdownMenuCheckboxItem.displayName = |
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName |
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.RadioItem |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> |
||||||
|
<DropdownMenuPrimitive.ItemIndicator> |
||||||
|
<Circle className="h-2 w-2 fill-current" /> |
||||||
|
</DropdownMenuPrimitive.ItemIndicator> |
||||||
|
</span> |
||||||
|
{children} |
||||||
|
</DropdownMenuPrimitive.RadioItem> |
||||||
|
)) |
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName |
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { |
||||||
|
inset?: boolean |
||||||
|
} |
||||||
|
>(({ className, inset, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.Label |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"px-2 py-1.5 text-sm font-semibold text-center", |
||||||
|
inset && "pl-8", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName |
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef< |
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DropdownMenuPrimitive.Separator |
||||||
|
ref={ref} |
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName |
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => { |
||||||
|
return ( |
||||||
|
<span |
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" |
||||||
|
|
||||||
|
export { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuTrigger, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuCheckboxItem, |
||||||
|
DropdownMenuRadioItem, |
||||||
|
DropdownMenuLabel, |
||||||
|
DropdownMenuSeparator, |
||||||
|
DropdownMenuShortcut, |
||||||
|
DropdownMenuGroup, |
||||||
|
DropdownMenuPortal, |
||||||
|
DropdownMenuSub, |
||||||
|
DropdownMenuSubContent, |
||||||
|
DropdownMenuSubTrigger, |
||||||
|
DropdownMenuRadioGroup, |
||||||
|
} |
@ -0,0 +1,178 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label" |
||||||
|
import { Slot } from "@radix-ui/react-slot" |
||||||
|
import { |
||||||
|
Controller, |
||||||
|
ControllerProps, |
||||||
|
FieldPath, |
||||||
|
FieldValues, |
||||||
|
FormProvider, |
||||||
|
useFormContext, |
||||||
|
} from "react-hook-form" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { Label } from "@/components/ui/label" |
||||||
|
|
||||||
|
const Form = FormProvider |
||||||
|
|
||||||
|
type FormFieldContextValue< |
||||||
|
TFieldValues extends FieldValues = FieldValues, |
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> |
||||||
|
> = { |
||||||
|
name: TName |
||||||
|
} |
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>( |
||||||
|
{} as FormFieldContextValue |
||||||
|
) |
||||||
|
|
||||||
|
const FormField = < |
||||||
|
TFieldValues extends FieldValues = FieldValues, |
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> |
||||||
|
>({ |
||||||
|
...props |
||||||
|
}: ControllerProps<TFieldValues, TName>) => { |
||||||
|
return ( |
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}> |
||||||
|
<Controller {...props} /> |
||||||
|
</FormFieldContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const useFormField = () => { |
||||||
|
const fieldContext = React.useContext(FormFieldContext) |
||||||
|
const itemContext = React.useContext(FormItemContext) |
||||||
|
const { getFieldState, formState } = useFormContext() |
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState) |
||||||
|
|
||||||
|
if (!fieldContext) { |
||||||
|
throw new Error("useFormField should be used within <FormField>") |
||||||
|
} |
||||||
|
|
||||||
|
const { id } = itemContext |
||||||
|
|
||||||
|
return { |
||||||
|
id, |
||||||
|
name: fieldContext.name, |
||||||
|
formItemId: `${id}-form-item`, |
||||||
|
formDescriptionId: `${id}-form-item-description`, |
||||||
|
formMessageId: `${id}-form-item-message`, |
||||||
|
...fieldState, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type FormItemContextValue = { |
||||||
|
id: string |
||||||
|
} |
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>( |
||||||
|
{} as FormItemContextValue |
||||||
|
) |
||||||
|
|
||||||
|
const FormItem = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.HTMLAttributes<HTMLDivElement> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
const id = React.useId() |
||||||
|
|
||||||
|
return ( |
||||||
|
<FormItemContext.Provider value={{ id }}> |
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} /> |
||||||
|
</FormItemContext.Provider> |
||||||
|
) |
||||||
|
}) |
||||||
|
FormItem.displayName = "FormItem" |
||||||
|
|
||||||
|
const FormLabel = React.forwardRef< |
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
const { error, formItemId } = useFormField() |
||||||
|
|
||||||
|
return ( |
||||||
|
<Label |
||||||
|
ref={ref} |
||||||
|
className={cn(error && "text-destructive", className)} |
||||||
|
htmlFor={formItemId} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
FormLabel.displayName = "FormLabel" |
||||||
|
|
||||||
|
const FormControl = React.forwardRef< |
||||||
|
React.ElementRef<typeof Slot>, |
||||||
|
React.ComponentPropsWithoutRef<typeof Slot> |
||||||
|
>(({ ...props }, ref) => { |
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() |
||||||
|
|
||||||
|
return ( |
||||||
|
<Slot |
||||||
|
ref={ref} |
||||||
|
id={formItemId} |
||||||
|
aria-describedby={ |
||||||
|
!error |
||||||
|
? `${formDescriptionId}` |
||||||
|
: `${formDescriptionId} ${formMessageId}` |
||||||
|
} |
||||||
|
aria-invalid={!!error} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
FormControl.displayName = "FormControl" |
||||||
|
|
||||||
|
const FormDescription = React.forwardRef< |
||||||
|
HTMLParagraphElement, |
||||||
|
React.HTMLAttributes<HTMLParagraphElement> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
const { formDescriptionId } = useFormField() |
||||||
|
|
||||||
|
return ( |
||||||
|
<p |
||||||
|
ref={ref} |
||||||
|
id={formDescriptionId} |
||||||
|
className={cn("text-[0.8rem] text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
FormDescription.displayName = "FormDescription" |
||||||
|
|
||||||
|
const FormMessage = React.forwardRef< |
||||||
|
HTMLParagraphElement, |
||||||
|
React.HTMLAttributes<HTMLParagraphElement> |
||||||
|
>(({ className, children, ...props }, ref) => { |
||||||
|
const { error, formMessageId } = useFormField() |
||||||
|
const body = error ? String(error?.message) : children |
||||||
|
|
||||||
|
if (!body) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<p |
||||||
|
ref={ref} |
||||||
|
id={formMessageId} |
||||||
|
className={cn("text-[0.8rem] font-medium text-destructive", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{body} |
||||||
|
</p> |
||||||
|
) |
||||||
|
}) |
||||||
|
FormMessage.displayName = "FormMessage" |
||||||
|
|
||||||
|
export { |
||||||
|
useFormField, |
||||||
|
Form, |
||||||
|
FormItem, |
||||||
|
FormLabel, |
||||||
|
FormControl, |
||||||
|
FormDescription, |
||||||
|
FormMessage, |
||||||
|
FormField, |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( |
||||||
|
({ className, type, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<input |
||||||
|
type={type} |
||||||
|
className={cn( |
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", |
||||||
|
className |
||||||
|
)} |
||||||
|
ref={ref} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
Input.displayName = "Input" |
||||||
|
|
||||||
|
export { Input } |
@ -0,0 +1,26 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const labelVariants = cva( |
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
||||||
|
) |
||||||
|
|
||||||
|
const Label = React.forwardRef< |
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & |
||||||
|
VariantProps<typeof labelVariants> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<LabelPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn(labelVariants(), className , 'text-sm')} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
Label.displayName = LabelPrimitive.Root.displayName |
||||||
|
|
||||||
|
export { Label } |
@ -0,0 +1,43 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
const PageLoader = () => { |
||||||
|
return ( |
||||||
|
<div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50"> |
||||||
|
<div className="flex flex-col items-center justify-center space-y-8"> |
||||||
|
<div className="relative w-40"> |
||||||
|
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center"> |
||||||
|
<div className="bg-white dark:bg-gray-900 rounded-full "> |
||||||
|
<h1 className="text-xl font-bold text-primary dark:text-blue-400 animate-pulse"> |
||||||
|
KJ HRMS |
||||||
|
</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="w-52 h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden"> |
||||||
|
<div className="h-full bg-primary animate-[loading_1.5s_ease-in-out_infinite]"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<style jsx>{` |
||||||
|
@keyframes loading { |
||||||
|
0% { |
||||||
|
width: 0%; |
||||||
|
margin-left: 0; |
||||||
|
} |
||||||
|
50% { |
||||||
|
width: 100%; |
||||||
|
margin-left: 0; |
||||||
|
} |
||||||
|
100% { |
||||||
|
width: 0%; |
||||||
|
margin-left: 100%; |
||||||
|
} |
||||||
|
} |
||||||
|
`}</style>
|
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default PageLoader; |
@ -0,0 +1,133 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button" |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( |
||||||
|
<nav |
||||||
|
role="navigation" |
||||||
|
aria-label="pagination" |
||||||
|
className={cn("mx-auto flex w-full justify-center", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
Pagination.displayName = "Pagination" |
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef< |
||||||
|
HTMLUListElement, |
||||||
|
React.ComponentProps<"ul"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ul |
||||||
|
ref={ref} |
||||||
|
className={cn("flex flex-row items-center gap-1", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
PaginationContent.displayName = "PaginationContent" |
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentProps<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li ref={ref} className={cn("", className)} {...props} /> |
||||||
|
)) |
||||||
|
PaginationItem.displayName = "PaginationItem" |
||||||
|
|
||||||
|
type PaginationLinkProps = { |
||||||
|
isActive?: boolean |
||||||
|
} & Pick<ButtonProps, "size"> & |
||||||
|
React.ComponentProps<"a"> |
||||||
|
|
||||||
|
const PaginationLink = ({ |
||||||
|
className, |
||||||
|
isActive, |
||||||
|
size = "icon", |
||||||
|
...props |
||||||
|
}: PaginationLinkProps) => ( |
||||||
|
<a |
||||||
|
aria-current={isActive ? "page" : undefined} |
||||||
|
className={cn( |
||||||
|
buttonVariants({ |
||||||
|
variant: isActive ? "outline" : "ghost", |
||||||
|
size, |
||||||
|
}), |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
PaginationLink.displayName = "PaginationLink" |
||||||
|
|
||||||
|
const PaginationPrevious = ({ |
||||||
|
className, |
||||||
|
disabled, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink> & { disabled?: boolean }) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to previous page" |
||||||
|
size="default" |
||||||
|
className={cn( |
||||||
|
"gap-1 pl-2.5",
|
||||||
|
disabled && "pointer-events-none opacity-50", |
||||||
|
!disabled && "cursor-pointer", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<button disabled={disabled} className="flex gap-1 items-center"> |
||||||
|
<ChevronLeft className="h-4 w-4" /> |
||||||
|
<span>Previous</span> |
||||||
|
</button> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
|
||||||
|
const PaginationNext = ({ |
||||||
|
className, |
||||||
|
disabled, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink> & { disabled?: boolean }) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to next page" |
||||||
|
size="default" |
||||||
|
className={cn( |
||||||
|
"gap-1 pr-2.5",
|
||||||
|
disabled && "pointer-events-none opacity-50", |
||||||
|
!disabled && "cursor-pointer", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<button disabled={disabled} className="flex gap-1 items-center"> |
||||||
|
<span>Next</span> |
||||||
|
<ChevronRight className="h-4 w-4" /> |
||||||
|
</button> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
|
||||||
|
const PaginationEllipsis = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"span">) => ( |
||||||
|
<span |
||||||
|
aria-hidden |
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
<span className="sr-only">More pages</span> |
||||||
|
</span> |
||||||
|
) |
||||||
|
PaginationEllipsis.displayName = "PaginationEllipsis" |
||||||
|
|
||||||
|
export { |
||||||
|
Pagination, |
||||||
|
PaginationContent, |
||||||
|
PaginationLink, |
||||||
|
PaginationItem, |
||||||
|
PaginationPrevious, |
||||||
|
PaginationNext, |
||||||
|
PaginationEllipsis, |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root |
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger |
||||||
|
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor |
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> |
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( |
||||||
|
<PopoverPrimitive.Portal> |
||||||
|
<PopoverPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
align={align} |
||||||
|
sideOffset={sideOffset} |
||||||
|
className={cn( |
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</PopoverPrimitive.Portal> |
||||||
|
)) |
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName |
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } |
@ -0,0 +1,44 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" |
||||||
|
import { Circle } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef< |
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<RadioGroupPrimitive.Root |
||||||
|
className={cn("grid gap-2", className)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName |
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<RadioGroupPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"aspect-square h-4 w-4 rounded-full border border-gray-300 text-gray-300 shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center"> |
||||||
|
<Circle className="h-3.5 w-3.5 fill-primary" /> |
||||||
|
</RadioGroupPrimitive.Indicator> |
||||||
|
</RadioGroupPrimitive.Item> |
||||||
|
) |
||||||
|
}) |
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName |
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem } |
@ -0,0 +1,48 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef< |
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<ScrollAreaPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn("relative overflow-hidden", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> |
||||||
|
{children} |
||||||
|
</ScrollAreaPrimitive.Viewport> |
||||||
|
<ScrollBar /> |
||||||
|
<ScrollAreaPrimitive.Corner /> |
||||||
|
</ScrollAreaPrimitive.Root> |
||||||
|
)) |
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName |
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef< |
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> |
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => ( |
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar |
||||||
|
ref={ref} |
||||||
|
orientation={orientation} |
||||||
|
className={cn( |
||||||
|
"flex touch-none select-none transition-colors", |
||||||
|
orientation === "vertical" && |
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]", |
||||||
|
orientation === "horizontal" && |
||||||
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> |
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar> |
||||||
|
)) |
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName |
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar } |
@ -0,0 +1,48 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef< |
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<ScrollAreaPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn("relative overflow-hidden", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> |
||||||
|
{children} |
||||||
|
</ScrollAreaPrimitive.Viewport> |
||||||
|
<ScrollBar /> |
||||||
|
<ScrollAreaPrimitive.Corner /> |
||||||
|
</ScrollAreaPrimitive.Root> |
||||||
|
)) |
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName |
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef< |
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> |
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => ( |
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar |
||||||
|
ref={ref} |
||||||
|
orientation={orientation} |
||||||
|
className={cn( |
||||||
|
"flex touch-none select-none transition-colors", |
||||||
|
orientation === "vertical" && |
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]", |
||||||
|
orientation === "horizontal" && |
||||||
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> |
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar> |
||||||
|
)) |
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName |
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar } |
@ -0,0 +1,159 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select" |
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root |
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group |
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value |
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.Trigger |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
<SelectPrimitive.Icon asChild> |
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" /> |
||||||
|
</SelectPrimitive.Icon> |
||||||
|
</SelectPrimitive.Trigger> |
||||||
|
)) |
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName |
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.ScrollUpButton |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex cursor-default items-center justify-center py-1", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronUp className="h-4 w-4" /> |
||||||
|
</SelectPrimitive.ScrollUpButton> |
||||||
|
)) |
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName |
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.ScrollDownButton |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex cursor-default items-center justify-center py-1", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronDown className="h-4 w-4" /> |
||||||
|
</SelectPrimitive.ScrollDownButton> |
||||||
|
)) |
||||||
|
SelectScrollDownButton.displayName = |
||||||
|
SelectPrimitive.ScrollDownButton.displayName |
||||||
|
|
||||||
|
const SelectContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> |
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => ( |
||||||
|
<SelectPrimitive.Portal> |
||||||
|
<SelectPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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", |
||||||
|
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", |
||||||
|
className |
||||||
|
)} |
||||||
|
position={position} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<SelectScrollUpButton /> |
||||||
|
<SelectPrimitive.Viewport |
||||||
|
className={cn( |
||||||
|
"p-1", |
||||||
|
position === "popper" && |
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" |
||||||
|
)} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</SelectPrimitive.Viewport> |
||||||
|
<SelectScrollDownButton /> |
||||||
|
</SelectPrimitive.Content> |
||||||
|
</SelectPrimitive.Portal> |
||||||
|
)) |
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName |
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.Label |
||||||
|
ref={ref} |
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName |
||||||
|
|
||||||
|
const SelectItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> |
||||||
|
<SelectPrimitive.ItemIndicator> |
||||||
|
<Check className="h-4 w-4" /> |
||||||
|
</SelectPrimitive.ItemIndicator> |
||||||
|
</span> |
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> |
||||||
|
</SelectPrimitive.Item> |
||||||
|
)) |
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName |
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef< |
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SelectPrimitive.Separator |
||||||
|
ref={ref} |
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
Select, |
||||||
|
SelectGroup, |
||||||
|
SelectValue, |
||||||
|
SelectTrigger, |
||||||
|
SelectContent, |
||||||
|
SelectLabel, |
||||||
|
SelectItem, |
||||||
|
SelectSeparator, |
||||||
|
SelectScrollUpButton, |
||||||
|
SelectScrollDownButton, |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Separator = React.forwardRef< |
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> |
||||||
|
>( |
||||||
|
( |
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props }, |
||||||
|
ref |
||||||
|
) => ( |
||||||
|
<SeparatorPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
decorative={decorative} |
||||||
|
orientation={orientation} |
||||||
|
className={cn( |
||||||
|
"shrink-0 bg-border", |
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
) |
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName |
||||||
|
|
||||||
|
export { Separator } |
@ -0,0 +1,140 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root |
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger |
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close |
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal |
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Overlay |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
const sheetVariants = cva( |
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
side: { |
||||||
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", |
||||||
|
bottom: |
||||||
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", |
||||||
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", |
||||||
|
right: |
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
side: "right", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
interface SheetContentProps |
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, |
||||||
|
VariantProps<typeof sheetVariants> {} |
||||||
|
|
||||||
|
const SheetContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>, |
||||||
|
SheetContentProps |
||||||
|
>(({ side = "right", className, children, ...props }, ref) => ( |
||||||
|
<SheetPortal> |
||||||
|
<SheetOverlay /> |
||||||
|
<SheetPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn(sheetVariants({ side }), className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
<span className="sr-only">Close</span> |
||||||
|
</SheetPrimitive.Close> |
||||||
|
{children} |
||||||
|
</SheetPrimitive.Content> |
||||||
|
</SheetPortal> |
||||||
|
)) |
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName |
||||||
|
|
||||||
|
const SheetHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-2 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
SheetHeader.displayName = "SheetHeader" |
||||||
|
|
||||||
|
const SheetFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
SheetFooter.displayName = "SheetFooter" |
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-lg font-semibold text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName |
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
Sheet, |
||||||
|
SheetPortal, |
||||||
|
SheetOverlay, |
||||||
|
SheetTrigger, |
||||||
|
SheetClose, |
||||||
|
SheetContent, |
||||||
|
SheetHeader, |
||||||
|
SheetFooter, |
||||||
|
SheetTitle, |
||||||
|
SheetDescription, |
||||||
|
} |
@ -0,0 +1,764 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import { Slot } from "@radix-ui/react-slot" |
||||||
|
import { VariantProps, cva } from "class-variance-authority" |
||||||
|
import { PanelLeft } from "lucide-react" |
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile" |
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { Button } from "@/components/ui/button" |
||||||
|
import { Input } from "@/components/ui/input" |
||||||
|
import { Separator } from "@/components/ui/separator" |
||||||
|
import { Sheet, SheetContent } from "@/components/ui/sheet" |
||||||
|
import { Skeleton } from "@/components/ui/skeleton" |
||||||
|
import { |
||||||
|
Tooltip, |
||||||
|
TooltipContent, |
||||||
|
TooltipProvider, |
||||||
|
TooltipTrigger, |
||||||
|
} from "@/components/ui/tooltip" |
||||||
|
|
||||||
|
const SIDEBAR_COOKIE_NAME = "sidebar:state" |
||||||
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 |
||||||
|
const SIDEBAR_WIDTH = "16rem" |
||||||
|
const SIDEBAR_WIDTH_MOBILE = "18rem" |
||||||
|
const SIDEBAR_WIDTH_ICON = "3rem" |
||||||
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b" |
||||||
|
|
||||||
|
type SidebarContext = { |
||||||
|
state: "expanded" | "collapsed" |
||||||
|
open: boolean |
||||||
|
setOpen: (open: boolean) => void |
||||||
|
openMobile: boolean |
||||||
|
setOpenMobile: (open: boolean) => void |
||||||
|
isMobile: boolean |
||||||
|
toggleSidebar: () => void |
||||||
|
} |
||||||
|
|
||||||
|
const SidebarContext = React.createContext<SidebarContext | null>(null) |
||||||
|
|
||||||
|
function useSidebar() { |
||||||
|
const context = React.useContext(SidebarContext) |
||||||
|
if (!context) { |
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider. from sidebar") |
||||||
|
} |
||||||
|
|
||||||
|
return context |
||||||
|
} |
||||||
|
|
||||||
|
const SidebarProvider = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & { |
||||||
|
defaultOpen?: boolean |
||||||
|
open?: boolean |
||||||
|
onOpenChange?: (open: boolean) => void |
||||||
|
} |
||||||
|
>( |
||||||
|
( |
||||||
|
{ |
||||||
|
defaultOpen = true, |
||||||
|
open: openProp, |
||||||
|
onOpenChange: setOpenProp, |
||||||
|
className, |
||||||
|
style, |
||||||
|
children, |
||||||
|
...props |
||||||
|
}, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const isMobile = useIsMobile() |
||||||
|
const [openMobile, setOpenMobile] = React.useState(false) |
||||||
|
|
||||||
|
// This is the internal state of the sidebar.
|
||||||
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
|
const [_open, _setOpen] = React.useState(defaultOpen) |
||||||
|
const open = openProp ?? _open |
||||||
|
const setOpen = React.useCallback( |
||||||
|
(value: boolean | ((value: boolean) => boolean)) => { |
||||||
|
const openState = typeof value === "function" ? value(open) : value |
||||||
|
if (setOpenProp) { |
||||||
|
setOpenProp(openState) |
||||||
|
} else { |
||||||
|
_setOpen(openState) |
||||||
|
} |
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` |
||||||
|
}, |
||||||
|
[setOpenProp, open] |
||||||
|
) |
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
const toggleSidebar = React.useCallback(() => { |
||||||
|
return isMobile |
||||||
|
? setOpenMobile((open) => !open) |
||||||
|
: setOpen((open) => !open) |
||||||
|
}, [isMobile, setOpen, setOpenMobile]) |
||||||
|
|
||||||
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
|
React.useEffect(() => { |
||||||
|
const handleKeyDown = (event: KeyboardEvent) => { |
||||||
|
if ( |
||||||
|
event.key === SIDEBAR_KEYBOARD_SHORTCUT && |
||||||
|
(event.metaKey || event.ctrlKey) |
||||||
|
) { |
||||||
|
event.preventDefault() |
||||||
|
toggleSidebar() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown) |
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown) |
||||||
|
}, [toggleSidebar]) |
||||||
|
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = open ? "expanded" : "collapsed" |
||||||
|
|
||||||
|
const contextValue = React.useMemo<SidebarContext>( |
||||||
|
() => ({ |
||||||
|
state, |
||||||
|
open, |
||||||
|
setOpen, |
||||||
|
isMobile, |
||||||
|
openMobile, |
||||||
|
setOpenMobile, |
||||||
|
toggleSidebar, |
||||||
|
}), |
||||||
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] |
||||||
|
) |
||||||
|
|
||||||
|
return ( |
||||||
|
<SidebarContext.Provider value={contextValue}> |
||||||
|
<TooltipProvider delayDuration={0}> |
||||||
|
<div |
||||||
|
style={ |
||||||
|
{ |
||||||
|
"--sidebar-width": SIDEBAR_WIDTH, |
||||||
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, |
||||||
|
...style, |
||||||
|
} as React.CSSProperties |
||||||
|
} |
||||||
|
className={cn( |
||||||
|
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", |
||||||
|
className |
||||||
|
)} |
||||||
|
ref={ref} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
</TooltipProvider> |
||||||
|
</SidebarContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
SidebarProvider.displayName = "SidebarProvider" |
||||||
|
|
||||||
|
const Sidebar = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & { |
||||||
|
side?: "left" | "right" |
||||||
|
variant?: "sidebar" | "floating" | "inset" |
||||||
|
collapsible?: "offcanvas" | "icon" | "none" |
||||||
|
} |
||||||
|
>( |
||||||
|
( |
||||||
|
{ |
||||||
|
side = "left", |
||||||
|
variant = "sidebar", |
||||||
|
collapsible = "offcanvas", |
||||||
|
className, |
||||||
|
children, |
||||||
|
...props |
||||||
|
}, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar() |
||||||
|
|
||||||
|
if (collapsible === "none") { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
ref={ref} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (isMobile) { |
||||||
|
return ( |
||||||
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> |
||||||
|
<SheetContent |
||||||
|
data-sidebar="sidebar" |
||||||
|
data-mobile="true" |
||||||
|
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" |
||||||
|
style={ |
||||||
|
{ |
||||||
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, |
||||||
|
} as React.CSSProperties |
||||||
|
} |
||||||
|
side={side} |
||||||
|
> |
||||||
|
<div className="flex h-full w-full flex-col">{children}</div> |
||||||
|
</SheetContent> |
||||||
|
</Sheet> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
className="group peer hidden md:block text-sidebar-foreground" |
||||||
|
data-state={state} |
||||||
|
data-collapsible={state === "collapsed" ? collapsible : ""} |
||||||
|
data-variant={variant} |
||||||
|
data-side={side} |
||||||
|
> |
||||||
|
{/* This is what handles the sidebar gap on desktop */} |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear", |
||||||
|
"group-data-[collapsible=offcanvas]:w-0", |
||||||
|
"group-data-[side=right]:rotate-180", |
||||||
|
variant === "floating" || variant === "inset" |
||||||
|
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" |
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]" |
||||||
|
)} |
||||||
|
/> |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex", |
||||||
|
side === "left" |
||||||
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" |
||||||
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", |
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === "floating" || variant === "inset" |
||||||
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" |
||||||
|
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<div |
||||||
|
data-sidebar="sidebar" |
||||||
|
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow" |
||||||
|
> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
Sidebar.displayName = "Sidebar" |
||||||
|
|
||||||
|
const SidebarTrigger = React.forwardRef< |
||||||
|
React.ElementRef<typeof Button>, |
||||||
|
React.ComponentProps<typeof Button> |
||||||
|
>(({ className, onClick, ...props }, ref) => { |
||||||
|
const { toggleSidebar } = useSidebar() |
||||||
|
|
||||||
|
return ( |
||||||
|
<Button |
||||||
|
ref={ref} |
||||||
|
data-sidebar="trigger" |
||||||
|
variant="ghost" |
||||||
|
size="icon" |
||||||
|
className={cn("h-7 w-7", className)} |
||||||
|
onClick={(event) => { |
||||||
|
onClick?.(event) |
||||||
|
toggleSidebar() |
||||||
|
}} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<PanelLeft /> |
||||||
|
<span className="sr-only">Toggle Sidebar</span> |
||||||
|
</Button> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarTrigger.displayName = "SidebarTrigger" |
||||||
|
|
||||||
|
const SidebarRail = React.forwardRef< |
||||||
|
HTMLButtonElement, |
||||||
|
React.ComponentProps<"button"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
const { toggleSidebar } = useSidebar() |
||||||
|
|
||||||
|
return ( |
||||||
|
<button |
||||||
|
ref={ref} |
||||||
|
data-sidebar="rail" |
||||||
|
aria-label="Toggle Sidebar" |
||||||
|
tabIndex={-1} |
||||||
|
onClick={toggleSidebar} |
||||||
|
title="Toggle Sidebar" |
||||||
|
className={cn( |
||||||
|
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", |
||||||
|
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", |
||||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", |
||||||
|
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", |
||||||
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", |
||||||
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarRail.displayName = "SidebarRail" |
||||||
|
|
||||||
|
const SidebarInset = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"main"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<main |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"relative flex min-h-svh flex-1 flex-col bg-background", |
||||||
|
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarInset.displayName = "SidebarInset" |
||||||
|
|
||||||
|
const SidebarInput = React.forwardRef< |
||||||
|
React.ElementRef<typeof Input>, |
||||||
|
React.ComponentProps<typeof Input> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<Input |
||||||
|
ref={ref} |
||||||
|
data-sidebar="input" |
||||||
|
className={cn( |
||||||
|
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarInput.displayName = "SidebarInput" |
||||||
|
|
||||||
|
const SidebarHeader = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="header" |
||||||
|
className={cn("flex flex-col gap-2 p-2", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarHeader.displayName = "SidebarHeader" |
||||||
|
|
||||||
|
const SidebarFooter = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="footer" |
||||||
|
className={cn("flex flex-col gap-2 p-2", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarFooter.displayName = "SidebarFooter" |
||||||
|
|
||||||
|
const SidebarSeparator = React.forwardRef< |
||||||
|
React.ElementRef<typeof Separator>, |
||||||
|
React.ComponentProps<typeof Separator> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<Separator |
||||||
|
ref={ref} |
||||||
|
data-sidebar="separator" |
||||||
|
className={cn("mx-2 w-auto bg-sidebar-border", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarSeparator.displayName = "SidebarSeparator" |
||||||
|
|
||||||
|
const SidebarContent = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="content" |
||||||
|
className={cn( |
||||||
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarContent.displayName = "SidebarContent" |
||||||
|
|
||||||
|
const SidebarGroup = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="group" |
||||||
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarGroup.displayName = "SidebarGroup" |
||||||
|
|
||||||
|
const SidebarGroupLabel = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & { asChild?: boolean } |
||||||
|
>(({ className, asChild = false, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "div" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
data-sidebar="group-label" |
||||||
|
className={cn( |
||||||
|
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", |
||||||
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarGroupLabel.displayName = "SidebarGroupLabel" |
||||||
|
|
||||||
|
const SidebarGroupAction = React.forwardRef< |
||||||
|
HTMLButtonElement, |
||||||
|
React.ComponentProps<"button"> & { asChild?: boolean } |
||||||
|
>(({ className, asChild = false, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "button" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
data-sidebar="group-action" |
||||||
|
className={cn( |
||||||
|
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", |
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden", |
||||||
|
"group-data-[collapsible=icon]:hidden", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarGroupAction.displayName = "SidebarGroupAction" |
||||||
|
|
||||||
|
const SidebarGroupContent = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="group-content" |
||||||
|
className={cn("w-full text-sm", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SidebarGroupContent.displayName = "SidebarGroupContent" |
||||||
|
|
||||||
|
const SidebarMenu = React.forwardRef< |
||||||
|
HTMLUListElement, |
||||||
|
React.ComponentProps<"ul"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ul |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu" |
||||||
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SidebarMenu.displayName = "SidebarMenu" |
||||||
|
|
||||||
|
const SidebarMenuItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentProps<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-item" |
||||||
|
className={cn("group/menu-item relative", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SidebarMenuItem.displayName = "SidebarMenuItem" |
||||||
|
|
||||||
|
const sidebarMenuButtonVariants = cva( |
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", |
||||||
|
outline: |
||||||
|
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", |
||||||
|
}, |
||||||
|
size: { |
||||||
|
default: "h-8 text-sm", |
||||||
|
sm: "h-7 text-xs", |
||||||
|
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
size: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
const SidebarMenuButton = React.forwardRef< |
||||||
|
HTMLButtonElement, |
||||||
|
React.ComponentProps<"button"> & { |
||||||
|
asChild?: boolean |
||||||
|
isActive?: boolean |
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent> |
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants> |
||||||
|
>( |
||||||
|
( |
||||||
|
{ |
||||||
|
asChild = false, |
||||||
|
isActive = false, |
||||||
|
variant = "default", |
||||||
|
size = "default", |
||||||
|
tooltip, |
||||||
|
className, |
||||||
|
...props |
||||||
|
}, |
||||||
|
ref |
||||||
|
) => { |
||||||
|
const Comp = asChild ? Slot : "button" |
||||||
|
const { isMobile, state } = useSidebar() |
||||||
|
|
||||||
|
const button = ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-button" |
||||||
|
data-size={size} |
||||||
|
data-active={isActive} |
||||||
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
|
||||||
|
if (!tooltip) { |
||||||
|
return button |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof tooltip === "string") { |
||||||
|
tooltip = { |
||||||
|
children: tooltip, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Tooltip> |
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger> |
||||||
|
<TooltipContent |
||||||
|
side="right" |
||||||
|
align="center" |
||||||
|
hidden={state !== "collapsed" || isMobile} |
||||||
|
{...tooltip} |
||||||
|
/> |
||||||
|
</Tooltip> |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
SidebarMenuButton.displayName = "SidebarMenuButton" |
||||||
|
|
||||||
|
const SidebarMenuAction = React.forwardRef< |
||||||
|
HTMLButtonElement, |
||||||
|
React.ComponentProps<"button"> & { |
||||||
|
asChild?: boolean |
||||||
|
showOnHover?: boolean |
||||||
|
} |
||||||
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "button" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-action" |
||||||
|
className={cn( |
||||||
|
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", |
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 after:md:hidden", |
||||||
|
"peer-data-[size=sm]/menu-button:top-1", |
||||||
|
"peer-data-[size=default]/menu-button:top-1.5", |
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5", |
||||||
|
"group-data-[collapsible=icon]:hidden", |
||||||
|
showOnHover && |
||||||
|
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarMenuAction.displayName = "SidebarMenuAction" |
||||||
|
|
||||||
|
const SidebarMenuBadge = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-badge" |
||||||
|
className={cn( |
||||||
|
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none", |
||||||
|
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", |
||||||
|
"peer-data-[size=sm]/menu-button:top-1", |
||||||
|
"peer-data-[size=default]/menu-button:top-1.5", |
||||||
|
"peer-data-[size=lg]/menu-button:top-2.5", |
||||||
|
"group-data-[collapsible=icon]:hidden", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SidebarMenuBadge.displayName = "SidebarMenuBadge" |
||||||
|
|
||||||
|
const SidebarMenuSkeleton = React.forwardRef< |
||||||
|
HTMLDivElement, |
||||||
|
React.ComponentProps<"div"> & { |
||||||
|
showIcon?: boolean |
||||||
|
} |
||||||
|
>(({ className, showIcon = false, ...props }, ref) => { |
||||||
|
// Random width between 50 to 90%.
|
||||||
|
const width = React.useMemo(() => { |
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%` |
||||||
|
}, []) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-skeleton" |
||||||
|
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{showIcon && ( |
||||||
|
<Skeleton |
||||||
|
className="size-4 rounded-md" |
||||||
|
data-sidebar="menu-skeleton-icon" |
||||||
|
/> |
||||||
|
)} |
||||||
|
<Skeleton |
||||||
|
className="h-4 flex-1 max-w-[--skeleton-width]" |
||||||
|
data-sidebar="menu-skeleton-text" |
||||||
|
style={ |
||||||
|
{ |
||||||
|
"--skeleton-width": width, |
||||||
|
} as React.CSSProperties |
||||||
|
} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" |
||||||
|
|
||||||
|
const SidebarMenuSub = React.forwardRef< |
||||||
|
HTMLUListElement, |
||||||
|
React.ComponentProps<"ul"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ul |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-sub" |
||||||
|
className={cn( |
||||||
|
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", |
||||||
|
"group-data-[collapsible=icon]:hidden", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SidebarMenuSub.displayName = "SidebarMenuSub" |
||||||
|
|
||||||
|
const SidebarMenuSubItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentProps<"li"> |
||||||
|
>(({ ...props }, ref) => <li ref={ref} {...props} />) |
||||||
|
SidebarMenuSubItem.displayName = "SidebarMenuSubItem" |
||||||
|
|
||||||
|
const SidebarMenuSubButton = React.forwardRef< |
||||||
|
HTMLAnchorElement, |
||||||
|
React.ComponentProps<"a"> & { |
||||||
|
asChild?: boolean |
||||||
|
size?: "sm" | "md" |
||||||
|
isActive?: boolean |
||||||
|
} |
||||||
|
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "a" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
data-sidebar="menu-sub-button" |
||||||
|
data-size={size} |
||||||
|
data-active={isActive} |
||||||
|
className={cn( |
||||||
|
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", |
||||||
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", |
||||||
|
size === "sm" && "text-xs", |
||||||
|
size === "md" && "text-sm", |
||||||
|
"group-data-[collapsible=icon]:hidden", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
SidebarMenuSubButton.displayName = "SidebarMenuSubButton" |
||||||
|
|
||||||
|
export { |
||||||
|
Sidebar, |
||||||
|
SidebarContent, |
||||||
|
SidebarFooter, |
||||||
|
SidebarGroup, |
||||||
|
SidebarGroupAction, |
||||||
|
SidebarGroupContent, |
||||||
|
SidebarGroupLabel, |
||||||
|
SidebarHeader, |
||||||
|
SidebarInput, |
||||||
|
SidebarInset, |
||||||
|
SidebarMenu, |
||||||
|
SidebarMenuAction, |
||||||
|
SidebarMenuBadge, |
||||||
|
SidebarMenuButton, |
||||||
|
SidebarMenuItem, |
||||||
|
SidebarMenuSkeleton, |
||||||
|
SidebarMenuSub, |
||||||
|
SidebarMenuSubButton, |
||||||
|
SidebarMenuSubItem, |
||||||
|
SidebarProvider, |
||||||
|
SidebarRail, |
||||||
|
SidebarSeparator, |
||||||
|
SidebarTrigger, |
||||||
|
useSidebar, |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Skeleton({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn("animate-pulse rounded-md bg-primary/10", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { Skeleton } |
@ -0,0 +1,111 @@ |
|||||||
|
import React, { useRef } from "react"; |
||||||
|
import { cn } from "@/lib/utils"; |
||||||
|
import { Check } from "lucide-react"; |
||||||
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; |
||||||
|
|
||||||
|
interface Step { |
||||||
|
number: number; |
||||||
|
label: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface StepperProps { |
||||||
|
steps: Step[]; |
||||||
|
currentStep: number; |
||||||
|
onStepClick: (stepNumber: number) => void; |
||||||
|
} |
||||||
|
|
||||||
|
const Stepper: React.FC<StepperProps> = ({ |
||||||
|
steps, |
||||||
|
currentStep, |
||||||
|
onStepClick, |
||||||
|
}) => { |
||||||
|
const scrollAreaRef = useRef<HTMLDivElement | null>(null); |
||||||
|
|
||||||
|
const scrollToCurrentStep = (stepIndex: number) => { |
||||||
|
const scrollArea = scrollAreaRef.current; |
||||||
|
if (scrollArea && scrollArea.children.length) { |
||||||
|
const stepElement = scrollArea.children[stepIndex] as HTMLElement; |
||||||
|
|
||||||
|
if (stepElement) { |
||||||
|
stepElement.scrollIntoView({ |
||||||
|
behavior: "smooth", |
||||||
|
block: "center", |
||||||
|
inline: "center", |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
React.useEffect(() => { |
||||||
|
scrollToCurrentStep(currentStep - 1); |
||||||
|
}, [currentStep]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<ScrollArea |
||||||
|
className="w-full whitespace-nowrap rounded-md antialiased py-4" |
||||||
|
dir="ltr" |
||||||
|
style={{ |
||||||
|
WebkitFontSmoothing: 'antialiased', |
||||||
|
MozOsxFontSmoothing: 'grayscale', |
||||||
|
textRendering: 'optimizeLegibility' |
||||||
|
}} |
||||||
|
> |
||||||
|
<div className="flex items-center justify-center" ref={scrollAreaRef}> |
||||||
|
{steps.map((step, index) => ( |
||||||
|
<div |
||||||
|
key={step.number} |
||||||
|
className="flex flex-col items-center" |
||||||
|
> |
||||||
|
<div className="flex items-center"> |
||||||
|
<div className="flex flex-col items-center justify-center"> |
||||||
|
<div |
||||||
|
onClick={() => onStepClick(step.number)} |
||||||
|
className={cn( |
||||||
|
"w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold cursor-pointer transform-gpu", |
||||||
|
step.number === currentStep |
||||||
|
? "bg-primary text-primary-foreground shadow-sm" |
||||||
|
: step.number < currentStep |
||||||
|
? "bg-primary text-primary-foreground" |
||||||
|
: "bg-secondary text-secondary-foreground" |
||||||
|
)} |
||||||
|
// style={{
|
||||||
|
// backfaceVisibility: 'hidden'
|
||||||
|
// }}
|
||||||
|
> |
||||||
|
{step.number < currentStep ? ( |
||||||
|
<Check className="w-6 h-6" /> |
||||||
|
) : ( |
||||||
|
step.number |
||||||
|
)} |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"mt-2 text-xs text-center w-20 antialiased", |
||||||
|
step.number === currentStep |
||||||
|
? "text-black dark:text-primary font-bold" |
||||||
|
: "text-black dark:text-primary" |
||||||
|
)} |
||||||
|
> |
||||||
|
{step.label} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{index < steps.length - 1 && ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"w-16 h-[2px] mb-5 -mx-5", |
||||||
|
step.number <= currentStep |
||||||
|
? "bg-primary" |
||||||
|
: "bg-slate-200" |
||||||
|
)} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
<ScrollBar orientation="horizontal" /> |
||||||
|
</ScrollArea> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Stepper; |
@ -0,0 +1,29 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Switch = React.forwardRef< |
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SwitchPrimitives.Root |
||||||
|
className={cn( |
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
> |
||||||
|
<SwitchPrimitives.Thumb |
||||||
|
className={cn( |
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" |
||||||
|
)} |
||||||
|
/> |
||||||
|
</SwitchPrimitives.Root> |
||||||
|
)) |
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName |
||||||
|
|
||||||
|
export { Switch } |
@ -0,0 +1,120 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Table = React.forwardRef< |
||||||
|
HTMLTableElement, |
||||||
|
React.HTMLAttributes<HTMLTableElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div className="relative w-full overflow-y-scroll table-scroll"> |
||||||
|
<table |
||||||
|
ref={ref} |
||||||
|
className={cn("w-full caption-bottom text-sm", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
)) |
||||||
|
Table.displayName = "Table" |
||||||
|
|
||||||
|
const TableHeader = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> |
||||||
|
)) |
||||||
|
TableHeader.displayName = "TableHeader" |
||||||
|
|
||||||
|
const TableBody = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tbody |
||||||
|
ref={ref} |
||||||
|
className={cn("[&_tr:last-child]:border-0", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableBody.displayName = "TableBody" |
||||||
|
|
||||||
|
const TableFooter = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tfoot |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableFooter.displayName = "TableFooter" |
||||||
|
|
||||||
|
const TableRow = React.forwardRef< |
||||||
|
HTMLTableRowElement, |
||||||
|
React.HTMLAttributes<HTMLTableRowElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tr |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableRow.displayName = "TableRow" |
||||||
|
|
||||||
|
const TableHead = React.forwardRef< |
||||||
|
HTMLTableCellElement, |
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<th |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableHead.displayName = "TableHead" |
||||||
|
|
||||||
|
const TableCell = React.forwardRef< |
||||||
|
HTMLTableCellElement, |
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<td |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableCell.displayName = "TableCell" |
||||||
|
|
||||||
|
const TableCaption = React.forwardRef< |
||||||
|
HTMLTableCaptionElement, |
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<caption |
||||||
|
ref={ref} |
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableCaption.displayName = "TableCaption" |
||||||
|
|
||||||
|
export { |
||||||
|
Table, |
||||||
|
TableHeader, |
||||||
|
TableBody, |
||||||
|
TableFooter, |
||||||
|
TableHead, |
||||||
|
TableRow, |
||||||
|
TableCell, |
||||||
|
TableCaption, |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root |
||||||
|
|
||||||
|
const TabsList = React.forwardRef< |
||||||
|
React.ElementRef<typeof TabsPrimitive.List>, |
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<TabsPrimitive.List |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName |
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef< |
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>, |
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<TabsPrimitive.Trigger |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName |
||||||
|
|
||||||
|
const TabsContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<TabsPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName |
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent } |
@ -0,0 +1,22 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Textarea = React.forwardRef< |
||||||
|
HTMLTextAreaElement, |
||||||
|
React.ComponentProps<"textarea"> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<textarea |
||||||
|
className={cn( |
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", |
||||||
|
className |
||||||
|
)} |
||||||
|
ref={ref} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
Textarea.displayName = "Textarea" |
||||||
|
|
||||||
|
export { Textarea } |
@ -0,0 +1,9 @@ |
|||||||
|
export const PageHeading: React.FC<{children : React.ReactNode}> = ({ |
||||||
|
children |
||||||
|
}) => { |
||||||
|
return( |
||||||
|
<>
|
||||||
|
<h2 className="text-3xl mb-1 font-semibold capitalize">{children}</h2> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider |
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Viewport |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName |
||||||
|
|
||||||
|
const toastVariants = cva( |
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: "border bg-background text-foreground", |
||||||
|
destructive: |
||||||
|
"destructive group border-destructive bg-red-500 text-white", |
||||||
|
success : 'success group border-green-500 bg-green-500 text-white' |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
const Toast = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & |
||||||
|
VariantProps<typeof toastVariants> |
||||||
|
>(({ className, variant, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<ToastPrimitives.Root |
||||||
|
ref={ref} |
||||||
|
className={cn(toastVariants({ variant }), className)} |
||||||
|
duration={2000} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName |
||||||
|
|
||||||
|
const ToastAction = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Action |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName |
||||||
|
|
||||||
|
const ToastClose = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Close |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", |
||||||
|
className |
||||||
|
)} |
||||||
|
toast-close="" |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
</ToastPrimitives.Close> |
||||||
|
)) |
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName |
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName |
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm opacity-90", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName |
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> |
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction> |
||||||
|
|
||||||
|
export { |
||||||
|
type ToastProps, |
||||||
|
type ToastActionElement, |
||||||
|
ToastProvider, |
||||||
|
ToastViewport, |
||||||
|
Toast, |
||||||
|
ToastTitle, |
||||||
|
ToastDescription, |
||||||
|
ToastClose, |
||||||
|
ToastAction, |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import { useToast } from "@/hooks/use-toast" |
||||||
|
import { |
||||||
|
Toast, |
||||||
|
ToastClose, |
||||||
|
ToastDescription, |
||||||
|
ToastProvider, |
||||||
|
ToastTitle, |
||||||
|
ToastViewport, |
||||||
|
} from "@/components/ui/toast" |
||||||
|
|
||||||
|
export function Toaster() { |
||||||
|
const { toasts } = useToast() |
||||||
|
|
||||||
|
return ( |
||||||
|
<ToastProvider> |
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) { |
||||||
|
return ( |
||||||
|
<Toast key={id} {...props}> |
||||||
|
<div className="grid gap-1"> |
||||||
|
{title && <ToastTitle>{title}</ToastTitle>} |
||||||
|
{description && ( |
||||||
|
<ToastDescription>{description}</ToastDescription> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
{action} |
||||||
|
<ToastClose /> |
||||||
|
</Toast> |
||||||
|
) |
||||||
|
})} |
||||||
|
<ToastViewport /> |
||||||
|
</ToastProvider> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider |
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root |
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger |
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> |
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => ( |
||||||
|
<TooltipPrimitive.Portal> |
||||||
|
<TooltipPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
sideOffset={sideOffset} |
||||||
|
className={cn( |
||||||
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</TooltipPrimitive.Portal> |
||||||
|
)) |
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName |
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } |
@ -0,0 +1,141 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { buttonVariants } from "@/components/ui/button" |
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root |
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger |
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal |
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Overlay |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPortal> |
||||||
|
<AlertDialogOverlay /> |
||||||
|
<AlertDialogPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</AlertDialogPortal> |
||||||
|
)) |
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName |
||||||
|
|
||||||
|
const AlertDialogHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-2 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader" |
||||||
|
|
||||||
|
const AlertDialogFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter" |
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-lg font-semibold", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName |
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogDescription.displayName = |
||||||
|
AlertDialogPrimitive.Description.displayName |
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Action |
||||||
|
ref={ref} |
||||||
|
className={cn(buttonVariants(), className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName |
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef< |
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, |
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<AlertDialogPrimitive.Cancel |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
buttonVariants({ variant: "outline" }), |
||||||
|
"mt-2 sm:mt-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
AlertDialog, |
||||||
|
AlertDialogPortal, |
||||||
|
AlertDialogOverlay, |
||||||
|
AlertDialogTrigger, |
||||||
|
AlertDialogContent, |
||||||
|
AlertDialogHeader, |
||||||
|
AlertDialogFooter, |
||||||
|
AlertDialogTitle, |
||||||
|
AlertDialogDescription, |
||||||
|
AlertDialogAction, |
||||||
|
AlertDialogCancel, |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { Slot } from "@radix-ui/react-slot" |
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef< |
||||||
|
HTMLElement, |
||||||
|
React.ComponentPropsWithoutRef<"nav"> & { |
||||||
|
separator?: React.ReactNode |
||||||
|
} |
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) |
||||||
|
Breadcrumb.displayName = "Breadcrumb" |
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef< |
||||||
|
HTMLOListElement, |
||||||
|
React.ComponentPropsWithoutRef<"ol"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ol |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbList.displayName = "BreadcrumbList" |
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentPropsWithoutRef<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li |
||||||
|
ref={ref} |
||||||
|
className={cn("inline-flex items-center gap-1.5", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem" |
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef< |
||||||
|
HTMLAnchorElement, |
||||||
|
React.ComponentPropsWithoutRef<"a"> & { |
||||||
|
asChild?: boolean |
||||||
|
} |
||||||
|
>(({ asChild, className, ...props }, ref) => { |
||||||
|
const Comp = asChild ? Slot : "a" |
||||||
|
|
||||||
|
return ( |
||||||
|
<Comp |
||||||
|
ref={ref} |
||||||
|
className={cn("transition-colors hover:text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink" |
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef< |
||||||
|
HTMLSpanElement, |
||||||
|
React.ComponentPropsWithoutRef<"span"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<span |
||||||
|
ref={ref} |
||||||
|
role="link" |
||||||
|
aria-disabled="true" |
||||||
|
aria-current="page" |
||||||
|
className={cn("font-normal text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage" |
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({ |
||||||
|
children, |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"li">) => ( |
||||||
|
<li |
||||||
|
role="presentation" |
||||||
|
aria-hidden="true" |
||||||
|
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children ?? <ChevronRight />} |
||||||
|
</li> |
||||||
|
) |
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" |
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"span">) => ( |
||||||
|
<span |
||||||
|
role="presentation" |
||||||
|
aria-hidden="true" |
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
<span className="sr-only">More</span> |
||||||
|
</span> |
||||||
|
) |
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" |
||||||
|
|
||||||
|
export { |
||||||
|
Breadcrumb, |
||||||
|
BreadcrumbList, |
||||||
|
BreadcrumbItem, |
||||||
|
BreadcrumbLink, |
||||||
|
BreadcrumbPage, |
||||||
|
BreadcrumbSeparator, |
||||||
|
BreadcrumbEllipsis, |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" |
||||||
|
import { Check } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Checkbox = React.forwardRef< |
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<CheckboxPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<CheckboxPrimitive.Indicator |
||||||
|
className={cn("flex items-center justify-center text-current")} |
||||||
|
> |
||||||
|
<Check className="h-4 w-4" /> |
||||||
|
</CheckboxPrimitive.Indicator> |
||||||
|
</CheckboxPrimitive.Root> |
||||||
|
)) |
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName |
||||||
|
|
||||||
|
export { Checkbox } |
@ -0,0 +1,11 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" |
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root |
||||||
|
|
||||||
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger |
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent |
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent } |
@ -0,0 +1,122 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root |
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger |
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal |
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close |
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Overlay |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
const DialogContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> |
||||||
|
>(({ className, children, ...props }, ref) => ( |
||||||
|
<DialogPortal> |
||||||
|
<DialogOverlay /> |
||||||
|
<DialogPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
<span className="sr-only">Close</span> |
||||||
|
</DialogPrimitive.Close> |
||||||
|
</DialogPrimitive.Content> |
||||||
|
</DialogPortal> |
||||||
|
)) |
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName |
||||||
|
|
||||||
|
const DialogHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
DialogHeader.displayName = "DialogHeader" |
||||||
|
|
||||||
|
const DialogFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
DialogFooter.displayName = "DialogFooter" |
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"text-lg font-semibold leading-none tracking-tight", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName |
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<DialogPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
Dialog, |
||||||
|
DialogPortal, |
||||||
|
DialogOverlay, |
||||||
|
DialogTrigger, |
||||||
|
DialogClose, |
||||||
|
DialogContent, |
||||||
|
DialogHeader, |
||||||
|
DialogFooter, |
||||||
|
DialogTitle, |
||||||
|
DialogDescription, |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button" |
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( |
||||||
|
<nav |
||||||
|
role="navigation" |
||||||
|
aria-label="pagination" |
||||||
|
className={cn("mx-auto flex w-full justify-center", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
Pagination.displayName = "Pagination" |
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef< |
||||||
|
HTMLUListElement, |
||||||
|
React.ComponentProps<"ul"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ul |
||||||
|
ref={ref} |
||||||
|
className={cn("flex flex-row items-center gap-1", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
PaginationContent.displayName = "PaginationContent" |
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef< |
||||||
|
HTMLLIElement, |
||||||
|
React.ComponentProps<"li"> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<li ref={ref} className={cn("", className)} {...props} /> |
||||||
|
)) |
||||||
|
PaginationItem.displayName = "PaginationItem" |
||||||
|
|
||||||
|
type PaginationLinkProps = { |
||||||
|
isActive?: boolean |
||||||
|
} & Pick<ButtonProps, "size"> & |
||||||
|
React.ComponentProps<"a"> |
||||||
|
|
||||||
|
const PaginationLink = ({ |
||||||
|
className, |
||||||
|
isActive, |
||||||
|
size = "icon", |
||||||
|
...props |
||||||
|
}: PaginationLinkProps) => ( |
||||||
|
<a |
||||||
|
aria-current={isActive ? "page" : undefined} |
||||||
|
className={cn( |
||||||
|
buttonVariants({ |
||||||
|
variant: isActive ? "outline" : "ghost", |
||||||
|
size, |
||||||
|
}), |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
PaginationLink.displayName = "PaginationLink" |
||||||
|
|
||||||
|
const PaginationPrevious = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to previous page" |
||||||
|
size="default" |
||||||
|
className={cn("gap-1 pl-2.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronLeft className="h-4 w-4" /> |
||||||
|
<span>Previous</span> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
PaginationPrevious.displayName = "PaginationPrevious" |
||||||
|
|
||||||
|
const PaginationNext = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => ( |
||||||
|
<PaginationLink |
||||||
|
aria-label="Go to next page" |
||||||
|
size="default" |
||||||
|
className={cn("gap-1 pr-2.5", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<span>Next</span> |
||||||
|
<ChevronRight className="h-4 w-4" /> |
||||||
|
</PaginationLink> |
||||||
|
) |
||||||
|
PaginationNext.displayName = "PaginationNext" |
||||||
|
|
||||||
|
const PaginationEllipsis = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"span">) => ( |
||||||
|
<span |
||||||
|
aria-hidden |
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<MoreHorizontal className="h-4 w-4" /> |
||||||
|
<span className="sr-only">More pages</span> |
||||||
|
</span> |
||||||
|
) |
||||||
|
PaginationEllipsis.displayName = "PaginationEllipsis" |
||||||
|
|
||||||
|
export { |
||||||
|
Pagination, |
||||||
|
PaginationContent, |
||||||
|
PaginationLink, |
||||||
|
PaginationItem, |
||||||
|
PaginationPrevious, |
||||||
|
PaginationNext, |
||||||
|
PaginationEllipsis, |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" |
||||||
|
import { Circle } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef< |
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<RadioGroupPrimitive.Root |
||||||
|
className={cn("grid gap-2", className)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName |
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef< |
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>, |
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> |
||||||
|
>(({ className, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<RadioGroupPrimitive.Item |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"aspect-square h-4 w-4 rounded-full border border-purple-700 text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center"> |
||||||
|
<Circle className="h-3.5 w-3.5 fill-purple-700/50" /> |
||||||
|
</RadioGroupPrimitive.Indicator> |
||||||
|
</RadioGroupPrimitive.Item> |
||||||
|
) |
||||||
|
}) |
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName |
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem } |
@ -0,0 +1,31 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Separator = React.forwardRef< |
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> |
||||||
|
>( |
||||||
|
( |
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props }, |
||||||
|
ref |
||||||
|
) => ( |
||||||
|
<SeparatorPrimitive.Root |
||||||
|
ref={ref} |
||||||
|
decorative={decorative} |
||||||
|
orientation={orientation} |
||||||
|
className={cn( |
||||||
|
"shrink-0 bg-border", |
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
) |
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName |
||||||
|
|
||||||
|
export { Separator } |
@ -0,0 +1,140 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root |
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger |
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close |
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal |
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Overlay |
||||||
|
className={cn( |
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
ref={ref} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName |
||||||
|
|
||||||
|
const sheetVariants = cva( |
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
side: { |
||||||
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", |
||||||
|
bottom: |
||||||
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", |
||||||
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", |
||||||
|
right: |
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
side: "right", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
interface SheetContentProps |
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, |
||||||
|
VariantProps<typeof sheetVariants> {} |
||||||
|
|
||||||
|
const SheetContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>, |
||||||
|
SheetContentProps |
||||||
|
>(({ side = "right", className, children, ...props }, ref) => ( |
||||||
|
<SheetPortal> |
||||||
|
<SheetOverlay /> |
||||||
|
<SheetPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
className={cn(sheetVariants({ side }), className)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
<span className="sr-only">Close</span> |
||||||
|
</SheetPrimitive.Close> |
||||||
|
{children} |
||||||
|
</SheetPrimitive.Content> |
||||||
|
</SheetPortal> |
||||||
|
)) |
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName |
||||||
|
|
||||||
|
const SheetHeader = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col space-y-2 text-center sm:text-left", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
SheetHeader.displayName = "SheetHeader" |
||||||
|
|
||||||
|
const SheetFooter = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
SheetFooter.displayName = "SheetFooter" |
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-lg font-semibold text-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName |
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<SheetPrimitive.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName |
||||||
|
|
||||||
|
export { |
||||||
|
Sheet, |
||||||
|
SheetPortal, |
||||||
|
SheetOverlay, |
||||||
|
SheetTrigger, |
||||||
|
SheetClose, |
||||||
|
SheetContent, |
||||||
|
SheetHeader, |
||||||
|
SheetFooter, |
||||||
|
SheetTitle, |
||||||
|
SheetDescription, |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Skeleton({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn("animate-pulse rounded-md bg-primary/10", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { Skeleton } |
@ -0,0 +1,120 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const Table = React.forwardRef< |
||||||
|
HTMLTableElement, |
||||||
|
React.HTMLAttributes<HTMLTableElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<div className="relative w-full overflow-auto"> |
||||||
|
<table |
||||||
|
ref={ref} |
||||||
|
className={cn("w-full caption-bottom text-sm", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
)) |
||||||
|
Table.displayName = "Table" |
||||||
|
|
||||||
|
const TableHeader = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> |
||||||
|
)) |
||||||
|
TableHeader.displayName = "TableHeader" |
||||||
|
|
||||||
|
const TableBody = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tbody |
||||||
|
ref={ref} |
||||||
|
className={cn("[&_tr:last-child]:border-0", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableBody.displayName = "TableBody" |
||||||
|
|
||||||
|
const TableFooter = React.forwardRef< |
||||||
|
HTMLTableSectionElement, |
||||||
|
React.HTMLAttributes<HTMLTableSectionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tfoot |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableFooter.displayName = "TableFooter" |
||||||
|
|
||||||
|
const TableRow = React.forwardRef< |
||||||
|
HTMLTableRowElement, |
||||||
|
React.HTMLAttributes<HTMLTableRowElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<tr |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableRow.displayName = "TableRow" |
||||||
|
|
||||||
|
const TableHead = React.forwardRef< |
||||||
|
HTMLTableCellElement, |
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<th |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableHead.displayName = "TableHead" |
||||||
|
|
||||||
|
const TableCell = React.forwardRef< |
||||||
|
HTMLTableCellElement, |
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<td |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableCell.displayName = "TableCell" |
||||||
|
|
||||||
|
const TableCaption = React.forwardRef< |
||||||
|
HTMLTableCaptionElement, |
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<caption |
||||||
|
ref={ref} |
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
TableCaption.displayName = "TableCaption" |
||||||
|
|
||||||
|
export { |
||||||
|
Table, |
||||||
|
TableHeader, |
||||||
|
TableBody, |
||||||
|
TableFooter, |
||||||
|
TableHead, |
||||||
|
TableRow, |
||||||
|
TableCell, |
||||||
|
TableCaption, |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast" |
||||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||||
|
import { X } from "lucide-react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider |
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Viewport |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName |
||||||
|
|
||||||
|
const toastVariants = cva( |
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", |
||||||
|
{ |
||||||
|
variants: { |
||||||
|
variant: { |
||||||
|
default: "border bg-background text-foreground", |
||||||
|
destructive: |
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground", |
||||||
|
success: |
||||||
|
"green-500 group border-green-500 bg-green-500 text-white", |
||||||
|
}, |
||||||
|
}, |
||||||
|
defaultVariants: { |
||||||
|
variant: "default", |
||||||
|
}, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
const Toast = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & |
||||||
|
VariantProps<typeof toastVariants> |
||||||
|
>(({ className, variant, ...props }, ref) => { |
||||||
|
return ( |
||||||
|
<ToastPrimitives.Root |
||||||
|
ref={ref} |
||||||
|
className={cn(toastVariants({ variant }), className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
}) |
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName |
||||||
|
|
||||||
|
const ToastAction = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Action |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName |
||||||
|
|
||||||
|
const ToastClose = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Close |
||||||
|
ref={ref} |
||||||
|
className={cn( |
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", |
||||||
|
className |
||||||
|
)} |
||||||
|
toast-close="" |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<X className="h-4 w-4" /> |
||||||
|
</ToastPrimitives.Close> |
||||||
|
)) |
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName |
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Title |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName |
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef< |
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>, |
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> |
||||||
|
>(({ className, ...props }, ref) => ( |
||||||
|
<ToastPrimitives.Description |
||||||
|
ref={ref} |
||||||
|
className={cn("text-sm opacity-90", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
)) |
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName |
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> |
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction> |
||||||
|
|
||||||
|
export { |
||||||
|
type ToastProps, |
||||||
|
type ToastActionElement, |
||||||
|
ToastProvider, |
||||||
|
ToastViewport, |
||||||
|
Toast, |
||||||
|
ToastTitle, |
||||||
|
ToastDescription, |
||||||
|
ToastClose, |
||||||
|
ToastAction, |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import { useToast } from "@/hooks/use-toast" |
||||||
|
import { |
||||||
|
Toast, |
||||||
|
ToastClose, |
||||||
|
ToastDescription, |
||||||
|
ToastProvider, |
||||||
|
ToastTitle, |
||||||
|
ToastViewport, |
||||||
|
} from "@/components/ui/toast" |
||||||
|
|
||||||
|
export function Toaster() { |
||||||
|
const { toasts } = useToast() |
||||||
|
|
||||||
|
return ( |
||||||
|
<ToastProvider> |
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) { |
||||||
|
return ( |
||||||
|
<Toast key={id} {...props}> |
||||||
|
<div className="grid gap-1"> |
||||||
|
{title && <ToastTitle>{title}</ToastTitle>} |
||||||
|
{description && ( |
||||||
|
<ToastDescription>{description}</ToastDescription> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
{action} |
||||||
|
<ToastClose /> |
||||||
|
</Toast> |
||||||
|
) |
||||||
|
})} |
||||||
|
<ToastViewport /> |
||||||
|
</ToastProvider> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider |
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root |
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger |
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef< |
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>, |
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> |
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => ( |
||||||
|
<TooltipPrimitive.Portal> |
||||||
|
<TooltipPrimitive.Content |
||||||
|
ref={ref} |
||||||
|
sideOffset={sideOffset} |
||||||
|
className={cn( |
||||||
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</TooltipPrimitive.Portal> |
||||||
|
)) |
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName |
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue