import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Card,
TextField,
Select,
Stack,
Button,
Thumbnail,
Text,
Banner,
Layout,
} from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";
import { ProductMultiSelect } from "./ProductMultiSelect";
import { autoColorGuess } from "../utils/autoColor";
function memberFromProduct(p) {
return {
productId: p.id,
productTitle: p.title,
handle: p.handle,
image: p.image || "",
// Default the swatch label to the product title; merchant can shorten it.
swatchValue: p.title,
swatchColor: autoColorGuess(p.title) || "",
};
}
function fromGroup(group) {
if (!group)
return { name: "", label: "Color", style: "color", status: "published", members: [] };
return {
name: group.name,
label: group.label,
style: group.style,
status: group.status || "published",
members: (group.members || []).map((m) => ({
productId: m.product_id,
productTitle: m.product_title,
handle: m.handle,
image: m.image,
swatchValue: m.swatch_value,
swatchColor: m.swatch_color,
})),
};
}
export function GroupEditor({ existing }) {
const navigate = useNavigate();
const app = useAppBridge();
const [form, setForm] = useState(fromGroup(existing));
const [errors, setErrors] = useState([]);
const [saving, setSaving] = useState(false);
const selectedIds = form.members.map((m) => m.productId);
const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v }));
const toggleProduct = (p) =>
setForm((f) => {
const exists = f.members.some((m) => m.productId === p.id);
return {
...f,
members: exists
? f.members.filter((m) => m.productId !== p.id)
: [...f.members, memberFromProduct(p)],
};
});
const setMember = (productId, key, value) =>
setForm((f) => ({
...f,
members: f.members.map((m) =>
m.productId === productId ? { ...m, [key]: value } : m
),
}));
const save = async () => {
setErrors([]);
if (!form.name.trim()) return setErrors(["Group name is required"]);
if (form.members.length < 2)
return setErrors(["Add at least 2 products to a group"]);
setSaving(true);
const method = existing ? "PUT" : "POST";
const url = existing ? `/api/groups/${existing.id}` : "/api/groups";
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
setSaving(false);
if (res.ok) {
app.toast.show("Group saved");
navigate("/groups");
} else {
const data = await res.json().catch(() => ({}));
setErrors(data.errors || ["Could not save group"]);
}
};
return (
<Layout>
<Layout.Section>
{errors.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Banner status="critical" title="Please fix the following">
<ul>
{errors.map((e) => (
<li key={e}>{e}</li>
))}
</ul>
</Banner>
</div>
)}
<Card sectioned title="Group details">
<Stack vertical spacing="loose">
<TextField
label="Group name (internal)"
value={form.name}
onChange={set("name")}
autoComplete="off"
/>
<Stack distribution="fillEvenly">
<TextField
label="Storefront label"
helpText='Shown above the swatches, e.g. "Color"'
value={form.label}
onChange={set("label")}
autoComplete="off"
/>
<Select
label="Swatch style"
options={[
{ label: "Color swatches", value: "color" },
{ label: "Image swatches", value: "image" },
{ label: "Buttons", value: "button" },
]}
value={form.style}
onChange={set("style")}
/>
<Select
label="Status"
helpText="Published groups appear on the storefront."
options={[
{ label: "Published", value: "published" },
{ label: "Draft (hidden)", value: "draft" },
]}
value={form.status}
onChange={set("status")}
/>
</Stack>
</Stack>
</Card>
<Card sectioned title={`Products in this group (${form.members.length})`}>
{form.members.length === 0 ? (
<Text as="p" color="subdued">
Pick products on the right to link them as swatches.
</Text>
) : (
<Stack vertical spacing="loose">
{form.members.map((m) => (
<Stack key={m.productId} alignment="center" spacing="tight" wrap={false}>
<Thumbnail size="small" source={m.image || ""} alt={m.productTitle} />
<Stack.Item fill>
<Text variant="bodyMd" as="p" fontWeight="semibold">
{m.productTitle}
</Text>
</Stack.Item>
<div style={{ width: 130 }}>
<TextField
label="Label"
labelHidden
value={m.swatchValue}
onChange={(v) => setMember(m.productId, "swatchValue", v)}
placeholder="Red"
autoComplete="off"
/>
</div>
{form.style === "color" && (
<Stack alignment="center" spacing="extraTight" wrap={false}>
<div
style={{
width: 28,
height: 28,
borderRadius: "50%",
border: "1px solid rgba(0,0,0,.15)",
background: m.swatchColor || "#eee",
}}
/>
<div style={{ width: 110 }}>
<TextField
label="Color"
labelHidden
value={m.swatchColor}
onChange={(v) => setMember(m.productId, "swatchColor", v)}
placeholder="#000000"
autoComplete="off"
/>
</div>
</Stack>
)}
<Button
plain
destructive
onClick={() => toggleProduct({ id: m.productId })}
>
Remove
</Button>
</Stack>
))}
</Stack>
)}
</Card>
<Stack distribution="trailing">
<Button onClick={() => navigate("/groups")}>Cancel</Button>
<Button primary loading={saving} onClick={save}>
{existing ? "Save group" : "Create group"}
</Button>
</Stack>
</Layout.Section>
<Layout.Section secondary>
<ProductMultiSelect selectedIds={selectedIds} onToggle={toggleProduct} />
</Layout.Section>
</Layout>
);
}