2025-08-07 06:22:27 +00:00

279 lines
8.4 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") || "[]");
// get the shop domain from the headers (as you mentioned)
const shop = request.headers.get("shop-domain") || "";
// 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 }),
});
console.log("Request to Home:", { shop, selectedBrands });
console.log("Request headers:", { "shop-domain": shop });
console.log("Request body:", { selectedBrands });
console.log("Response status:", resp.status);
console.log("Response headers:", resp.headers);
console.log("Response URL:", resp.url);
console.log("Response status text:", resp.statusText);
console.log("Response ok:", resp.ok);
console.log("Response type:", resp.type);
console.log("Response redirected:", resp.redirected);
console.log("Response from backend:", resp);
if (!resp.ok) {
const err = await resp.text();
return json({ error: err }, { status: resp.status });
}
const { processId, status } = await resp.json();
// return the processId (and initial status if you like) to the client
return json({ processId, status });
};
export default function BrandsPage() {
const actionData = useActionData();
const [status, setStatus] = useState(actionData?.status || "");
const [polling, setPolling] = useState(false);
// the processId returned from the action
const processId = actionData?.processId;
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 ? (
<Toast
content="Collections updated successfully!"
onDismiss={() => 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 (
<Frame>
<Page title="Data4Autos Turn14 Brands List">
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<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.Section>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<TextField
label="Search brands"
value={search}
onChange={setSearch}
autoComplete="off"
placeholder="Type brand name..."
/>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<Checkbox
label="Select All"
checked={allFilteredSelected}
onChange={toggleSelectAll}
/>
</div>
</div>
</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>
))}
{processId && (
<div style={{ marginTop: 20 }}>
<p>
<strong>Process ID:</strong> {processId}
</p>
<p>
<strong>Status:</strong> {status || ""}
</p>
<button
onClick={checkStatus}
disabled={polling}
style={{ marginTop: 10 }}
>
{polling ? "Checking…" : "Check Status"}
</button>
</div>
)}
</div>
</Layout.Section>
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}