import { json } from "@remix-run/node"; import { useLoaderData, useFetcher, useActionData } 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 }) => { return json({ success: true }); const formData = await request.formData(); const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); const { session } = await authenticate.admin(request); const shop = session.shop; // "veloxautomotive.myshopify.com" // make the POST to your backend const resp = await fetch("https://backend.dine360.ca/managebrands", { method: "POST", headers: { "Content-Type": "application/json", "shop-domain": shop, }, body: JSON.stringify({ shop, selectedBrands }), }); if (!resp.ok) { const err = await resp.text(); return json({ error: err }, { status: resp.status }); } const { processId, status } = await resp.json(); console.log("Process ID:", processId); console.log("Status:", status); return json({ processId, status }); 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 } } } `); } } const fallbackLogo = "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"; for (const brand of selectedBrands) { const exists = existingCollections.find( (c) => c.title.toLowerCase() === brand.name.toLowerCase() ); if (exists) continue; const escapedName = brand.name.replace(/"/g, '\\"'); const logoSrc = brand.logo || fallbackLogo; await admin.graphql(` mutation { collectionCreate(input: { title: "${escapedName}", descriptionHtml: "Products from brand ${escapedName}", image: { altText: "${escapedName} Logo", src: "${logoSrc}" } }) { 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 fetcher1 = useFetcher(); const actionData = fetcher1.data; const [status, setStatus] = useState(actionData?.status || ""); const [polling, setPolling] = useState(false); console.log("Action Data:", actionData); // the processId returned from the action const processId = actionData?.processId; useEffect(() => { console.log("Action Data:", fetcher.data); }, [fetcher1.data]); async function checkStatus() { if (!processId) return; setPolling(true); const resp = await fetch( `https://backend.dine360.ca/managebrands/status/${processId}`, { headers: { "shop-domain": window.shopify.shop || "" }, } ); const json = await resp.json(); setStatus(json.status + (json.detail ? ` (${json.detail})` : "")); setPolling(false); } 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 toggleSelectAll = () => { const filteredBrandIds = filteredBrands.map(b => b.id); const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id)); if (allFilteredSelected) { // Deselect all filtered brands setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id))); } else { // Select all filtered brands setSelectedIds(prev => { const combined = new Set([...prev, ...filteredBrandIds]); return Array.from(combined); }); } }; const toastMarkup = toastActive ? ( setToastActive(false)} /> ) : null; const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every(brand => selectedIds.includes(brand.id)); return (
{filteredBrands.map((brand) => (
toggleSelect(brand.id)} />
{brand.name}
))} {processId && (

Process ID: {processId}

Status: {status || "—"}

)}
{toastMarkup}
); }