2026-04-17 21:19:17 +00:00

656 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
Page,
Layout,
Card,
TextField,
Checkbox,
Button,
Thumbnail,
Spinner,
Toast,
Frame,
Text,
} 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";
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1; // ✅ true if shop exists, false otherwise
} catch (err) {
console.error("Error checking shop:", err);
return false; // default to false if error
}
}
// export const loader = async ({ request }) => {
// // const accessToken = await getTurn14AccessTokenFromMetafield(request);
// const { admin } = await authenticate.admin(request);
// const { session } = await authenticate.admin(request);
// const shop = session.shop;
// var accessToken = ""
// try {
// accessToken = await getTurn14AccessTokenFromMetafield(request);
// } catch (err) {
// return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, err });
// console.error("Error getting Turn14 access token:", err);
// // Proceeding with empty accessToken
// }
// // fetch 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 });
// }
// // fetch Shopify collections
// const gqlRaw = await admin.graphql(`
// {
// collections(first: 100) {
// edges {
// node {
// id
// title
// }
// }
// }
// }
// `);
// const gql = await gqlRaw.json();
// const collections = gql?.data?.collections?.edges.map(e => e.node) || [];
// const res = await admin.graphql(`{
// shop {
// metafield(namespace: "turn14", key: "selected_brands") {
// value
// }
// }
// }`);
// const data = await res.json();
// const rawValue = data?.data?.shop?.metafield?.value;
// let brands = [];
// try {
// brands = JSON.parse(rawValue);
// } catch (err) {
// console.error("❌ Failed to parse metafield value:", err);
// }
// return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop });
// };
export const loader = async ({ request }) => {
console.log("🚀 Loader started");
let admin, session, shop;
try {
const authResult = await authenticate.admin(request);
admin = authResult.admin;
session = authResult.session;
shop = session?.shop;
console.log("✅ Shopify auth success");
console.log("🏪 Shop:", shop);
} catch (err) {
console.error("❌ Shopify authentication failed:", err);
return json({
brands: [],
collections: [],
selectedBrandsFromShopify: [],
shop: "",
error: "Shopify authentication failed",
});
}
let accessToken = "";
try {
console.log("🔑 Fetching Turn14 access token from metafield...");
accessToken = await getTurn14AccessTokenFromMetafield(request);
console.log("✅ Turn14 access token received:", accessToken ? "YES" : "EMPTY");
} catch (err) {
console.error("❌ Error getting Turn14 access token:", err);
return json({
brands: [],
collections: [],
selectedBrandsFromShopify: [],
shop,
error: "Failed to fetch Turn14 access token",
});
}
/* =========================
FETCH TURN14 BRANDS
========================== */
let brandJson;
try {
console.log("📦 Fetching Turn14 brands...");
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
console.log("🔄 Awaiting Turn14 brands response...",brandRes);
console.log("2345678909876543567 Turn14 brands fetch initiated");
console.log("📡 Turn14 brands fetch completed", accessToken);
console.log("📡 Turn14 brands HTTP status:", brandRes.status);
brandJson = await brandRes.json();
console.log("📦 Turn14 brands raw response:", brandJson);
if (!brandRes.ok) {
console.error("❌ Turn14 brands fetch failed");
return json({
brands: [],
collections: [],
selectedBrandsFromShopify: [],
shop,
error: brandJson?.error || "Failed to fetch brands",
});
}
} catch (err) {
console.error("❌ Exception while fetching Turn14 brands:", err);
return json({
brands: [],
collections: [],
selectedBrandsFromShopify: [],
shop,
error: "Turn14 brands fetch crashed",
});
}
/* =========================
FETCH SHOPIFY COLLECTIONS
========================== */
let collections = [];
try {
console.log("🗂️ Fetching Shopify collections...");
const gqlRaw = await admin.graphql(`
{
collections(first: 100) {
edges {
node {
id
title
}
}
}
}
`);
const gql = await gqlRaw.json();
console.log("🧾 Shopify collections raw response:", gql);
collections =
gql?.data?.collections?.edges?.map((e) => e.node) || [];
console.log("✅ Parsed collections count:", collections.length);
} catch (err) {
console.error("❌ Error fetching Shopify collections:", err);
}
/* =========================
FETCH SELECTED BRANDS METAFIELD
========================== */
let selectedBrands = [];
try {
console.log("🏷️ Fetching shop metafield: turn14.selected_brands");
const res = await admin.graphql(`
{
shop {
metafield(namespace: "turn14", key: "selected_brands") {
value
}
}
}
`);
const data = await res.json();
console.log("🧾 Metafield raw response:", data);
const rawValue = data?.data?.shop?.metafield?.value;
console.log("📄 Raw metafield value:", rawValue);
if (rawValue) {
selectedBrands = JSON.parse(rawValue);
console.log(
"✅ Parsed selectedBrands count:",
selectedBrands.length
);
} else {
console.log(" No metafield value found (first-time setup)");
}
} catch (err) {
console.error("❌ Failed parsing selected_brands metafield:", err);
}
/* =========================
FINAL RETURN
========================== */
console.log("🎯 Loader final return payload:", {
brandsCount: brandJson?.data?.length || 0,
collectionsCount: collections.length,
selectedBrandsCount: selectedBrands.length,
shop,
});
return json({
brands: brandJson?.data || [],
collections,
selectedBrandsFromShopify: selectedBrands,
shop,
});
};
export const action = async ({ request }) => {
const formData = await request.formData();
const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
const selectedOldBrands = JSON.parse(formData.get("selectedOldBrands") || "[]");
const { session } = await authenticate.admin(request);
const shop = session.shop; // "veloxautomotive.myshopify.com"
selectedBrands.forEach(brand => {
delete brand.pricegroups;
});
selectedOldBrands.forEach(brand => {
delete brand.pricegroups;
});
const resp = await fetch("https://backend.data4autos.com/managebrands", {
method: "POST",
headers: {
"Content-Type": "application/json",
"shop-domain": shop,
},
body: JSON.stringify({ shop, selectedBrands, selectedOldBrands }),
});
if (!resp.ok) {
const err = await resp.text();
return json({ error: err }, { status: resp.status });
}
const { processId, status } = await resp.json();
return json({ processId, status });
};
export default function BrandsPage() {
const {
brands = [],
collections = [],
selectedBrandsFromShopify = [],
shop = "",
err,
error,
} = useLoaderData() || {};
console.log(err)
console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`);
const actionData = useActionData() || {};
const [selectedIdsold, setSelectedIdsold] = useState([])
const [selectedIds, setSelectedIds] = useState(() => {
return (selectedBrandsFromShopify ?? []).map((b) => b.id);
});
// console.log("Selected IDS : ", selectedIds)
const [search, setSearch] = useState("");
const [filteredBrands, setFilteredBrands] = useState(brands);
const [toastActive, setToastActive] = useState(false);
const [polling, setPolling] = useState(false);
const [status, setStatus] = useState(actionData.status || "");
const [Turn14Enabled, setTurn14Enabled] = useState(null); // null | true | false
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => {
const selids = selectedIds
// console.log("Selected IDS : ", selids)
setSelectedIdsold(selids)
}, [toastActive]);
useEffect(() => {
const term = search.toLowerCase();
setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term)));
}, [search, brands]);
useEffect(() => {
if (actionData.status) {
setStatus(actionData.status);
setToastActive(true);
}
}, [actionData.status]);
const checkStatus = async () => {
if (!actionData.processId) return;
setPolling(true);
const resp = await fetch(
`https://backend.data4autos.com/managebrands/status/${actionData.processId}`,
{ headers: { "shop-domain": window.shopify.shop || "" } }
);
const jsonBody = await resp.json();
setStatus(
jsonBody.status + (jsonBody.detail ? ` (${jsonBody.detail})` : "")
);
setPolling(false);
};
const toggleSelect = id =>
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
const allFilteredSelected =
filteredBrands.length > 0 &&
filteredBrands.every(b => selectedIds.includes(b.id));
const toggleSelectAll = () => {
const ids = filteredBrands.map(b => b.id);
if (allFilteredSelected) {
setSelectedIds(prev => prev.filter(id => !ids.includes(id)));
} else {
setSelectedIds(prev => Array.from(new Set([...prev, ...ids])));
}
};
var isSubmitting;
// console.log("actionData", actionData);
if (actionData.status) {
isSubmitting = !actionData.status && !actionData.error && !actionData.processId;
} else {
isSubmitting = false;
}
// console.log("isSubmitting", isSubmitting);
const toastMarkup = toastActive ? (
<Toast
content="Collections updated successfully!"
onDismiss={() => setToastActive(false)}
/>
) : null;
const selectedBrands = brands.filter(b => selectedIds.includes(b.id));
const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id));
const shopDomain = (shop || "").split(".")[0];
const items = [
{ icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` },
{ icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` },
{ icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` },
{ icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` },
];
// If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen
if (Turn14Enabled === false) {
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" background="critical" />
<Layout>
<Layout.Section>
<Card>
<div style={{ padding: 24, textAlign: "center" }}>
<Text as="h1" variant="headingLg">
Turn14 isnt connected yet
</Text>
<div style={{ marginTop: 8 }}>
<Text as="p" variant="bodyMd">
This shop hasnt been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
</Text>
</div>
{/* Primary actions */}
<div style={{ marginTop: 24, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
<a href={items[0].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[0].icon} {items[0].text}
</Text>
</a>
<a href={items[3].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[3].icon} {items[3].text}
</Text>
</a>
</div>
<div style={{ marginTop: 28 }}>
<Text as="p" variant="bodySm" tone="subdued">
Once connected, youll be able to browse brands and sync collections.
</Text>
</div>
{/* Secondary links */}
<div style={{ marginTop: 20, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
<a href={items[1].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[1].icon} {items[1].text}
</Text>
</a>
<a href={items[2].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[2].icon} {items[2].text}
</Text>
</a>
</div>
</div>
</Card>
</Layout.Section>
</Layout>
</Page>
</Frame>
);
}
// console.log("Selected Brands:", selectedBrands)
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" background="primary" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", padding: "20px 0px", position: "fixed", zIndex: 2, background: "rgb(241 241 241)", width: "100%", top: 0, }}>
<Text as="h1" variant="headingLg">
Data4Autos Turn14 Brands List
</Text>
<br />
</div>
{/* <div>
<p>
<strong>Turn 14 Status:</strong>{" "}
{Turn14Enabled === true
? "✅ Turn14 x Shopify Connected!"
: Turn14Enabled === false
? "❌ Turn14 x Shopify Connection Doesn't Exists"
: "Checking..."}
</p>
</div> */}
<Layout >
<Layout.Section>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap", position: "fixed", zIndex: 2, background: "white", padding: "20px", width: "97%", marginTop: "40px", backgroundColor: "#00d1ff" }}>
{/* Left side - Search + Select All */}
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
{(actionData?.processId || false) && (
<div>
<p>
<strong>Process ID:</strong> {actionData.processId}
</p>
<p>
<strong>Status:</strong> {status || ""}
</p>
<Button onClick={checkStatus} loading={polling}>
Check Status
</Button>
</div>
)}
<TextField
labelHidden
label="Search brands"
value={search}
onChange={setSearch}
placeholder="Type brand name…"
autoComplete="off"
/>
<Checkbox
label="Select All"
checked={allFilteredSelected}
onChange={toggleSelectAll}
/>
</div>
{/* Right side - Save Button */}
<Form method="post" style={{ display: "flex", alignItems: "center", gap: 8 }}>
<input
type="hidden"
name="selectedBrands"
value={JSON.stringify(selectedBrands)}
/>
<input
type="hidden"
name="selectedOldBrands"
value={JSON.stringify(selectedOldBrands)}
/>
{/* <Button primary submit disabled={selectedIds.length === 0 || isSubmitting}> */}
<Button primary submit disabled={isSubmitting} size="large" variant="primary">
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
</Button>
</Form>
</div>
</Layout.Section>
<Layout.Section>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
gap: 16,
marginTop: "120px"
}}
>
{filteredBrands.map((brand) => (
<Card key={brand.id} sectioned>
<div style={{ position: "relative", textAlign: "center" }}>
{/* Checkbox in top-right corner */}
<div style={{ position: "absolute", top: 0, right: 0 }}>
<Checkbox
label=""
checked={selectedIds.includes(brand.id)}
onChange={() => toggleSelect(brand.id)}
/>
</div>
{/* Brand image */}
<div style={{ display: "flex", justifyContent: "center" }}>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="large"
/>
</div>
{/* Brand name */}
<div style={{ marginTop: "15px", fontWeight: "600", fontSize: "16px", lineHeight: "26px" }}>
{brand.name}
</div>
</div>
</Card>
))}
</div>
</Layout.Section>
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}