continued the kita-planer
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Home,
|
||||
MapPin,
|
||||
Phone,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type UserRoleDto =
|
||||
| "SUPERADMIN"
|
||||
| "ADMIN"
|
||||
| "KOORDINATOR"
|
||||
| "ERZIEHER"
|
||||
| "ELTERN";
|
||||
|
||||
type OnboardingDialogProps = {
|
||||
user: {
|
||||
phone: string | null;
|
||||
street: string | null;
|
||||
postalCode: string | null;
|
||||
city: string | null;
|
||||
familyId: string | null;
|
||||
role: UserRoleDto;
|
||||
};
|
||||
family: {
|
||||
childrenCount: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const HIDE_ONBOARDING_DIALOG_KEY = "hideOnboardingDialog";
|
||||
|
||||
export function OnboardingDialog({ user, family }: OnboardingDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const status = useMemo(() => {
|
||||
const hasPhone = !!user.phone?.trim();
|
||||
const hasAddress =
|
||||
!!user.street?.trim() && !!user.postalCode?.trim() && !!user.city?.trim();
|
||||
const needsFamilyData = user.role !== "ERZIEHER";
|
||||
const hasFamilyAndChildren =
|
||||
!needsFamilyData || (!!user.familyId && !!family?.childrenCount);
|
||||
|
||||
const missingItems = [
|
||||
!hasPhone ? { label: "Telefonnummer fehlt", icon: Phone } : null,
|
||||
!hasAddress ? { label: "Adresse fehlt", icon: MapPin } : null,
|
||||
!hasFamilyAndChildren
|
||||
? { label: "Haushalts-/Kinderdaten unvollständig", icon: Home }
|
||||
: null,
|
||||
].filter(Boolean) as { label: string; icon: typeof Phone }[];
|
||||
|
||||
const completedCount = [hasPhone, hasAddress, hasFamilyAndChildren].filter(
|
||||
Boolean,
|
||||
).length;
|
||||
const progress = completedCount === 3 ? 100 : completedCount * 33;
|
||||
|
||||
return {
|
||||
completedCount,
|
||||
progress,
|
||||
missingItems,
|
||||
hasFamilyAndChildren,
|
||||
isComplete: completedCount === 3,
|
||||
};
|
||||
}, [family?.childrenCount, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status.isComplete) return;
|
||||
|
||||
const hidden = localStorage.getItem(HIDE_ONBOARDING_DIALOG_KEY);
|
||||
if (hidden !== "true") {
|
||||
const timer = window.setTimeout(() => setOpen(true), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
}, [status.isComplete]);
|
||||
|
||||
if (status.isComplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ctaLabel = status.hasFamilyAndChildren
|
||||
? "Kontaktdaten hinterlegen"
|
||||
: "Haushalt & Kinder einrichten";
|
||||
|
||||
function hidePermanently() {
|
||||
localStorage.setItem(HIDE_ONBOARDING_DIALOG_KEY, "true");
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="overflow-hidden p-0 sm:max-w-xl">
|
||||
<div className="bg-emerald-950 px-6 py-6 text-white">
|
||||
<div className="mb-5 flex h-11 w-11 items-center justify-center rounded-full bg-emerald-300 text-emerald-950">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<DialogHeader className="space-y-2 text-left">
|
||||
<DialogTitle className="text-2xl leading-tight">
|
||||
Dein Kita-Profil ist fast bereit
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-emerald-50/80">
|
||||
Vervollständige die wichtigsten Daten, damit Notdienst,
|
||||
Adressbuch und Familienverwaltung reibungslos funktionieren.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 px-6 py-6">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-sm">
|
||||
<span className="font-medium">Profil-Fortschritt</span>
|
||||
<span className="text-muted-foreground">
|
||||
{status.completedCount}/3 erledigt
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={status.progress} />
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{status.progress}% vollständig
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/30 p-4">
|
||||
<p className="mb-3 text-sm font-medium">Noch offen</p>
|
||||
<ul className="space-y-2">
|
||||
{status.missingItems.map(({ label, icon: Icon }) => (
|
||||
<li key={label} className="flex items-center gap-2 text-sm">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
{label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 rounded-md bg-emerald-50 p-3 text-sm text-emerald-950">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<p>
|
||||
Du kannst die App weiter nutzen. Diese Erinnerung hilft nur beim
|
||||
sauberen Einrichten der wichtigsten MVP-Daten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 border-t px-6 py-4 sm:justify-between sm:space-x-0">
|
||||
<Button type="button" variant="ghost" onClick={hidePermanently}>
|
||||
Nicht mehr anzeigen
|
||||
</Button>
|
||||
<Button asChild onClick={() => setOpen(false)}>
|
||||
<Link href="/dashboard/profil">
|
||||
{ctaLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,18 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LayoutDashboard, Users, CalendarCheck2, CalendarDays, Contact, GraduationCap, UserCircle } from "lucide-react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
CalendarCheck2,
|
||||
CalendarDays,
|
||||
Contact,
|
||||
GraduationCap,
|
||||
UserCircle,
|
||||
ClipboardList,
|
||||
Stethoscope,
|
||||
Megaphone,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -45,7 +56,28 @@ const navItems = [
|
||||
label: "Familienverwaltung",
|
||||
icon: Users,
|
||||
exact: false,
|
||||
allowedRoles: [UserRole.ADMIN, UserRole.KOORDINATOR],
|
||||
allowedRoles: [UserRole.ADMIN],
|
||||
},
|
||||
{
|
||||
href: "/dashboard/admin/dienste",
|
||||
label: "Dienstplan",
|
||||
icon: ClipboardList,
|
||||
exact: false,
|
||||
allowedRoles: [UserRole.ADMIN],
|
||||
},
|
||||
{
|
||||
href: "/dashboard/admin/news",
|
||||
label: "Schwarzes Brett",
|
||||
icon: Megaphone,
|
||||
exact: false,
|
||||
allowedRoles: [UserRole.ADMIN],
|
||||
},
|
||||
{
|
||||
href: "/dashboard/admin/abwesenheiten",
|
||||
label: "Abwesenheiten",
|
||||
icon: Stethoscope,
|
||||
exact: false,
|
||||
allowedRoles: [UserRole.ADMIN, UserRole.KOORDINATOR, UserRole.ERZIEHER],
|
||||
},
|
||||
{
|
||||
href: "/dashboard/erzieher",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
export function MarkdownContent({ content }: { content: string }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ children, ...props }) => (
|
||||
<a
|
||||
{...props}
|
||||
className="font-medium text-primary underline underline-offset-4"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="mb-3 leading-7 last:mb-0">{children}</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="mb-3 list-disc space-y-1 pl-5">{children}</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="mb-3 list-decimal space-y-1 pl-5">{children}</ol>
|
||||
),
|
||||
h1: ({ children }) => (
|
||||
<h1 className="mb-3 text-2xl font-semibold">{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="mb-3 text-xl font-semibold">{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="mb-2 text-lg font-semibold">{children}</h3>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="mb-3 border-l-4 pl-4 text-muted-foreground">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} 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 } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
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,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div className={cn("grid gap-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
id={formMessageId}
|
||||
className={cn("text-sm text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ComponentRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-transform"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,122 @@
|
||||
"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.ComponentRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 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.ComponentRef<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.ComponentRef<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.ComponentRef<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",
|
||||
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 SelectItem = React.forwardRef<
|
||||
React.ComponentRef<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-8 pr-2 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 left-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;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SwitchProps = Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
"type" | "onChange"
|
||||
> & {
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
};
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
SwitchProps
|
||||
>(({ className, onCheckedChange, onChange, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
onChange={(event) => {
|
||||
onChange?.(event);
|
||||
onCheckedChange?.(event.currentTarget.checked);
|
||||
}}
|
||||
className={cn(
|
||||
"peer h-5 w-9 shrink-0 cursor-pointer appearance-none rounded-full border border-transparent bg-input shadow-sm transition-colors checked:bg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"before:block before:h-4 before:w-4 before:translate-x-0 before:rounded-full before:bg-background before:shadow before:transition-transform checked:before:translate-x-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Switch.displayName = "Switch";
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
Reference in New Issue
Block a user