feat(auth): enterprise authentication and user management

This commit is contained in:
Schubert Ferenc 2026-07-02 22:19:12 +02:00
parent 4bc8b20a21
commit 86a32a942c
36 changed files with 2239 additions and 324 deletions

View file

@ -0,0 +1,67 @@
"use client";
import type { ReactNode } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
type Props = {
open: boolean;
title: string;
description: string;
confirmLabel?: string;
pending?: boolean;
children?: ReactNode;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
};
export default function ConfirmDialog({
open,
title,
description,
confirmLabel = "Löschen",
pending = false,
children,
onOpenChange,
onConfirm,
}: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{children && <div className="rounded-lg border bg-slate-50 p-3">{children}</div>}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={pending}
>
Abbrechen
</Button>
<Button
type="button"
variant="destructive"
onClick={onConfirm}
disabled={pending}
>
{pending ? "Wird gelöscht..." : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,117 @@
"use client";
import type { ReactNode } from "react";
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
export type DataTableColumn<T> = {
key: string;
label: string;
sortable?: boolean;
className?: string;
render: (row: T) => ReactNode;
};
type Props<T> = {
columns: DataTableColumn<T>[];
rows: T[];
rowKey: (row: T) => string | number;
sortKey: string;
sortDirection: "asc" | "desc";
loading?: boolean;
error?: string;
emptyTitle: string;
emptyDescription: string;
onSort: (key: string) => void;
};
export default function DataTable<T>({
columns,
rows,
rowKey,
sortKey,
sortDirection,
loading = false,
error,
emptyTitle,
emptyDescription,
onSort,
}: Props<T>) {
return (
<div className="overflow-hidden rounded-lg border bg-white">
<div className="overflow-x-auto">
<table className="w-full min-w-[920px] text-sm">
<thead className="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr>
{columns.map((column) => {
const active = sortKey === column.key;
const SortIcon = active
? sortDirection === "asc"
? ArrowUp
: ArrowDown
: ArrowUpDown;
return (
<th key={column.key} className={column.className ?? "px-4 py-3"}>
{column.sortable ? (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-1 text-xs uppercase tracking-wide"
onClick={() => onSort(column.key)}
>
{column.label}
<SortIcon size={14} />
</Button>
) : (
column.label
)}
</th>
);
})}
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-slate-500">
Daten werden geladen...
</td>
</tr>
)}
{!loading && error && (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-red-600">
{error}
</td>
</tr>
)}
{!loading && !error && rows.length === 0 && (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center">
<p className="font-medium text-slate-900">{emptyTitle}</p>
<p className="mt-1 text-slate-500">{emptyDescription}</p>
</td>
</tr>
)}
{!loading && !error && rows.map((row) => (
<tr key={rowKey(row)} className="border-t hover:bg-slate-50">
{columns.map((column) => (
<td key={column.key} className={column.className ?? "px-4 py-3"}>
{column.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View file

@ -0,0 +1,29 @@
"use client";
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
type Props = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
};
export default function SearchInput({
value,
onChange,
placeholder = "Suchen",
}: Props) {
return (
<div className="relative w-full max-w-sm">
<Search className="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
<Input
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
className="pl-8"
/>
</div>
);
}

View file

@ -0,0 +1,20 @@
import { cn } from "@/lib/utils";
type Props = {
active: boolean;
};
export default function StatusBadge({ active }: Props) {
return (
<span
className={cn(
"inline-flex h-6 items-center rounded-full px-2 text-xs font-medium",
active
? "bg-emerald-50 text-emerald-700 ring-1 ring-emerald-600/20"
: "bg-slate-100 text-slate-600 ring-1 ring-slate-500/20",
)}
>
{active ? "Aktiv" : "Inaktiv"}
</span>
);
}