import { useNavigate } from "react-router-dom";
import {
Page,
Layout,
Card,
ResourceList,
ResourceItem,
Stack,
Text,
Badge,
Button,
EmptyState,
Thumbnail,
SkeletonBodyText,
} from "@shopify/polaris";
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
import { useQuery, useQueryClient } from "react-query";
export default function GroupsPage() {
const navigate = useNavigate();
const qc = useQueryClient();
const app = useAppBridge();
const { data, isLoading } = useQuery({
queryKey: ["groups"],
queryFn: async () => (await fetch("/api/groups")).json(),
refetchOnWindowFocus: false,
});
const groups = data?.groups ?? [];
const remove = async (id) => {
await fetch(`/api/groups/${id}`, { method: "DELETE" });
// Safety net: clear any metafields left behind if the targeted delete missed
// a product (keeps the storefront in sync automatically after deletes).
await fetch("/api/groups/cleanup-orphans", { method: "POST" }).catch(() => {});
qc.invalidateQueries(["groups"]);
};
const togglePublish = async (g) => {
await fetch(`/api/groups/${g.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: g.status === "published" ? "draft" : "published",
}),
});
qc.invalidateQueries(["groups"]);
};
const resync = async (g) => {
const res = await fetch(`/api/groups/${g.id}/resync`, { method: "POST" });
const json = await res.json();
const ok = !!json?.readback?.value;
app.toast.show(
ok
? "Re-synced — storefront metafield is set"
: "Re-synced, but metafield not readable yet",
{ isError: !ok }
);
};
const cleanupOrphans = async () => {
const res = await fetch("/api/groups/cleanup-orphans", { method: "POST" });
const json = await res.json();
app.toast.show(
`Cleaned ${json.cleared} orphaned product${json.cleared === 1 ? "" : "s"} from the storefront`
);
};
return (
<Page>
<TitleBar
title="Product groups"
breadcrumbs={[{ content: "Linked Products", url: "/" }]}
secondaryActions={[
{ content: "Clean up storefront", onAction: cleanupOrphans },
]}
primaryAction={{
content: "Create group",
onAction: () => navigate("/groups/new"),
}}
/>
<Layout>
<Layout.Section>
{isLoading ? (
<Card sectioned>
<SkeletonBodyText lines={6} />
</Card>
) : groups.length === 0 ? (
<Card sectioned>
<EmptyState
heading="Group related products as swatches"
action={{
content: "Create group",
onAction: () => navigate("/groups/new"),
}}
image="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
>
<p>
Link products that are colour/variant siblings (e.g. the same
shirt sold as separate products) so each shows the whole
group's swatches. Clicking a swatch goes to that product.
</p>
</EmptyState>
</Card>
) : (
<Card>
<ResourceList
resourceName={{ singular: "group", plural: "groups" }}
items={groups}
renderItem={(g) => (
<ResourceItem
id={String(g.id)}
onClick={() => navigate(`/groups/${g.id}`)}
>
<Stack alignment="center">
<Stack.Item fill>
<Stack spacing="tight" alignment="center">
<Text variant="bodyMd" fontWeight="bold" as="h3">
{g.name}
</Text>
<Badge status={g.status === "published" ? "success" : "attention"}>
{g.status === "published" ? "Published" : "Draft"}
</Badge>
{g.source === "auto" && <Badge>Auto</Badge>}
</Stack>
<Stack spacing="extraTight">
{(g.members || []).slice(0, 6).map((m) => (
<Thumbnail
key={m.product_id}
size="small"
source={m.image || ""}
alt={m.product_title}
/>
))}
</Stack>
</Stack.Item>
<Badge>{`${(g.members || []).length} products`}</Badge>
{g.status === "published" && (
<Button
onClick={(e) => {
e.stopPropagation();
resync(g);
}}
>
Re-sync
</Button>
)}
<Button
primary={g.status !== "published"}
onClick={(e) => {
e.stopPropagation();
togglePublish(g);
}}
>
{g.status === "published" ? "Unpublish" : "Publish"}
</Button>
<Button
destructive
outline
onClick={(e) => {
e.stopPropagation();
remove(g.id);
}}
>
Delete
</Button>
</Stack>
</ResourceItem>
)}
/>
</Card>
)}
</Layout.Section>
</Layout>
</Page>
);
}