style: refine settings taxonomy UI (field cells, icon headers, 2-col grid)
- PoolManager: defined field cells with value count, refined chips, inline ghost + button, focus ring, pending/empty states - TaxonomyManager: icon-badged section headers, ordered 2-col grid, sync explainer note - Settings page: wider container (max-w-4xl) for the grid to breathe Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,8 @@ export default async function ImpostazioniPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[#1a1a1a] mb-6">Impostazioni</h1>
|
<h1 className="text-2xl font-bold text-[#1a1a1a] mb-6">Impostazioni</h1>
|
||||||
|
|
||||||
<div className="space-y-6 max-w-2xl">
|
<div className="space-y-6 max-w-4xl">
|
||||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
|
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6 max-w-md">
|
||||||
<h2 className="text-base font-semibold text-[#1a1a1a] mb-4">Analytics Profittabilità</h2>
|
<h2 className="text-base font-semibold text-[#1a1a1a] mb-4">Analytics Profittabilità</h2>
|
||||||
|
|
||||||
<form action={handleSave} className="space-y-4">
|
<form action={handleSave} className="space-y-4">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { X } from "lucide-react";
|
import { X, Plus } from "lucide-react";
|
||||||
|
|
||||||
export function PoolManager({
|
export function PoolManager({
|
||||||
label,
|
label,
|
||||||
@@ -18,6 +18,8 @@ export function PoolManager({
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const canAdd = input.trim().length > 0;
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
@@ -48,52 +50,68 @@ export function PoolManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-w-[180px]">
|
<div
|
||||||
<p className="text-sm font-medium text-[#1a1a1a] mb-2">{label}</p>
|
className={`rounded-lg border border-[#ececee] bg-[#fafafa] p-3.5 transition-opacity ${
|
||||||
|
isPending ? "opacity-60" : ""
|
||||||
<div className="flex flex-wrap gap-1.5 mb-3 min-h-[28px]">
|
}`}
|
||||||
{pool.length === 0 && (
|
>
|
||||||
<span className="text-xs text-[#a1a1aa]">Nessun valore</span>
|
<div className="flex items-center justify-between mb-2.5">
|
||||||
|
<p className="text-sm font-medium text-[#1a1a1a]">{label}</p>
|
||||||
|
{pool.length > 0 && (
|
||||||
|
<span className="text-[11px] text-[#a1a1aa] tabular-nums">{pool.length}</span>
|
||||||
)}
|
)}
|
||||||
{pool.map((v) => (
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1.5 mb-3 min-h-[26px]">
|
||||||
|
{pool.length === 0 ? (
|
||||||
|
<span className="text-xs text-[#a1a1aa] italic">Nessun valore</span>
|
||||||
|
) : (
|
||||||
|
pool.map((v) => (
|
||||||
<span
|
<span
|
||||||
key={v}
|
key={v}
|
||||||
className="inline-flex items-center gap-1 bg-[#f4f4f5] text-[#1a1a1a] text-xs px-2 py-0.5 rounded-full"
|
className="group inline-flex items-center gap-1 rounded-md border border-[#e5e7eb] bg-white pl-2.5 pr-1 py-1 text-xs text-[#27272a] shadow-[0_1px_1px_rgba(0,0,0,0.02)]"
|
||||||
>
|
>
|
||||||
{v}
|
{v}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemove(v)}
|
onClick={() => handleRemove(v)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="text-[#71717a] hover:text-red-500 transition-colors disabled:opacity-50"
|
className="rounded p-0.5 text-[#c4c4c8] transition-colors hover:bg-red-50 hover:text-red-500 disabled:opacity-50"
|
||||||
aria-label={`Rimuovi ${v}`}
|
aria-label={`Elimina ${v}`}
|
||||||
|
title={`Elimina "${v}" ovunque`}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1.5">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => { setInput(e.target.value); setError(null); }}
|
onChange={(e) => {
|
||||||
|
setInput(e.target.value);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
placeholder="Aggiungi..."
|
placeholder="Aggiungi valore"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="border border-[#e5e7eb] rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20 flex-1 min-w-0 disabled:opacity-50"
|
aria-label={`Aggiungi valore a ${label}`}
|
||||||
|
className="h-8 w-full rounded-md border border-[#e5e7eb] bg-white pl-3 pr-9 text-sm text-[#1a1a1a] placeholder:text-[#a1a1aa] transition-shadow focus:border-[#1A463C]/30 focus:outline-none focus:ring-2 focus:ring-[#1A463C]/15 disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
disabled={isPending || !input.trim()}
|
disabled={isPending || !canAdd}
|
||||||
className="bg-[#1A463C] text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors disabled:opacity-40"
|
aria-label={`Conferma aggiunta a ${label}`}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 grid h-6 w-6 place-items-center rounded text-[#1A463C] transition-colors hover:bg-[#1A463C]/10 disabled:text-[#c4c4c8] disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
+
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
|
{error && <p className="mt-1.5 text-xs text-red-500">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Tag, BookOpen } from "lucide-react";
|
||||||
import { PoolManager } from "./PoolManager";
|
import { PoolManager } from "./PoolManager";
|
||||||
import { addTaxonomyValue, removeTaxonomyValue } from "@/app/admin/impostazioni/actions";
|
import { addTaxonomyValue, removeTaxonomyValue } from "@/app/admin/impostazioni/actions";
|
||||||
|
|
||||||
@@ -19,11 +20,13 @@ const CATALOG_FIELDS: FieldDef[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function Section({
|
function Section({
|
||||||
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
fields,
|
fields,
|
||||||
pools,
|
pools,
|
||||||
}: {
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
fields: FieldDef[];
|
fields: FieldDef[];
|
||||||
@@ -31,9 +34,17 @@ function Section({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
|
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
|
||||||
<h2 className="text-base font-semibold text-[#1a1a1a] mb-1">{title}</h2>
|
<div className="flex items-start gap-3 mb-5">
|
||||||
<p className="text-xs text-[#71717a] mb-5">{subtitle}</p>
|
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[#1A463C]/8 text-[#1A463C]">
|
||||||
<div className="flex flex-wrap gap-6">
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold text-[#1a1a1a] leading-tight">{title}</h2>
|
||||||
|
<p className="text-xs text-[#71717a] mt-0.5">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{fields.map((f) => (
|
{fields.map((f) => (
|
||||||
<PoolManager
|
<PoolManager
|
||||||
key={f.id}
|
key={f.id}
|
||||||
@@ -50,19 +61,25 @@ function Section({
|
|||||||
|
|
||||||
export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) {
|
export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<Section
|
<Section
|
||||||
|
icon={<Tag className="h-[18px] w-[18px]" />}
|
||||||
title="Tassonomie Offerte"
|
title="Tassonomie Offerte"
|
||||||
subtitle="Valori disponibili nei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte."
|
subtitle="Valori dei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte."
|
||||||
fields={OFFER_FIELDS}
|
fields={OFFER_FIELDS}
|
||||||
pools={pools}
|
pools={pools}
|
||||||
/>
|
/>
|
||||||
<Section
|
<Section
|
||||||
|
icon={<BookOpen className="h-[18px] w-[18px]" />}
|
||||||
title="Tassonomie Catalogo"
|
title="Tassonomie Catalogo"
|
||||||
subtitle="Valori disponibili nei campi Fase, Offerta e Pacchetto del catalogo servizi."
|
subtitle="Valori dei campi Fase, Offerta e Pacchetto del catalogo servizi."
|
||||||
fields={CATALOG_FIELDS}
|
fields={CATALOG_FIELDS}
|
||||||
pools={pools}
|
pools={pools}
|
||||||
/>
|
/>
|
||||||
</>
|
<p className="text-xs text-[#a1a1aa] leading-relaxed">
|
||||||
|
I valori si sincronizzano ovunque: creandone uno dall'editor offerte o dal catalogo
|
||||||
|
comparirà qui automaticamente. Eliminando un valore da qui viene rimosso ovunque sia usato.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user