mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(admin): migrate section cards onto the shared card primitives
The admin tree hand-rolled its card surfaces and stepped padding at sm: instead of md:, so it read denser than every other route on a tablet. Move the sections onto SettingsCard/SettingsCardHeader and the mini-rows onto ListRow; no behaviour, data, or copy changes.
This commit is contained in:
1 parent
c6dea5eb56
commit
86d80d2c04
23 files changed
+200
-183
No files matched your search
@@ -115,11 +115,12 @@ describe("ApiTokenOverviewSection — responsive", () => {
|
||||
expect(html).toContain("All");
|
||||
});
|
||||
|
||||
it("uses a smaller card padding on mobile (p-4 sm:p-6)", () => {
|
||||
it("uses a smaller card padding on mobile (p-4 md:p-6)", () => {
|
||||
const html = render();
|
||||
// The card root carries the responsive padding to leave more room
|
||||
// for content within a 393 CSS-px viewport.
|
||||
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*sm:p-6/);
|
||||
// The card root carries the shared SettingsCard padding contract
|
||||
// (`p-4 md:p-6`) to leave more room for content within a 393 CSS-px
|
||||
// viewport while stepping up only from `md`, not `sm`.
|
||||
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*md:p-6/);
|
||||
});
|
||||
|
||||
it("desktop date cells drop whitespace-nowrap (v1.4.22 C2 5th-attempt fix)", () => {
|
||||
|
||||
@@ -115,9 +115,9 @@ describe("UserManagementSection — mobile responsive", () => {
|
||||
expect(mobileSlice).toContain("Force logout");
|
||||
});
|
||||
|
||||
it("uses smaller card padding on mobile (p-4 sm:p-6)", () => {
|
||||
it("uses smaller card padding on mobile (p-4 md:p-6)", () => {
|
||||
const html = render();
|
||||
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*sm:p-6/);
|
||||
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*md:p-6/);
|
||||
});
|
||||
|
||||
it("wraps the filter pills below the title on narrow viewports", () => {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Sparkles, Loader2 } from "lucide-react";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -75,7 +76,7 @@ export function AiQualitySection() {
|
||||
// resolves. Mirrors <CoachFeedbackSection> (header outside the
|
||||
// fetch-state branch).
|
||||
return (
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={Sparkles}
|
||||
title={t("admin.aiQuality.title")}
|
||||
@@ -178,6 +179,6 @@ export function AiQualitySection() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -92,10 +93,7 @@ export function AiServerKeySection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="admin-ai-server-key"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCard data-slot="admin-ai-server-key">
|
||||
<SettingsCardHeader
|
||||
icon={KeyRound}
|
||||
title={t("admin.aiServerKey.title")}
|
||||
@@ -183,6 +181,6 @@ export function AiServerKeySection() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { formatDate, formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ListRow } from "@/components/ui/list-row";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { type ApiTokenInfo } from "./_shared";
|
||||
@@ -157,7 +159,7 @@ export function ApiTokenOverviewSection() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border overflow-hidden rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="overflow-hidden">
|
||||
<SettingsCardHeader
|
||||
icon={Key}
|
||||
title={t("admin.apiTokens")}
|
||||
@@ -293,55 +295,60 @@ export function ApiTokenOverviewSection() {
|
||||
>
|
||||
{tokens.map((token) => {
|
||||
return (
|
||||
<li
|
||||
<ListRow
|
||||
asChild
|
||||
key={token.id}
|
||||
className="bg-muted/30 border-border overflow-hidden rounded-lg border p-3"
|
||||
className="bg-muted/30 border-border overflow-hidden"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<li>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<TruncatedCell
|
||||
value={formatTokenName(token.name)}
|
||||
className="min-w-0 flex-1 font-medium"
|
||||
/>
|
||||
<TokenStatusBadge token={token} size="xs" />
|
||||
</div>
|
||||
<TruncatedCell
|
||||
value={formatTokenName(token.name)}
|
||||
className="min-w-0 flex-1 font-medium"
|
||||
value={token.user.username}
|
||||
className="text-muted-foreground text-xs"
|
||||
/>
|
||||
<TokenStatusBadge token={token} size="xs" />
|
||||
</div>
|
||||
<TruncatedCell
|
||||
value={token.user.username}
|
||||
className="text-muted-foreground text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{token.permissions.map((p) => (
|
||||
<Badge
|
||||
key={p}
|
||||
variant="secondary"
|
||||
className="max-w-full truncate text-[10px]"
|
||||
title={
|
||||
p === "*" ? t("admin.tokenPermissionAllTooltip") : p
|
||||
}
|
||||
>
|
||||
{p === "*" ? t("admin.tokenPermissionAll") : p}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-[11px]">
|
||||
{t("admin.tokenLastUsed")}:{" "}
|
||||
{token.lastUsedAt
|
||||
? formatDateTime(token.lastUsedAt)
|
||||
: t("admin.tokenNeverUsed")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.tokenCreated")}: {formatDate(token.createdAt)}
|
||||
</p>
|
||||
</li>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{token.permissions.map((p) => (
|
||||
<Badge
|
||||
key={p}
|
||||
variant="secondary"
|
||||
className="max-w-full truncate text-[10px]"
|
||||
title={
|
||||
p === "*"
|
||||
? t("admin.tokenPermissionAllTooltip")
|
||||
: p
|
||||
}
|
||||
>
|
||||
{p === "*" ? t("admin.tokenPermissionAll") : p}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-[11px]">
|
||||
{t("admin.tokenLastUsed")}:{" "}
|
||||
{token.lastUsedAt
|
||||
? formatDateTime(token.lastUsedAt)
|
||||
: t("admin.tokenNeverUsed")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.tokenCreated")}: {formatDate(token.createdAt)}
|
||||
</p>
|
||||
</li>
|
||||
</ListRow>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -115,7 +116,7 @@ export function AppLogPreviewSection() {
|
||||
const events = data?.events ?? [];
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={FileText}
|
||||
title={t("admin.section.app-logs.title")}
|
||||
@@ -303,6 +304,6 @@ export function AppLogPreviewSection() {
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -99,7 +100,7 @@ export function AssistantSection() {
|
||||
const disabledSubs = !masterOn || mutation.isPending;
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Sparkles}
|
||||
title={t("admin.assistant.title")}
|
||||
@@ -166,6 +167,6 @@ export function AssistantSection() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,9 @@ import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ListRow } from "@/components/ui/list-row";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { BackupRow, BackupsList } from "@/types/backups";
|
||||
@@ -332,7 +334,7 @@ export function BackupsSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
{/* v1.18.1 E3 — the snapshot count leads (numbers first), the
|
||||
"Run now" button follows, and the explainer + docs link move
|
||||
into the header's description slot. Previously the description
|
||||
@@ -550,48 +552,51 @@ export function BackupsSection() {
|
||||
data-testid="admin-backups-mobile-list"
|
||||
>
|
||||
{rows.map((row) => (
|
||||
<li
|
||||
<ListRow
|
||||
asChild
|
||||
key={row.id}
|
||||
className="bg-muted/30 border-border rounded-lg border p-3"
|
||||
className="bg-muted/30 border-border"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium">{row.username}</span>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{formatBackupType(row.type, t)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.section.backups.colSize")}:{" "}
|
||||
{formatBytes(row.sizeBytes, fmt)} ·{" "}
|
||||
{fmt.dateTime(row.createdAt)}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={downloadingId === row.id}
|
||||
onClick={() => handleDownload(row)}
|
||||
aria-label={t("admin.section.backups.downloadAria", {
|
||||
username: row.username,
|
||||
})}
|
||||
className="min-h-11"
|
||||
>
|
||||
{downloadingId === row.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("admin.section.backups.download")}
|
||||
</Button>
|
||||
<RestoreRowDialog
|
||||
row={row}
|
||||
pending={
|
||||
restore.isPending && restore.variables?.id === row.id
|
||||
}
|
||||
onConfirm={() => restore.mutate(row)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium">{row.username}</span>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{formatBackupType(row.type, t)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.section.backups.colSize")}:{" "}
|
||||
{formatBytes(row.sizeBytes, fmt)} ·{" "}
|
||||
{fmt.dateTime(row.createdAt)}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={downloadingId === row.id}
|
||||
onClick={() => handleDownload(row)}
|
||||
aria-label={t("admin.section.backups.downloadAria", {
|
||||
username: row.username,
|
||||
})}
|
||||
className="min-h-11"
|
||||
>
|
||||
{downloadingId === row.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("admin.section.backups.download")}
|
||||
</Button>
|
||||
<RestoreRowDialog
|
||||
row={row}
|
||||
pending={
|
||||
restore.isPending && restore.variables?.id === row.id
|
||||
}
|
||||
onConfirm={() => restore.mutate(row)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ListRow>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -602,6 +607,6 @@ export function BackupsSection() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, Sparkles } from "lucide-react";
|
||||
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -70,7 +71,7 @@ export function CoachFeedbackSection() {
|
||||
// /admin/coach-feedback. Mirrors the canonical structure used by
|
||||
// <SystemStatusSection> (header outside the fetch-state branch).
|
||||
return (
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="space-y-4">
|
||||
<SettingsCardHeader
|
||||
icon={Sparkles}
|
||||
title={t("admin.coachFeedback.title")}
|
||||
@@ -167,6 +168,6 @@ export function CoachFeedbackSection() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Settings } from "lucide-react";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
@@ -26,7 +27,7 @@ export function GeneralSettingsSection() {
|
||||
const currentDefaultTz = settings?.defaultUserTimezone ?? "";
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Settings}
|
||||
title={t("admin.appSettings")}
|
||||
@@ -117,6 +118,6 @@ export function GeneralSettingsSection() {
|
||||
</NativeSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -69,7 +70,7 @@ export function GlitchtipSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={AlertTriangle}
|
||||
title={t("admin.glitchtipTitle")}
|
||||
@@ -152,6 +153,6 @@ export function GlitchtipSection() {
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import { useMemo } from "react";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
|
||||
interface HostMetricsApiSample {
|
||||
@@ -156,10 +157,7 @@ export function HostMetricsChart() {
|
||||
// jump when data lands. Animated only when motion isn't reduced.
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-host-metrics-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 md:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
|
||||
{t("admin.hostMetrics.title")}
|
||||
@@ -169,16 +167,13 @@ export function HostMetricsChart() {
|
||||
</span>
|
||||
</div>
|
||||
<Skeleton className="bg-muted/40 h-[180px] w-full" />
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-host-metrics-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 md:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
|
||||
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
|
||||
{t("admin.hostMetrics.title")}
|
||||
</h2>
|
||||
@@ -188,7 +183,7 @@ export function HostMetricsChart() {
|
||||
>
|
||||
{t("admin.hostMetrics.loadError")}
|
||||
</p>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,10 +192,7 @@ export function HostMetricsChart() {
|
||||
// instead of an empty plot.
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-host-metrics-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 md:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
|
||||
{t("admin.hostMetrics.title")}
|
||||
@@ -212,15 +204,12 @@ export function HostMetricsChart() {
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("admin.hostMetrics.empty")}
|
||||
</p>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-host-metrics-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 md:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
|
||||
{t("admin.hostMetrics.title")}
|
||||
@@ -354,6 +343,6 @@ export function HostMetricsChart() {
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -219,7 +220,7 @@ export function LoginOverviewSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={ScrollText}
|
||||
title={t("admin.loginOverview")}
|
||||
@@ -580,6 +581,6 @@ export function LoginOverviewSection() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ToggleLeft } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -87,7 +88,7 @@ export function ModuleAvailabilitySection() {
|
||||
const availability = data?.availability;
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={ToggleLeft}
|
||||
title={t("admin.modules.title")}
|
||||
@@ -119,6 +120,6 @@ export function ModuleAvailabilitySection() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -53,10 +54,7 @@ export function RecentAuditPreview() {
|
||||
const entries = data?.entries ?? [];
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-overview-audit-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-overview-audit-heading">
|
||||
<SettingsCardHeader
|
||||
icon={ScrollText}
|
||||
titleId="admin-overview-audit-heading"
|
||||
@@ -145,6 +143,6 @@ export function RecentAuditPreview() {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -53,7 +54,7 @@ function NotificationHealthPanel() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border mt-6 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard className="mt-6">
|
||||
<SettingsCardHeader
|
||||
icon={HeartPulse}
|
||||
title={t("admin.notificationHealth.title")}
|
||||
@@ -110,7 +111,7 @@ function NotificationHealthPanel() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ export function RemindersSection() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
{/* v1.4.22 D / F-58 — icon swap so the section header matches
|
||||
the admin-shell nav tile (Bell). Clock now lives on the
|
||||
timing chips inline instead of competing as the page icon. */}
|
||||
@@ -404,7 +405,7 @@ export function RemindersSection() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
<NotificationHealthPanel />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, Globe, Key, MessageCircle, Smile } from "lucide-react";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
|
||||
@@ -11,7 +12,7 @@ export function ServicesSection() {
|
||||
const updateSettings = useUpdateSettings();
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Globe}
|
||||
title={t("admin.servicesGlobal")}
|
||||
@@ -69,6 +70,6 @@ export function ServicesSection() {
|
||||
disabled={updateSettings.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
Key,
|
||||
Loader2,
|
||||
Map,
|
||||
RotateCw,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
||||
@@ -51,7 +52,7 @@ export function SystemStatusSection() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<HostMetricsChart />
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Server}
|
||||
title={t("admin.systemStatus")}
|
||||
@@ -205,7 +206,7 @@ export function SystemStatusSection() {
|
||||
{isFetching ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<RotateCw className="h-3.5 w-3.5" />
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("admin.systemStatusRetry")}
|
||||
</Button>
|
||||
@@ -218,7 +219,7 @@ export function SystemStatusSection() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
||||
@@ -33,9 +34,9 @@ export function SystemStatusSummary() {
|
||||
const { data: version } = usePublicVersion();
|
||||
|
||||
return (
|
||||
<section
|
||||
<SettingsCard
|
||||
as="section"
|
||||
aria-labelledby="admin-overview-snapshot-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={Server}
|
||||
@@ -136,6 +137,6 @@ export function SystemStatusSummary() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Activity, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -72,7 +73,7 @@ export function UmamiSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Activity}
|
||||
title={t("admin.umamiTitle")}
|
||||
@@ -153,6 +154,6 @@ export function UmamiSection() {
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,9 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ListRow } from "@/components/ui/list-row";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { type AdminUser, PasswordInput } from "./_shared";
|
||||
@@ -253,7 +255,7 @@ export function UserManagementSection() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Users}
|
||||
title={t("admin.userManagement")}
|
||||
@@ -385,36 +387,41 @@ export function UserManagementSection() {
|
||||
data-testid="admin-users-mobile-list"
|
||||
>
|
||||
{filteredUsers.map((u) => (
|
||||
<li
|
||||
<ListRow
|
||||
asChild
|
||||
key={u.id}
|
||||
className="bg-muted/30 border-border rounded-lg border p-3"
|
||||
className="bg-muted/30 border-border"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium">
|
||||
{u.username}
|
||||
</span>
|
||||
<Badge
|
||||
variant={u.role === "ADMIN" ? "default" : "secondary"}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{u.role}
|
||||
</Badge>
|
||||
<li>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium">
|
||||
{u.username}
|
||||
</span>
|
||||
<Badge
|
||||
variant={
|
||||
u.role === "ADMIN" ? "default" : "secondary"
|
||||
}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{u.role}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{u.email || "—"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.userPasskeys")}: {u.passkeyCount} ·{" "}
|
||||
{formatDate(u.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{u.email || "—"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("admin.userPasskeys")}: {u.passkeyCount} ·{" "}
|
||||
{formatDate(u.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1">
|
||||
{renderUserActions(u)}
|
||||
</div>
|
||||
</li>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1">
|
||||
{renderUserActions(u)}
|
||||
</div>
|
||||
</li>
|
||||
</ListRow>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
@@ -594,6 +601,6 @@ export function UserManagementSection() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -82,10 +83,7 @@ export function VersionTileSection() {
|
||||
const sha = shortSha(version?.buildSha ?? null);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="admin-overview-version-heading"
|
||||
className="bg-card border-border rounded-xl border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCard as="section" aria-labelledby="admin-overview-version-heading">
|
||||
<SettingsCardHeader
|
||||
icon={Tag}
|
||||
titleId="admin-overview-version-heading"
|
||||
@@ -133,6 +131,6 @@ export function VersionTileSection() {
|
||||
value={version?.builtAt ? formatDateTime(version.builtAt) : "—"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { BellRing, KeyRound, Loader2 } from "lucide-react";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -119,7 +120,7 @@ export function WebPushVapidSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={BellRing}
|
||||
title={t("admin.webPushVapidTitle")}
|
||||
@@ -242,6 +243,6 @@ export function WebPushVapidSection() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user