271 lines
7.0 KiB
JavaScript
271 lines
7.0 KiB
JavaScript
import { json } from "@remix-run/node";
|
|
import { useLoaderData, useFetcher } from "@remix-run/react";
|
|
import {
|
|
Page,
|
|
Layout,
|
|
Card,
|
|
TextField,
|
|
Checkbox,
|
|
Button,
|
|
Thumbnail,
|
|
Spinner,
|
|
Toast,
|
|
Frame,
|
|
} from "@shopify/polaris";
|
|
import { useEffect, useState } from "react";
|
|
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
|
|
import { authenticate } from "../shopify.server";
|
|
|
|
export const loader = async ({ request }) => {
|
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
|
const { admin } = await authenticate.admin(request);
|
|
|
|
// Get brands
|
|
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
const brandJson = await brandRes.json();
|
|
if (!brandRes.ok) {
|
|
return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 });
|
|
}
|
|
|
|
// Get collections
|
|
const gqlRaw = await admin.graphql(`
|
|
{
|
|
collections(first: 100) {
|
|
edges {
|
|
node {
|
|
id
|
|
title
|
|
handle
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
const gql = await gqlRaw.json();
|
|
const collections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
|
|
|
|
return json({
|
|
brands: brandJson.data,
|
|
collections,
|
|
});
|
|
};
|
|
|
|
export const action = async ({ request }) => {
|
|
const formData = await request.formData();
|
|
const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
|
|
|
|
const { admin } = await authenticate.admin(request);
|
|
|
|
// Get current collections
|
|
const gqlRaw = await admin.graphql(`
|
|
{
|
|
collections(first: 100) {
|
|
edges {
|
|
node {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
const gql = await gqlRaw.json();
|
|
const existingCollections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
|
|
|
|
const selectedTitles = selectedBrands.map((b) => b.name.toLowerCase());
|
|
const logoMap = Object.fromEntries(selectedBrands.map(b => [b.name.toLowerCase(), b.logo]));
|
|
|
|
// Delete unselected
|
|
for (const col of existingCollections) {
|
|
if (!selectedTitles.includes(col.title.toLowerCase())) {
|
|
await admin.graphql(`
|
|
mutation {
|
|
collectionDelete(input: { id: "${col.id}" }) {
|
|
deletedCollectionId
|
|
userErrors { message }
|
|
}
|
|
}
|
|
`);
|
|
}
|
|
}
|
|
|
|
|
|
// Create new
|
|
for (const brand of selectedBrands) {
|
|
const exists = existingCollections.find(
|
|
(c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
|
);
|
|
if (!exists) {
|
|
const escapedName = brand.name.replace(/"/g, '\\"');
|
|
const logo = brand.logo || "";
|
|
|
|
await admin.graphql(`
|
|
mutation {
|
|
collectionCreate(input: {
|
|
title: "${escapedName}",
|
|
descriptionHtml: "Products from brand ${escapedName}",
|
|
image: {
|
|
altText: "${escapedName} Logo",
|
|
src: "${logo}"
|
|
}
|
|
}) {
|
|
collection { id }
|
|
userErrors { message }
|
|
}
|
|
}
|
|
`);
|
|
}
|
|
}
|
|
|
|
const shopDataRaw = await admin.graphql(`
|
|
{
|
|
shop {
|
|
id
|
|
}
|
|
}
|
|
`);
|
|
const shopRes = await admin.graphql(`{ shop { id } }`);
|
|
const shopJson = await shopRes.json();
|
|
const shopId = shopJson?.data?.shop?.id;
|
|
|
|
await admin.graphql(`
|
|
mutation {
|
|
metafieldsSet(metafields: [{
|
|
namespace: "turn14",
|
|
key: "selected_brands",
|
|
type: "json",
|
|
ownerId: "${shopId}",
|
|
value: ${JSON.stringify(JSON.stringify(selectedBrands))}
|
|
}]) {
|
|
metafields {
|
|
id
|
|
}
|
|
userErrors {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
|
|
|
|
return json({ success: true });
|
|
|
|
};
|
|
|
|
export default function BrandsPage() {
|
|
const { brands, collections } = useLoaderData();
|
|
const fetcher = useFetcher();
|
|
const isSubmitting = fetcher.state === "submitting";
|
|
const [toastActive, setToastActive] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
|
|
const collectionTitles = new Set(collections.map((c) => c.title.toLowerCase()));
|
|
const defaultSelected = brands
|
|
.filter((b) => collectionTitles.has(b.name.toLowerCase()))
|
|
.map((b) => b.id);
|
|
|
|
const [selectedIds, setSelectedIds] = useState(defaultSelected);
|
|
const [filteredBrands, setFilteredBrands] = useState(brands);
|
|
|
|
useEffect(() => {
|
|
const term = search.toLowerCase();
|
|
setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term)));
|
|
}, [search, brands]);
|
|
|
|
useEffect(() => {
|
|
if (fetcher.data?.success) {
|
|
setToastActive(true);
|
|
}
|
|
}, [fetcher.data]);
|
|
|
|
const toggleSelect = (id) => {
|
|
setSelectedIds((prev) =>
|
|
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
const toastMarkup = toastActive ? (
|
|
<Toast
|
|
content="Collections updated successfully!"
|
|
onDismiss={() => setToastActive(false)}
|
|
/>
|
|
) : null;
|
|
|
|
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
|
|
|
|
return (
|
|
<Frame>
|
|
<Page title="Brands List">
|
|
<Layout>
|
|
<Layout.Section>
|
|
<TextField
|
|
label="Search brands"
|
|
value={search}
|
|
onChange={setSearch}
|
|
autoComplete="off"
|
|
placeholder="Type brand name..."
|
|
/>
|
|
</Layout.Section>
|
|
|
|
<Layout.Section>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
|
|
gap: "16px",
|
|
}}
|
|
>
|
|
{filteredBrands.map((brand) => (
|
|
<Card key={brand.id} sectioned>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
|
<Checkbox
|
|
label=""
|
|
checked={selectedIds.includes(brand.id)}
|
|
onChange={() => toggleSelect(brand.id)}
|
|
/>
|
|
<Thumbnail
|
|
source={
|
|
brand.logo ||
|
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
|
}
|
|
alt={brand.name}
|
|
size="small"
|
|
/>
|
|
<div>
|
|
<strong>{brand.name}</strong>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</Layout.Section>
|
|
|
|
<Layout.Section>
|
|
<fetcher.Form method="post">
|
|
<input
|
|
type="hidden"
|
|
name="selectedBrands"
|
|
value={JSON.stringify(selectedBrands)}
|
|
/>
|
|
<Button
|
|
primary
|
|
submit
|
|
disabled={selectedIds.length === 0 || isSubmitting}
|
|
>
|
|
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
|
</Button>
|
|
</fetcher.Form>
|
|
</Layout.Section>
|
|
</Layout>
|
|
{toastMarkup}
|
|
</Page>
|
|
</Frame>
|
|
);
|
|
}
|