latest changes
This commit is contained in:
parent
90bedec2db
commit
7ef9040271
@ -90,7 +90,7 @@ export const action = async ({ request }) => {
|
||||
}
|
||||
],
|
||||
trialDays: 7, # ✅ trialDays is a top-level argument!
|
||||
test: false
|
||||
test: true
|
||||
) {
|
||||
confirmationUrl
|
||||
appSubscription {
|
||||
|
||||
@ -38,36 +38,36 @@ async function checkShopExists(shop) {
|
||||
}
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
// const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// 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 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) || [];
|
||||
// // 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) || [];
|
||||
|
||||
|
||||
|
||||
@ -75,23 +75,23 @@ export const loader = async ({ request }) => {
|
||||
|
||||
|
||||
|
||||
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;
|
||||
// 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);
|
||||
// let brands = [];
|
||||
// try {
|
||||
// brands = JSON.parse(rawValue);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to parse metafield value:", err);
|
||||
}
|
||||
// } catch (err) {
|
||||
// console.error("❌ Failed to parse metafield value:", err);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ export const loader = async ({ request }) => {
|
||||
const { session } = await authenticate.admin(request);
|
||||
const shop = session.shop;
|
||||
|
||||
return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop });
|
||||
return json({ brands: [], collections : [], selectedBrandsFromShopify: [], shop });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
@ -340,7 +340,7 @@ if (Turn14Enabled === false) {
|
||||
<br />
|
||||
|
||||
</div>
|
||||
<div>
|
||||
{/* <div>
|
||||
<p>
|
||||
<strong>Turn 14 Status:</strong>{" "}
|
||||
{Turn14Enabled === true
|
||||
@ -349,7 +349,7 @@ if (Turn14Enabled === false) {
|
||||
? "❌ Turn14 x Shopify Connection Doesn't Exists"
|
||||
: "Checking..."}
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
<Layout >
|
||||
|
||||
|
||||
|
||||
457
app/routes/app.brands_2808.jsx
Normal file
457
app/routes/app.brands_2808.jsx
Normal file
@ -0,0 +1,457 @@
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const { session } = await authenticate.admin(request);
|
||||
const shop = session.shop;
|
||||
|
||||
return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], 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 } = useLoaderData();
|
||||
// console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`);
|
||||
const actionData = useActionData() || {};
|
||||
const [selectedIdsold, setSelectedIdsold] = useState([])
|
||||
// const [selectedIds, setSelectedIds] = useState(() => {
|
||||
// const titles = new Set(collections.map(c => c.title.toLowerCase()));
|
||||
// return brands
|
||||
// .filter(b => titles.has(b.name.toLowerCase()))
|
||||
// .map(b => b.id);
|
||||
// });
|
||||
|
||||
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 isn’t connected yet
|
||||
</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
This shop hasn’t 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, you’ll 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>
|
||||
);
|
||||
}
|
||||
@ -532,14 +532,14 @@ export default function ManageBrandProducts() {
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
<p>
|
||||
{/* <p>
|
||||
<strong>Turn 14 Status:</strong>{" "}
|
||||
{Turn14Enabled === true
|
||||
? "✅ Turn14 x Shopify Connected!"
|
||||
: Turn14Enabled === false
|
||||
? "❌ Turn14 x Shopify Connection Doesn't Exists"
|
||||
: "Checking..."}
|
||||
</p>
|
||||
</p> */}
|
||||
|
||||
{brands.length === 0 ? (
|
||||
<Layout.Section>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user