Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"save": "Save",
"saving": "Saving…",
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"retry": "Retry",
"error": "Error",
Expand Down
1 change: 1 addition & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"save": "Enregistrer",
"saving": "Enregistrement…",
"add": "Ajouter",
"edit": "Modifier",
"delete": "Supprimer",
"retry": "Réessayer",
"error": "Erreur",
Expand Down
42 changes: 33 additions & 9 deletions src/app/(app)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function AdminPage() {
const [references, setReferences] = useState<References | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState("cards");

const tabs = useMemo(
() => [
Expand Down Expand Up @@ -108,24 +109,47 @@ export default function AdminPage() {
}

return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("admin.title")}</h2>
<p className="text-muted-foreground mt-1">
<div className="min-w-0 space-y-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="min-w-0">
<h2 className="break-words text-2xl font-bold tracking-tight">
{t("admin.title")}
</h2>
<p className="mt-1 text-sm text-muted-foreground sm:text-base">
{t("admin.subtitle", { count: cards.length })}
</p>
</div>
<Button variant="outline" size="sm" onClick={() => void loadData()}>
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
onClick={() => void loadData()}
>
<RefreshCw className="h-4 w-4 mr-1" />
{t("admin.reload")}
</Button>
</div>

<Tabs defaultValue="cards">
<TabsList className="flex h-auto w-full flex-wrap justify-start">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<select
value={activeTab}
onChange={(event) => setActiveTab(event.target.value)}
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm md:hidden"
>
{tabs.map((tab) => (
<option key={tab.value} value={tab.value}>
{tab.label}
</option>
))}
</select>

<TabsList className="hidden !h-auto w-full max-w-full justify-start p-1 md:inline-flex">
{tabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
<TabsTrigger
key={tab.value}
value={tab.value}
className="h-8 flex-none px-3 text-xs sm:text-sm"
>
{tab.label}
</TabsTrigger>
))}
Expand Down
10 changes: 6 additions & 4 deletions src/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ export default async function DashboardPage() {
};

return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">
<div className="min-w-0 space-y-6 sm:space-y-8">
<div className="min-w-0">
<h2 className="text-2xl font-bold tracking-tight break-words">
{t("dashboard.title")}
</h2>
<p className="text-muted-foreground mt-1">{t("dashboard.subtitle")}</p>
<p className="mt-1 text-sm text-muted-foreground sm:text-base">
{t("dashboard.subtitle")}
</p>
</div>

<StatsCards
Expand Down
149 changes: 145 additions & 4 deletions src/components/admin/admin-cards-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,14 @@ export function AdminCardsSection({
}

return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 space-y-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<p className="text-sm text-muted-foreground">
{t("admin.cards.intro")}
</p>
<Button
size="sm"
className="w-full sm:w-auto"
onClick={() => {
setEditingCard(null);
setSaveError(null);
Expand Down Expand Up @@ -413,7 +414,147 @@ export function AdminCardsSection({
/>
</div>

<div className="rounded-md border border-border overflow-auto">
<div className="grid gap-2 md:hidden">
<ColumnFilterCombobox
value={playerColumnFilter}
onChange={(nextValue) => {
setPlayerColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterPlayerTeam")}
suggestions={columnSuggestions.players}
className="h-9 text-xs"
/>
<ColumnFilterCombobox
value={teamColumnFilter}
onChange={(nextValue) => {
setTeamColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterTeam")}
suggestions={columnSuggestions.teams}
className="h-9 text-xs"
/>
<div className="grid grid-cols-2 gap-2">
<ColumnFilterCombobox
value={yearColumnFilter}
onChange={(nextValue) => {
setYearColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterYear")}
suggestions={columnSuggestions.years}
className="h-9 text-xs"
/>
<ColumnFilterCombobox
value={tagsColumnFilter}
onChange={(nextValue) => {
setTagsColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterTags")}
suggestions={columnSuggestions.tags}
className="h-9 text-xs"
/>
</div>
<ColumnFilterCombobox
value={brandColumnFilter}
onChange={(nextValue) => {
setBrandColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterBrand")}
suggestions={columnSuggestions.brands}
className="h-9 text-xs"
/>
<ColumnFilterCombobox
value={setColumnFilter}
onChange={(nextValue) => {
setSetColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterSet")}
suggestions={columnSuggestions.sets}
className="h-9 text-xs"
/>
<ColumnFilterCombobox
value={variationColumnFilter}
onChange={(nextValue) => {
setVariationColumnFilter(nextValue);
setPage(0);
}}
placeholder={t("admin.cards.filterVariation")}
suggestions={columnSuggestions.variations}
className="h-9 text-xs"
/>
</div>

<div className="space-y-2 md:hidden">
{paged.length ? (
paged.map((card) => {
const meta = [card.team, card.year].filter(Boolean).join(" · ");
const catalog = [card.brand, card.set].filter(Boolean).join(" · ");

return (
<article
key={card.id}
className="rounded-lg border border-border bg-card p-3 shadow-sm"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-medium leading-tight">
{card.player}
</p>
<p className="mt-1 text-xs text-muted-foreground">{meta}</p>
</div>
<CardBadges card={card} labels={badgeLabels} />
</div>

<div className="mt-2 space-y-0.5 text-sm">
<p className="truncate text-muted-foreground">{catalog}</p>
<p className="truncate">{card.variation}</p>
</div>

<div className="mt-2 flex justify-end gap-1">
<Button
variant="ghost"
size="sm"
aria-label={t("admin.cards.editCard", {
player: card.player,
})}
onClick={() => {
setEditingCard(card);
setSaveError(null);
setFormOpen(true);
}}
>
<Pencil className="h-3.5 w-3.5 mr-1" />
{t("common.edit")}
</Button>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
aria-label={t("admin.cards.deleteCard", {
player: card.player,
})}
onClick={() => setDeleteTarget(card)}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
{t("common.delete")}
</Button>
</div>
</article>
);
})
) : (
<div className="rounded-lg border border-border p-6 text-center text-sm text-muted-foreground">
{t("cards.noneFound")}
</div>
)}
</div>

<div className="hidden rounded-md border border-border md:block md:overflow-auto">
<Table>
<TableHeader>
<TableRow>
Expand Down Expand Up @@ -567,7 +708,7 @@ export function AdminCardsSection({
</div>

{totalPages > 1 && (
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
{t("admin.cards.pageInfo", {
page: page + 1,
Expand Down
32 changes: 26 additions & 6 deletions src/components/admin/admin-catalog-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export function AdminCatalogSection({
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState("brands");

const setsForBrand = useMemo(
() => setsLinkedToBrand(references, brandForSet),
Expand Down Expand Up @@ -219,18 +220,34 @@ export function AdminCatalogSection({
<div className="space-y-6">
<p className="text-sm text-muted-foreground">{t("admin.catalog.intro")}</p>

<Tabs defaultValue="brands">
<TabsList>
<TabsTrigger value="brands">{t("admin.catalog.brands")}</TabsTrigger>
<TabsTrigger value="sets">{t("admin.catalog.sets")}</TabsTrigger>
<TabsTrigger value="variations">{t("admin.catalog.variations")}</TabsTrigger>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<select
value={activeTab}
onChange={(event) => setActiveTab(event.target.value)}
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm md:hidden"
>
<option value="brands">{t("admin.catalog.brands")}</option>
<option value="sets">{t("admin.catalog.sets")}</option>
<option value="variations">{t("admin.catalog.variations")}</option>
</select>

<TabsList className="hidden !h-auto w-full max-w-full justify-start p-1 md:inline-flex">
<TabsTrigger value="brands" className="h-8 flex-none px-3">
{t("admin.catalog.brands")}
</TabsTrigger>
<TabsTrigger value="sets" className="h-8 flex-none px-3">
{t("admin.catalog.sets")}
</TabsTrigger>
<TabsTrigger value="variations" className="h-8 flex-none px-3">
{t("admin.catalog.variations")}
</TabsTrigger>
</TabsList>

<TabsContent value="brands" className="space-y-4 pt-4">
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-3 rounded-lg border border-border p-4">
<Label htmlFor="admin-brand-name">{t("admin.players.unitAdd")}</Label>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
id="admin-brand-name"
value={brand}
Expand All @@ -240,6 +257,7 @@ export function AdminCatalogSection({
/>
<Button
type="button"
className="w-full sm:w-auto"
disabled={loading}
onClick={() =>
void run(async () => {
Expand Down Expand Up @@ -293,6 +311,7 @@ export function AdminCatalogSection({
/>
<Button
type="button"
className="w-full sm:w-auto"
disabled={loading}
onClick={() =>
void run(async () => {
Expand Down Expand Up @@ -354,6 +373,7 @@ export function AdminCatalogSection({
/>
<Button
type="button"
className="w-full sm:w-auto"
disabled={loading}
onClick={() =>
void run(async () => {
Expand Down
9 changes: 7 additions & 2 deletions src/components/admin/admin-clubs-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,20 @@ export function AdminClubsSection({
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-3 rounded-lg border border-border p-4">
<Label htmlFor="admin-club-name">{t("admin.players.unitAdd")}</Label>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
id="admin-club-name"
value={team}
onChange={(e) => setTeam(e.target.value)}
placeholder={t("admin.clubs.placeholder")}
disabled={loading}
/>
<Button type="button" disabled={loading} onClick={() => void handleAddTeam()}>
<Button
type="button"
className="w-full sm:w-auto"
disabled={loading}
onClick={() => void handleAddTeam()}
>
{t("common.add")}
</Button>
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/components/admin/admin-players-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,20 @@ export function AdminPlayersSection({
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-3 rounded-lg border border-border p-4">
<Label htmlFor="admin-player-name">{t("admin.players.unitAdd")}</Label>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
id="admin-player-name"
value={player}
onChange={(e) => setPlayer(e.target.value)}
placeholder={t("admin.players.placeholder")}
disabled={loading}
/>
<Button type="button" disabled={loading} onClick={() => void handleAddPlayer()}>
<Button
type="button"
className="w-full sm:w-auto"
disabled={loading}
onClick={() => void handleAddPlayer()}
>
{t("common.add")}
</Button>
</div>
Expand Down
Loading