backend_shopify_data4autos/app/routes/app.brands copy.jsx
2025-06-30 12:51:23 +00:00

274 lines
7.2 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 { TitleBar } from "@shopify/app-bridge-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="Data4Autos Turn14 Brands List">
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<Layout.Section>
<div style={{ display: "flex", alignItems: "center", gap: "1rem", flexWrap: "wrap" }}></div>
<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>
);
}