Refine root UI around shadcn primitives
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-07 22:28:52 +01:00
parent 985fbe1745
commit a18370d003
36 changed files with 1344 additions and 597 deletions
+166 -87
View File
@@ -1,15 +1,34 @@
"use client";
import type { MediaKind, PortfolioAssetKind, PortfolioSectionType } from "@prisma/client";
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
import {
ArrowDown,
ArrowUp,
BriefcaseBusiness,
CalendarRange,
FolderTree,
Hash,
Link2,
Plus,
Sparkles,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { MediaFieldPicker, type MediaFieldState } from "@/components/root/media-field-picker";
import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { moveArrayItem } from "@/lib/array";
@@ -176,6 +195,8 @@ export function PortfolioProjectForm({
}))
: [],
);
const [isFeatured, setIsFeatured] = useState(project?.isFeatured ?? false);
const [isPublished, setIsPublished] = useState(project?.isPublished ?? false);
const sectionsPayload = JSON.stringify(
sections.map((section, index) => ({
@@ -205,71 +226,106 @@ export function PortfolioProjectForm({
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="categoryId">Kategorie</Label>
<select
id="categoryId"
name="categoryId"
defaultValue={project?.category.id ?? categories[0]?.id ?? ""}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
required
>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name.de} / {category.name.en}
</option>
))}
</select>
<Label htmlFor="categoryId" className="sr-only">Kategorie</Label>
<div className="relative">
<FolderTree className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Select
name="categoryId"
defaultValue={project?.category.id ?? categories[0]?.id ?? ""}
required
>
<SelectTrigger id="categoryId" className="pl-9">
<SelectValue placeholder="Kategorie" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name.de} / {category.name.en}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="slug">Slug</Label>
<Input id="slug" name="slug" defaultValue={project?.slug ?? ""} required />
<Label htmlFor="slug" className="sr-only">Slug</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="slug"
name="slug"
defaultValue={project?.slug ?? ""}
placeholder="Slug"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="clientName">Kundenname</Label>
<Input
id="clientName"
name="clientName"
defaultValue={project?.clientName ?? ""}
required
/>
<Label htmlFor="clientName" className="sr-only">Kundenname</Label>
<div className="relative">
<BriefcaseBusiness className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="clientName"
name="clientName"
defaultValue={project?.clientName ?? ""}
placeholder="Kundenname"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="projectYear">Projektjahr</Label>
<Input
id="projectYear"
name="projectYear"
type="number"
min="2000"
max="2100"
defaultValue={project?.projectYear ?? new Date().getFullYear()}
required
/>
<Label htmlFor="projectYear" className="sr-only">Projektjahr</Label>
<div className="relative">
<CalendarRange className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="projectYear"
name="projectYear"
type="number"
min="2000"
max="2100"
defaultValue={project?.projectYear ?? new Date().getFullYear()}
placeholder="Projektjahr"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="previewUrl">Preview URL</Label>
<Input
id="previewUrl"
name="previewUrl"
type="url"
defaultValue={project?.previewUrl ?? ""}
placeholder="https://example.com"
/>
<Label htmlFor="previewUrl" className="sr-only">Preview URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="previewUrl"
name="previewUrl"
type="url"
defaultValue={project?.previewUrl ?? ""}
placeholder="https://example.com"
className="pl-9"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sortOrder">Sortierung</Label>
<Input
id="sortOrder"
name="sortOrder"
type="number"
min="0"
defaultValue={project?.sortOrder ?? 0}
required
/>
<Label htmlFor="sortOrder" className="sr-only">Sortierung</Label>
<div className="relative">
<Hash className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="sortOrder"
name="sortOrder"
type="number"
min="0"
defaultValue={project?.sortOrder ?? 0}
placeholder="Sortierung"
className="pl-9"
required
/>
</div>
</div>
<div className="space-y-2 md:col-span-2">
@@ -284,13 +340,23 @@ export function PortfolioProjectForm({
/>
</div>
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
<input type="checkbox" name="isFeatured" defaultChecked={project?.isFeatured ?? false} />
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
<Checkbox
name="isFeatured"
checked={isFeatured}
onCheckedChange={(checked) => setIsFeatured(checked === true)}
/>
<Sparkles className="h-4 w-4 text-muted-foreground" />
Hervorgehoben
</label>
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
<input type="checkbox" name="isPublished" defaultChecked={project?.isPublished ?? false} />
<label className="flex items-center gap-3 rounded-md border border-border px-4 py-3 text-sm">
<Checkbox
name="isPublished"
checked={isPublished}
onCheckedChange={(checked) => setIsPublished(checked === true)}
/>
<Sparkles className="h-4 w-4 text-muted-foreground" />
Veroeffentlicht
</label>
</CardContent>
@@ -411,25 +477,29 @@ export function PortfolioProjectForm({
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Typ</Label>
<select
<Select
value={section.type}
onChange={(event) =>
onValueChange={(value) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index
? { ...item, type: event.target.value as PortfolioSectionType }
? { ...item, type: value as PortfolioSectionType }
: item,
),
)
}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
{sectionTypeOptions.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
<SelectTrigger>
<SelectValue placeholder="Typ" />
</SelectTrigger>
<SelectContent>
{sectionTypeOptions.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2">
@@ -457,17 +527,22 @@ export function PortfolioProjectForm({
</div>
<div className="space-y-2 md:col-span-2">
<Label>Link URL</Label>
<Input
value={section.linkUrl}
onChange={(event) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index ? { ...item, linkUrl: event.target.value } : item,
),
)
}
/>
<Label className="sr-only">Link URL</Label>
<div className="relative">
<Link2 className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={section.linkUrl}
onChange={(event) =>
setSections((current) =>
current.map((item, currentIndex) =>
currentIndex === index ? { ...item, linkUrl: event.target.value } : item,
),
)
}
placeholder="Link URL"
className="pl-9"
/>
</div>
</div>
{localeFieldConfig.map((locale) => (
@@ -575,32 +650,36 @@ export function PortfolioProjectForm({
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Typ</Label>
<select
<Select
value={asset.kind}
onChange={(event) =>
onValueChange={(value) =>
setAssets((current) =>
current.map((item, currentIndex) =>
currentIndex === index
? {
...item,
kind: event.target.value as PortfolioAssetKind,
kind: value as PortfolioAssetKind,
media: {
...item.media,
kind: event.target.value as PortfolioAssetKind,
kind: value as PortfolioAssetKind,
},
}
: item,
),
)
}
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
>
{assetKindOptions.map((kind) => (
<option key={kind} value={kind}>
{kind}
</option>
))}
</select>
<SelectTrigger>
<SelectValue placeholder="Typ" />
</SelectTrigger>
<SelectContent>
{assetKindOptions.map((kind) => (
<SelectItem key={kind} value={kind}>
{kind}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2 md:col-span-2">