feat(ui): harvest, extraction, bottling and equipment pages with heading and way back
Same DetailHeader as colonies/queens: an h1, a 44 px back link, state as icon + word, the obvious next step as the one primary button (harvest: "Schleuderung erfassen"); a failed load offers a retry and a way back instead of a bare red line. Colonies of a harvest are tappable links; changing equipment condition confirms with a toast. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
d4b1addb7d
commit
86f9291ee3
4 files changed
+122
-123
No files matched your search
@@ -11,41 +11,36 @@ import { useAuth } from "@/lib/auth/auth-context";
|
|||||||
import { BottlingEditDialog } from "./bottling-edit-dialog";
|
import { BottlingEditDialog } from "./bottling-edit-dialog";
|
||||||
import { BottlingDeleteButton } from "./bottling-delete-button";
|
import { BottlingDeleteButton } from "./bottling-delete-button";
|
||||||
import { PrintLabelsCard } from "./print-labels-card";
|
import { PrintLabelsCard } from "./print-labels-card";
|
||||||
|
import { DetailHeader, DetailQueryFallback } from "@/components/detail-header";
|
||||||
|
|
||||||
|
const BACK = { backHref: "/bottling", backLabel: "Alle Abfüllungen" };
|
||||||
|
|
||||||
export default function BottlingBatchDetailPage() {
|
export default function BottlingBatchDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const { data: bottlingBatch, isLoading, isError } = useQuery({
|
const bottlingQuery = useQuery({
|
||||||
queryKey: ["bottling-batches", params.id],
|
queryKey: ["bottling-batches", params.id],
|
||||||
queryFn: () => fetchBottlingBatch(params.id),
|
queryFn: () => fetchBottlingBatch(params.id),
|
||||||
});
|
});
|
||||||
|
const bottlingBatch = bottlingQuery.data;
|
||||||
|
|
||||||
if (isLoading) {
|
if (bottlingQuery.isLoading || bottlingQuery.isError || !bottlingBatch) {
|
||||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
return <DetailQueryFallback query={bottlingQuery} what="Daten der Abfüllung" {...BACK} />;
|
||||||
}
|
|
||||||
if (isError || !bottlingBatch) {
|
|
||||||
return (
|
|
||||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
|
||||||
Abfüllung konnte nicht geladen werden.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
|
<DetailHeader
|
||||||
|
icon={Package}
|
||||||
|
title={`Abfüllung ${bottlingBatch.lotNumber}`}
|
||||||
|
subtitle={`${bottlingBatch.jarCount} Gläser à ${bottlingBatch.jarSize} g`}
|
||||||
|
{...BACK}
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2.5">
|
||||||
<div className="flex items-center gap-2.5">
|
<CardTitle>Angaben</CardTitle>
|
||||||
<span
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
className="flex size-8 items-center justify-center rounded-lg text-white shadow-[var(--shadow-soft)]"
|
|
||||||
style={{ background: "var(--gradient-honey)" }}
|
|
||||||
>
|
|
||||||
<Package className="size-4" />
|
|
||||||
</span>
|
|
||||||
<CardTitle className="font-mono">Abfüllung {bottlingBatch.lotNumber}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{hasPermission("bottling:update") ? (
|
{hasPermission("bottling:update") ? (
|
||||||
<BottlingEditDialog bottlingBatch={bottlingBatch} />
|
<BottlingEditDialog bottlingBatch={bottlingBatch} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
|
|||||||
import { EquipmentEditDialog } from "./equipment-edit-dialog";
|
import { EquipmentEditDialog } from "./equipment-edit-dialog";
|
||||||
import { EquipmentCondition } from "@/lib/equipment/types";
|
import { EquipmentCondition } from "@/lib/equipment/types";
|
||||||
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -18,55 +16,52 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { DetailHeader, DetailQueryFallback, DetailStatus } from "@/components/detail-header";
|
||||||
|
import { errorMessage } from "@/lib/forms/error-message";
|
||||||
|
|
||||||
|
const BACK = { backHref: "/equipment", backLabel: "Gesamte Ausrüstung" };
|
||||||
|
|
||||||
export default function EquipmentDetailPage() {
|
export default function EquipmentDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: equipment, isLoading, isError } = useQuery({
|
const equipmentQuery = useQuery({
|
||||||
queryKey: ["equipment", params.id],
|
queryKey: ["equipment", params.id],
|
||||||
queryFn: () => fetchEquipment(params.id),
|
queryFn: () => fetchEquipment(params.id),
|
||||||
});
|
});
|
||||||
|
const equipment = equipmentQuery.data;
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (condition: EquipmentCondition) =>
|
mutationFn: (condition: EquipmentCondition) =>
|
||||||
updateEquipment(params.id, { condition }),
|
updateEquipment(params.id, { condition }),
|
||||||
onSuccess: () => {
|
onSuccess: (_, condition) => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["equipment", params.id] });
|
void queryClient.invalidateQueries({ queryKey: ["equipment", params.id] });
|
||||||
void queryClient.invalidateQueries({ queryKey: ["equipment"] });
|
void queryClient.invalidateQueries({ queryKey: ["equipment"] });
|
||||||
|
toast.success(`Zustand: ${EQUIPMENT_CONDITION_LABELS[condition]}`);
|
||||||
},
|
},
|
||||||
onError: () => toast.error("Zustand konnte nicht gespeichert werden"),
|
onError: (error) => toast.error(errorMessage(error, "Zustand konnte nicht gespeichert werden")),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (equipmentQuery.isLoading || equipmentQuery.isError || !equipment) {
|
||||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
return <DetailQueryFallback query={equipmentQuery} what="Daten des Gegenstands" {...BACK} />;
|
||||||
}
|
|
||||||
if (isError || !equipment) {
|
|
||||||
return (
|
|
||||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
|
||||||
Gegenstand konnte nicht geladen werden.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
|
<DetailHeader
|
||||||
|
icon={Wrench}
|
||||||
|
title={equipment.name}
|
||||||
|
status={
|
||||||
|
<DetailStatus
|
||||||
|
status={equipment.condition}
|
||||||
|
label={EQUIPMENT_CONDITION_LABELS[equipment.condition]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...BACK}
|
||||||
|
/>
|
||||||
<Card className="max-w-lg">
|
<Card className="max-w-lg">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div className="flex items-center gap-2.5">
|
<CardTitle>Angaben</CardTitle>
|
||||||
<span
|
<EquipmentEditDialog equipment={equipment} />
|
||||||
className="flex size-8 items-center justify-center rounded-lg text-white shadow-[var(--shadow-soft)]"
|
|
||||||
style={{ background: "var(--gradient-sky)" }}
|
|
||||||
>
|
|
||||||
<Wrench className="size-4" />
|
|
||||||
</span>
|
|
||||||
<CardTitle>{equipment.name}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant={statusTone(equipment.condition)}>
|
|
||||||
{EQUIPMENT_CONDITION_LABELS[equipment.condition]}
|
|
||||||
</Badge>
|
|
||||||
<EquipmentEditDialog equipment={equipment} />
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
<p>
|
<p>
|
||||||
@@ -91,7 +86,7 @@ export default function EquipmentDetailPage() {
|
|||||||
value ? updateMutation.mutate(value as EquipmentCondition) : undefined
|
value ? updateMutation.mutate(value as EquipmentCondition) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-full sm:w-48" aria-label="Zustand ändern">
|
||||||
<SelectValue>{EQUIPMENT_CONDITION_LABELS[equipment.condition]}</SelectValue>
|
<SelectValue>{EQUIPMENT_CONDITION_LABELS[equipment.condition]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -7,45 +7,40 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { fetchExtraction } from "@/lib/extractions/api";
|
import { fetchExtraction } from "@/lib/extractions/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { DetailHeader, DetailQueryFallback } from "@/components/detail-header";
|
||||||
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
||||||
|
|
||||||
|
const BACK = { backHref: "/extractions", backLabel: "Alle Schleuderungen" };
|
||||||
|
|
||||||
export default function ExtractionDetailPage() {
|
export default function ExtractionDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const {
|
const extractionQuery = useQuery({
|
||||||
data: extraction,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["extractions", params.id],
|
queryKey: ["extractions", params.id],
|
||||||
queryFn: () => fetchExtraction(params.id),
|
queryFn: () => fetchExtraction(params.id),
|
||||||
});
|
});
|
||||||
|
const extraction = extractionQuery.data;
|
||||||
|
|
||||||
if (isLoading) {
|
if (extractionQuery.isLoading || extractionQuery.isError || !extraction) {
|
||||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
|
||||||
}
|
|
||||||
if (isError || !extraction) {
|
|
||||||
return (
|
return (
|
||||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
<DetailQueryFallback query={extractionQuery} what="Daten der Schleuderung" {...BACK} />
|
||||||
Schleuderung konnte nicht geladen werden.
|
|
||||||
</p>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
|
<DetailHeader
|
||||||
|
icon={FlaskConical}
|
||||||
|
title={`Schleuderung vom ${new Date(extraction.date).toLocaleDateString("de-DE")}`}
|
||||||
|
primaryAction={
|
||||||
|
<Button size="touch" nativeButton={false} render={<Link href="/honey-batches" />}>
|
||||||
|
Zu den Honigchargen
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{...BACK}
|
||||||
|
/>
|
||||||
<Card className="max-w-lg">
|
<Card className="max-w-lg">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
||||||
<div className="flex items-center gap-2.5">
|
<CardTitle>Angaben</CardTitle>
|
||||||
<span
|
|
||||||
className="flex size-8 items-center justify-center rounded-lg text-white shadow-[var(--shadow-soft)]"
|
|
||||||
style={{ background: "var(--gradient-honey)" }}
|
|
||||||
>
|
|
||||||
<FlaskConical className="size-4" />
|
|
||||||
</span>
|
|
||||||
<CardTitle>
|
|
||||||
Schleuderung vom {new Date(extraction.date).toLocaleDateString("de-DE")}
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
<ExtractionEditDialog extraction={extraction} />
|
<ExtractionEditDialog extraction={extraction} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
@@ -63,7 +58,10 @@ export default function ExtractionDetailPage() {
|
|||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
{extraction.harvests.map((entry) => (
|
{extraction.harvests.map((entry) => (
|
||||||
<li key={entry.id}>
|
<li key={entry.id}>
|
||||||
<Link href={`/harvests/${entry.harvestId}`} className="hover:underline">
|
<Link
|
||||||
|
href={`/harvests/${entry.harvestId}`}
|
||||||
|
className="inline-flex min-h-11 items-center hover:underline"
|
||||||
|
>
|
||||||
{new Date(entry.harvest.date).toLocaleDateString("de-DE")} ·{" "}
|
{new Date(entry.harvest.date).toLocaleDateString("de-DE")} ·{" "}
|
||||||
{entry.harvest.colonies.map((c) => c.colony.code).join(", ")}
|
{entry.harvest.colonies.map((c) => c.colony.code).join(", ")}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -71,14 +69,6 @@ export default function ExtractionDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
className="w-fit"
|
|
||||||
variant="outline"
|
|
||||||
nativeButton={false}
|
|
||||||
render={<Link href="/honey-batches" />}
|
|
||||||
>
|
|
||||||
Zu den Honigchargen
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -7,63 +7,82 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { fetchHarvest } from "@/lib/harvests/api";
|
import { fetchHarvest } from "@/lib/harvests/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { DetailHeader, DetailQueryFallback } from "@/components/detail-header";
|
||||||
import { HarvestEditDialog } from "./harvest-edit-dialog";
|
import { HarvestEditDialog } from "./harvest-edit-dialog";
|
||||||
|
|
||||||
|
const BACK = { backHref: "/harvests", backLabel: "Alle Ernten" };
|
||||||
|
|
||||||
export default function HarvestDetailPage() {
|
export default function HarvestDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const { data: harvest, isLoading, isError } = useQuery({
|
const harvestQuery = useQuery({
|
||||||
queryKey: ["harvests", params.id],
|
queryKey: ["harvests", params.id],
|
||||||
queryFn: () => fetchHarvest(params.id),
|
queryFn: () => fetchHarvest(params.id),
|
||||||
});
|
});
|
||||||
|
const harvest = harvestQuery.data;
|
||||||
|
|
||||||
if (isLoading) {
|
if (harvestQuery.isLoading || harvestQuery.isError || !harvest) {
|
||||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
return <DetailQueryFallback query={harvestQuery} what="Daten der Ernte" {...BACK} />;
|
||||||
}
|
|
||||||
if (isError || !harvest) {
|
|
||||||
return (
|
|
||||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
|
||||||
Ernte konnte nicht geladen werden.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
<Card className="max-w-lg">
|
<DetailHeader
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
icon={Boxes}
|
||||||
<div className="flex items-center gap-2.5">
|
title={`Ernte vom ${new Date(harvest.date).toLocaleDateString("de-DE")}`}
|
||||||
<span
|
subtitle={harvest.honeyType ?? undefined}
|
||||||
className="flex size-8 items-center justify-center rounded-lg text-white shadow-[var(--shadow-soft)]"
|
primaryAction={
|
||||||
style={{ background: "var(--gradient-honey)" }}
|
|
||||||
>
|
|
||||||
<Boxes className="size-4" />
|
|
||||||
</span>
|
|
||||||
<CardTitle>Ernte vom {new Date(harvest.date).toLocaleDateString("de-DE")}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<HarvestEditDialog harvest={harvest} />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
|
||||||
{harvest.honeyType ? <p>Honigsorte: {harvest.honeyType}</p> : null}
|
|
||||||
{harvest.netWeight !== null ? <p>Nettogewicht: {harvest.netWeight} kg</p> : null}
|
|
||||||
{harvest.honeySuperCount !== null ? (
|
|
||||||
<p>Honigräume: {harvest.honeySuperCount}</p>
|
|
||||||
) : null}
|
|
||||||
{harvest.notes ? <p>Notizen: {harvest.notes}</p> : null}
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Beteiligte Völker</p>
|
|
||||||
<ul className="list-disc pl-5">
|
|
||||||
{harvest.colonies.map((entry) => (
|
|
||||||
<li key={entry.id}>{entry.colony.code}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
className="w-fit"
|
size="touch"
|
||||||
nativeButton={false}
|
nativeButton={false}
|
||||||
render={<Link href={`/extractions/new?harvestId=${harvest.id}`} />}
|
render={<Link href={`/extractions/new?harvestId=${harvest.id}`} />}
|
||||||
>
|
>
|
||||||
Zur Schleuderung
|
Schleuderung erfassen
|
||||||
</Button>
|
</Button>
|
||||||
|
}
|
||||||
|
{...BACK}
|
||||||
|
/>
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
||||||
|
<CardTitle>Angaben</CardTitle>
|
||||||
|
<HarvestEditDialog harvest={harvest} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
|
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||||
|
{harvest.honeyType ? (
|
||||||
|
<>
|
||||||
|
<dt className="text-muted-foreground">Honigsorte</dt>
|
||||||
|
<dd>{harvest.honeyType}</dd>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{harvest.netWeight !== null ? (
|
||||||
|
<>
|
||||||
|
<dt className="text-muted-foreground">Nettogewicht</dt>
|
||||||
|
<dd>{harvest.netWeight} kg</dd>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{harvest.honeySuperCount !== null ? (
|
||||||
|
<>
|
||||||
|
<dt className="text-muted-foreground">Honigräume</dt>
|
||||||
|
<dd>{harvest.honeySuperCount}</dd>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</dl>
|
||||||
|
{harvest.notes ? <p>Notizen: {harvest.notes}</p> : null}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Beteiligte Völker</p>
|
||||||
|
<ul className="flex flex-wrap gap-2 pt-1">
|
||||||
|
{harvest.colonies.map((entry) => (
|
||||||
|
<li key={entry.id}>
|
||||||
|
<Link
|
||||||
|
href={`/colonies/${entry.colony.id}`}
|
||||||
|
className="inline-flex min-h-11 items-center rounded-lg border px-3 hover:bg-muted"
|
||||||
|
>
|
||||||
|
{entry.colony.code}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in new issue
Block a user