first commit

This commit is contained in:
2025-11-23 08:02:54 +01:00
commit afd3881015
52 changed files with 9280 additions and 0 deletions

202
components/ui/accordion.tsx Normal file
View File

@ -0,0 +1,202 @@
"use client";
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
interface AccordionContextValue {
value: string[];
onValueChange: (value: string[]) => void;
}
const AccordionContext = React.createContext<AccordionContextValue | undefined>(
undefined
);
interface AccordionProps {
type?: "single" | "multiple";
defaultValue?: string | string[];
value?: string | string[];
onValueChange?: (value: string | string[]) => void;
children: React.ReactNode;
className?: string;
}
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
({ type = "single", defaultValue, value, onValueChange, children, className }, ref) => {
const [internalValue, setInternalValue] = React.useState<string[]>(
defaultValue
? Array.isArray(defaultValue)
? defaultValue
: [defaultValue]
: []
);
const controlledValue = value
? Array.isArray(value)
? value
: [value]
: undefined;
const currentValue = controlledValue ?? internalValue;
const handleValueChange = React.useCallback(
(newValue: string[]) => {
if (!controlledValue) {
setInternalValue(newValue);
}
if (onValueChange) {
onValueChange(type === "single" ? newValue[0] || "" : newValue);
}
},
[controlledValue, onValueChange, type]
);
const contextValue = React.useMemo(
() => ({
value: currentValue,
onValueChange: handleValueChange,
}),
[currentValue, handleValueChange]
);
return (
<AccordionContext.Provider value={contextValue}>
<div ref={ref} className={cn("space-y-2", className)}>
{children}
</div>
</AccordionContext.Provider>
);
}
);
Accordion.displayName = "Accordion";
interface AccordionItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
({ value, children, className }, ref) => {
return (
<div
ref={ref}
className={cn("rounded-xl border border-gray-200 overflow-hidden", className)}
data-value={value}
>
{children}
</div>
);
}
);
AccordionItem.displayName = "AccordionItem";
interface AccordionTriggerProps {
children: React.ReactNode;
className?: string;
}
const AccordionTrigger = React.forwardRef<
HTMLButtonElement,
AccordionTriggerProps
>(({ children, className }, ref) => {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error("AccordionTrigger must be used within Accordion");
}
const item = React.useContext(ItemContext);
if (!item) {
throw new Error("AccordionTrigger must be used within AccordionItem");
}
const isOpen = context.value.includes(item.value);
const handleClick = () => {
const newValue = isOpen
? context.value.filter((v) => v !== item.value)
: [...context.value, item.value];
context.onValueChange(newValue);
};
return (
<button
ref={ref}
type="button"
onClick={handleClick}
className={cn(
"flex w-full items-center justify-between p-4 text-left font-medium text-primary transition-all hover:bg-secondary [&[data-state=open]>svg]:rotate-180",
className
)}
data-state={isOpen ? "open" : "closed"}
>
{children}
<ChevronDown className="h-5 w-5 shrink-0 transition-transform duration-200" />
</button>
);
});
AccordionTrigger.displayName = "AccordionTrigger";
interface AccordionContentProps {
children: React.ReactNode;
className?: string;
}
const ItemContext = React.createContext<{ value: string } | undefined>(
undefined
);
const AccordionContent = React.forwardRef<
HTMLDivElement,
AccordionContentProps
>(({ children, className }, ref) => {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error("AccordionContent must be used within Accordion");
}
const item = React.useContext(ItemContext);
if (!item) {
throw new Error("AccordionContent must be used within AccordionItem");
}
const isOpen = context.value.includes(item.value);
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all",
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
)}
>
<div className={cn("p-4 pt-0 text-gray-700", className)}>
{children}
</div>
</div>
);
});
AccordionContent.displayName = "AccordionContent";
const AccordionItemWithContext = React.forwardRef<
HTMLDivElement,
AccordionItemProps
>(({ value, children, ...props }, ref) => {
return (
<ItemContext.Provider value={{ value }}>
<AccordionItem ref={ref} value={value} {...props}>
{children}
</AccordionItem>
</ItemContext.Provider>
);
});
AccordionItemWithContext.displayName = "AccordionItem";
export {
Accordion,
AccordionItemWithContext as AccordionItem,
AccordionTrigger,
AccordionContent,
};

46
components/ui/button.tsx Normal file
View File

@ -0,0 +1,46 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white",
ghost: "hover:bg-secondary hover:text-secondary-foreground",
},
size: {
default: "h-12 px-6 py-3",
sm: "h-10 px-4",
lg: "h-14 px-8 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

76
components/ui/card.tsx Normal file
View File

@ -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-2xl border border-gray-200 bg-white shadow-sm",
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<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-xl font-semibold leading-none tracking-tight text-primary", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-gray-600", 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 };