feat(admin): Produkt anlegen gegliedert wie Produkt bearbeiten
Gemeinsame Formularbausteine in components/admin-form.tsx (Feld, Auswahl, Abschnitt, Schalter, Formularrahmen mit fester Speicherleiste). Das Anlegen-Formular nutzt dieselben Abschnitte wie das Bearbeiten und blendet Ernte/Charge sowie Geschmack per :has() aus, sobald keine Honig-Kategorie gewaehlt ist. Eingabefelder in Rastern stehen buendig, auch wenn Beschriftungen mobil umbrechen. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
76ebe7d042
commit
c125e91941
6 files changed
+324
-278
No files matched your search
@@ -0,0 +1,148 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Gemeinsame Bausteine der Admin-Formulare (Produkt anlegen/bearbeiten,
|
||||
// spaeter Gutscheine, Versand, Benutzer). Reine Server-Komponenten — sie
|
||||
// rendern nur Markup, damit jedes Formular gleich gegliedert aussieht.
|
||||
|
||||
const inputClass = "mt-1 w-full border border-ink/20 bg-background px-3 py-2";
|
||||
|
||||
export function AdminField({
|
||||
name,
|
||||
label,
|
||||
type = "text",
|
||||
defaultValue,
|
||||
placeholder,
|
||||
required = false,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
<input
|
||||
name={name}
|
||||
type={type}
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
className={inputClass}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminTextarea({
|
||||
name,
|
||||
label,
|
||||
defaultValue,
|
||||
required = false,
|
||||
rows = 3,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
defaultValue?: string;
|
||||
required?: boolean;
|
||||
rows?: number;
|
||||
}) {
|
||||
return (
|
||||
<label className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
<textarea name={name} defaultValue={defaultValue} required={required} rows={rows} className={inputClass} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminSelect({
|
||||
name,
|
||||
label,
|
||||
defaultValue,
|
||||
children,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
defaultValue?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="block text-sm">
|
||||
<span className="text-ink/70">{label}</span>
|
||||
<select name={name} defaultValue={defaultValue} className={inputClass}>
|
||||
{children}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminFieldset({
|
||||
legend,
|
||||
last = false,
|
||||
className = "",
|
||||
children,
|
||||
}: {
|
||||
legend: string;
|
||||
/** Letzter Abschnitt vor der Speicherleiste: ohne Trennlinie. */
|
||||
last?: boolean;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<fieldset className={`space-y-4 ${last ? "" : "border-b border-ink/10 pb-8"} ${className}`}>
|
||||
<legend className="mb-4 font-serif text-xl text-forest">{legend}</legend>
|
||||
{children}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminHint({ children }: { children: ReactNode }) {
|
||||
return <p className="text-xs text-ink/65">{children}</p>;
|
||||
}
|
||||
|
||||
/** Schalter statt kleiner Checkbox — dasselbe Formularfeld, nur als Umschalter dargestellt. */
|
||||
export function AdminSwitch({
|
||||
name,
|
||||
label,
|
||||
defaultChecked,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
defaultChecked?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="inline-flex cursor-pointer items-center gap-3 text-sm text-ink/80">
|
||||
<input type="checkbox" name={name} defaultChecked={defaultChecked} className="peer sr-only" />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="relative h-6 w-11 flex-shrink-0 rounded-full bg-ink/20 transition-colors duration-300 peer-checked:bg-forest peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-honey-dark after:absolute after:left-0.5 after:top-0.5 after:h-5 after:w-5 after:rounded-full after:bg-cream after:shadow after:transition-transform after:duration-300 after:content-[''] peer-checked:after:translate-x-5"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/** Formularrahmen samt Speicherleiste, die beim Scrollen am unteren Rand
|
||||
bleibt — der Knopf ist bei langen Formularen immer erreichbar. */
|
||||
export function AdminFormPanel({
|
||||
action,
|
||||
actions,
|
||||
className = "",
|
||||
children,
|
||||
}: {
|
||||
action: (formData: FormData) => void | Promise<void>;
|
||||
actions: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<form action={action} className={`space-y-8 border border-ink/10 bg-background p-6 sm:p-8 ${className}`}>
|
||||
{children}
|
||||
<div className="sticky bottom-0 -mx-6 -mb-6 flex items-center justify-end gap-4 border-t border-ink/10 bg-background/95 px-6 py-4 backdrop-blur sm:-mx-8 sm:-mb-8 sm:px-8">
|
||||
{actions}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user