Baseline for frontend
This commit is contained in:
parent
fa0d6eb57c
commit
40e05059c8
@ -1,5 +1,5 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, useFetcher } from "@remix-run/react";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
@ -21,7 +21,7 @@ export const loader = async ({ request }) => {
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// Get brands
|
||||
// fetch brands
|
||||
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@ -33,7 +33,7 @@ export const loader = async ({ request }) => {
|
||||
return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 });
|
||||
}
|
||||
|
||||
// Get collections
|
||||
// fetch Shopify collections
|
||||
const gqlRaw = await admin.graphql(`
|
||||
{
|
||||
collections(first: 100) {
|
||||
@ -41,230 +41,129 @@ export const loader = async ({ request }) => {
|
||||
node {
|
||||
id
|
||||
title
|
||||
handle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const gql = await gqlRaw.json();
|
||||
const collections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
|
||||
const collections = gql?.data?.collections?.edges.map(e => e.node) || [];
|
||||
|
||||
return json({
|
||||
brands: brandJson.data,
|
||||
collections,
|
||||
});
|
||||
return json({ brands: brandJson.data, collections });
|
||||
};
|
||||
|
||||
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"
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
selectedBrands.forEach(brand => {
|
||||
delete brand.pricegroups;
|
||||
});
|
||||
|
||||
// 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 }
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
// }
|
||||
// }
|
||||
selectedOldBrands.forEach(brand => {
|
||||
delete brand.pricegroups;
|
||||
});
|
||||
|
||||
|
||||
// for (const brand of selectedBrands) {
|
||||
// const exists = existingCollections.find(
|
||||
// (c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||
// );
|
||||
// if (!exists) {
|
||||
// const escapedName = brand.name.replace(/"/g, '\\"');
|
||||
// // Only build the image block if there's a logo URL:
|
||||
// const imageBlock = brand.logo
|
||||
// ? `
|
||||
// image: {
|
||||
// altText: "${escapedName} Logo",
|
||||
// src: "${brand.logo}"
|
||||
// }
|
||||
// `
|
||||
// : "";
|
||||
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 }),
|
||||
});
|
||||
|
||||
// await admin.graphql(`
|
||||
// mutation {
|
||||
// collectionCreate(input: {
|
||||
// title: "${escapedName}",
|
||||
// descriptionHtml: "Products from brand ${escapedName}"
|
||||
// ${imageBlock}
|
||||
// }) {
|
||||
// collection { id }
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
`);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text();
|
||||
return json({ error: err }, { status: resp.status });
|
||||
}
|
||||
|
||||
|
||||
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 });
|
||||
const { processId, status } = await resp.json();
|
||||
return json({ processId, status });
|
||||
};
|
||||
|
||||
export default function BrandsPage() {
|
||||
const { brands, collections } = useLoaderData();
|
||||
const fetcher = useFetcher();
|
||||
const isSubmitting = fetcher.state === "submitting";
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
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 [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);
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [status, setStatus] = useState(actionData.status || "");
|
||||
|
||||
|
||||
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)));
|
||||
setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term)));
|
||||
}, [search, brands]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.success) {
|
||||
if (actionData.status) {
|
||||
setStatus(actionData.status);
|
||||
setToastActive(true);
|
||||
}
|
||||
}, [fetcher.data]);
|
||||
}, [actionData.status]);
|
||||
|
||||
const toggleSelect = (id) => {
|
||||
setSelectedIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
||||
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 filteredBrandIds = filteredBrands.map(b => b.id);
|
||||
const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id));
|
||||
|
||||
const ids = filteredBrands.map(b => b.id);
|
||||
if (allFilteredSelected) {
|
||||
// Deselect all filtered brands
|
||||
setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id)));
|
||||
setSelectedIds(prev => prev.filter(id => !ids.includes(id)));
|
||||
} else {
|
||||
// Select all filtered brands
|
||||
setSelectedIds(prev => {
|
||||
const combined = new Set([...prev, ...filteredBrandIds]);
|
||||
return Array.from(combined);
|
||||
});
|
||||
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!"
|
||||
@ -272,47 +171,62 @@ export default function BrandsPage() {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
|
||||
const allFilteredSelected = filteredBrands.length > 0 &&
|
||||
filteredBrands.every(brand => selectedIds.includes(brand.id));
|
||||
|
||||
const selectedBrands = brands.filter(b => selectedIds.includes(b.id));
|
||||
const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id));
|
||||
console.log("123456", selectedOldBrands)
|
||||
return (
|
||||
<Frame>
|
||||
<Page title="Data4Autos Turn14 Brands List">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<fetcher.Form method="post">
|
||||
<Form method="post">
|
||||
<input
|
||||
type="hidden"
|
||||
name="selectedBrands"
|
||||
value={JSON.stringify(selectedBrands)}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
submit
|
||||
disabled={selectedIds.length === 0 || isSubmitting}
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="selectedOldBrands"
|
||||
value={JSON.stringify(selectedOldBrands)}
|
||||
/>
|
||||
<Button primary submit disabled={selectedIds.length === 0 || isSubmitting}>
|
||||
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
</Form>
|
||||
</Layout.Section>
|
||||
|
||||
<Layout.Section>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
|
||||
|
||||
{(actionData.processId || false) && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p>
|
||||
<strong>Process ID:</strong> {actionData.processId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Status:</strong> {status || "—"}
|
||||
</p>
|
||||
<Button onClick={checkStatus} loading={polling}>
|
||||
Check Status
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
label="Search brands"
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder="Type brand name…"
|
||||
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>
|
||||
|
||||
@ -321,12 +235,12 @@ export default function BrandsPage() {
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
|
||||
gap: "16px",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{filteredBrands.map((brand) => (
|
||||
{filteredBrands.map(brand => (
|
||||
<Card key={brand.id} sectioned>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Checkbox
|
||||
label=""
|
||||
checked={selectedIds.includes(brand.id)}
|
||||
@ -340,17 +254,16 @@ export default function BrandsPage() {
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
/>
|
||||
<div>
|
||||
<strong>{brand.name}</strong>
|
||||
</div>
|
||||
<span>{brand.name}</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</Layout.Section>
|
||||
|
||||
|
||||
</Layout>
|
||||
|
||||
{toastMarkup}
|
||||
</Page>
|
||||
</Frame>
|
||||
|
||||
@ -63,6 +63,7 @@ export default function App() {
|
||||
<Link to="/app/brands">🏷️ Brands</Link>
|
||||
<Link to="/app/managebrand">📦 Manage Brands</Link>
|
||||
<Link to="/app/help">🆘 Help</Link>
|
||||
<Link to="/app/testing">🆘 Testing</Link>
|
||||
</NavMenu>
|
||||
<Outlet />
|
||||
</AppProvider>
|
||||
|
||||
376
app/routes/app.managebrand copy 2.jsx
Normal file
376
app/routes/app.managebrand copy 2.jsx
Normal file
@ -0,0 +1,376 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
IndexTable,
|
||||
Card,
|
||||
Thumbnail,
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
TextField,
|
||||
Banner,
|
||||
InlineError,
|
||||
Toast,
|
||||
Frame,
|
||||
|
||||
ProgressBar,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(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;
|
||||
|
||||
let brands = [];
|
||||
try {
|
||||
brands = JSON.parse(rawValue);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to parse metafield value:", err);
|
||||
}
|
||||
|
||||
return json({ brands, accessToken });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const formData = await request.formData();
|
||||
const brandId = formData.get("brandId");
|
||||
const rawCount = formData.get("productCount");
|
||||
const productCount = parseInt(rawCount, 10) || 10;
|
||||
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const { session } = await authenticate.admin(request);
|
||||
const shop = session.shop;
|
||||
|
||||
const resp = await fetch("https://backend.data4autos.com/manageProducts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"shop-domain": shop,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shop,
|
||||
brandID: brandId,
|
||||
turn14accessToken: accessToken,
|
||||
productCount
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Response from manageProducts:", resp.status, resp.statusText);
|
||||
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, "Status:", status);
|
||||
return json({ success: true, processId, status });
|
||||
};
|
||||
|
||||
export default function ManageBrandProducts() {
|
||||
const actionData = useActionData();
|
||||
const { brands, accessToken } = useLoaderData();
|
||||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||
const [itemsMap, setItemsMap] = useState({});
|
||||
const [loadingMap, setLoadingMap] = useState({});
|
||||
const [productCount, setProductCount] = useState("10");
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [status, setStatus] = useState(actionData?.status || "");
|
||||
const [processId, setProcessId] = useState(actionData?.processId || null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [totalProducts, setTotalProducts] = useState(0);
|
||||
const [processedProducts, setProcessedProducts] = useState(0);
|
||||
const [currentProduct, setCurrentProduct] = useState(null);
|
||||
const [results, setResults] = useState([]);
|
||||
const [detail, setDetail] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.processId) {
|
||||
setProcessId(actionData.processId);
|
||||
setStatus(actionData.status || "processing");
|
||||
setToastActive(true);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
const checkStatus = async () => {
|
||||
setPolling(true);
|
||||
try {
|
||||
const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`);
|
||||
const data = await response.json();
|
||||
|
||||
setStatus(data.status);
|
||||
setDetail(data.detail);
|
||||
setProgress(data.progress);
|
||||
setTotalProducts(data.stats.total);
|
||||
setProcessedProducts(data.stats.processed);
|
||||
setCurrentProduct(data.current);
|
||||
|
||||
if (data.results) {
|
||||
setResults(data.results);
|
||||
}
|
||||
|
||||
// Continue polling if still processing
|
||||
if (data.status !== 'done' && data.status !== 'error') {
|
||||
setTimeout(checkStatus, 2000);
|
||||
} else {
|
||||
setPolling(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setPolling(false);
|
||||
setStatus('error');
|
||||
setDetail('Failed to check status');
|
||||
console.error('Error checking status:', error);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (status?.includes("processing") && processId) {
|
||||
interval = setInterval(checkStatus, 5000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [status, processId]);
|
||||
|
||||
const toggleAllBrands = async () => {
|
||||
for (const brand of brands) {
|
||||
await toggleBrandItems(brand.id);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoad && brands.length > 0) {
|
||||
toggleAllBrands();
|
||||
setInitialLoad(false);
|
||||
}
|
||||
}, [brands, initialLoad]);
|
||||
|
||||
const toggleBrandItems = async (brandId) => {
|
||||
const isExpanded = expandedBrand === brandId;
|
||||
if (isExpanded) {
|
||||
setExpandedBrand(null);
|
||||
} else {
|
||||
setExpandedBrand(brandId);
|
||||
if (!itemsMap[brandId]) {
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||
try {
|
||||
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
const validItems = Array.isArray(data)
|
||||
? data.filter(item => item && item.id && item.attributes)
|
||||
: [];
|
||||
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
|
||||
} catch (err) {
|
||||
console.error("Error fetching items:", err);
|
||||
setItemsMap((prev) => ({ ...prev, [brandId]: [] }));
|
||||
}
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast
|
||||
content={status.includes("completed") ?
|
||||
"Products imported successfully!" :
|
||||
`Status: ${status}`}
|
||||
onDismiss={() => setToastActive(false)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Frame>
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
{brands.length === 0 ? (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<p>No brands selected yet.</p>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
) : (
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<IndexTable
|
||||
resourceName={{ singular: "brand", plural: "brands" }}
|
||||
itemCount={brands.length}
|
||||
headings={[
|
||||
{ title: "Brand ID" },
|
||||
{ title: "Logo" },
|
||||
{ title: "Action" },
|
||||
{ title: "Products Count" },
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
{brands.map((brand, index) => (
|
||||
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
|
||||
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Thumbnail
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
/>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
|
||||
</IndexTable.Row>
|
||||
))}
|
||||
</IndexTable>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
|
||||
{brands.map(
|
||||
(brand) =>
|
||||
expandedBrand === brand.id && (
|
||||
<Layout.Section fullWidth key={brand.id + "-expanded"}>
|
||||
<Card sectioned>
|
||||
{processId && (
|
||||
<div style={{ marginBottom: "1rem", padding: "1rem", border: "1px solid #e1e1e1", borderRadius: "4px" }}>
|
||||
<p>
|
||||
<strong>Process ID:</strong> {processId}
|
||||
</p>
|
||||
|
||||
<div style={{ margin: "1rem 0" }}>
|
||||
<p>
|
||||
<strong>Status:</strong> {status || "—"}
|
||||
</p>
|
||||
|
||||
{progress > 0 && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={
|
||||
status === 'error' ? 'critical' :
|
||||
status === 'done' ? 'success' : 'highlight'
|
||||
}
|
||||
/>
|
||||
<p style={{ marginTop: "0.25rem", fontSize: "0.85rem" }}>
|
||||
{processedProducts} of {totalProducts} products processed
|
||||
{currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === 'done' && results.length > 0 && (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<p>
|
||||
<strong>Results:</strong> {results.length} products processed successfully
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div style={{ marginTop: "1rem", color: "#ff4d4f" }}>
|
||||
<strong>Error:</strong> {detail}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={checkStatus}
|
||||
loading={polling}
|
||||
style={{ marginTop: "1rem" }}
|
||||
>
|
||||
{status === 'done' ? 'View Results' : 'Check Status'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="brandId" value={brand.id} />
|
||||
<TextField
|
||||
label="Number of products to add"
|
||||
type="number"
|
||||
name="productCount"
|
||||
value={productCount}
|
||||
onChange={(value) => setProductCount(value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
submit
|
||||
primary
|
||||
style={{ marginTop: "1rem" }}
|
||||
loading={status?.includes("processing")}
|
||||
>
|
||||
Add First {productCount} Products to Store
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{(
|
||||
itemsMap[brand.id] && itemsMap[brand.id].length > 0
|
||||
? itemsMap[brand.id].filter(item => item && item.id)
|
||||
: []
|
||||
).map((item) => (
|
||||
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
|
||||
<Layout>
|
||||
<Layout.Section oneThird>
|
||||
<Thumbnail
|
||||
source={
|
||||
item?.attributes?.thumbnail ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={item?.attributes?.product_name || 'Product image'}
|
||||
size="large"
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
|
||||
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}</p>
|
||||
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
|
||||
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)
|
||||
)}
|
||||
</Layout>
|
||||
{toastMarkup}
|
||||
</Page>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
475
app/routes/app.managebrand_070825.jsx
Normal file
475
app/routes/app.managebrand_070825.jsx
Normal file
@ -0,0 +1,475 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
IndexTable,
|
||||
Card,
|
||||
Thumbnail,
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
TextField,
|
||||
Banner,
|
||||
InlineError,
|
||||
Toast,
|
||||
Frame,
|
||||
Select,
|
||||
ProgressBar,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(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;
|
||||
|
||||
let brands = [];
|
||||
try {
|
||||
brands = JSON.parse(rawValue);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to parse metafield value:", err);
|
||||
}
|
||||
|
||||
return json({ brands, accessToken });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const formData = await request.formData();
|
||||
const brandId = formData.get("brandId");
|
||||
const rawCount = formData.get("productCount");
|
||||
const productCount = parseInt(rawCount, 10) || 10;
|
||||
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const { session } = await authenticate.admin(request);
|
||||
const shop = session.shop;
|
||||
|
||||
const resp = await fetch("https://backend.data4autos.com/manageProducts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"shop-domain": shop,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shop,
|
||||
brandID: brandId,
|
||||
turn14accessToken: accessToken,
|
||||
productCount
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Response from manageProducts:", resp.status, resp.statusText);
|
||||
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, "Status:", status);
|
||||
return json({ success: true, processId, status });
|
||||
};
|
||||
|
||||
export default function ManageBrandProducts() {
|
||||
const actionData = useActionData();
|
||||
const { brands, accessToken } = useLoaderData();
|
||||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||
const [itemsMap, setItemsMap] = useState({});
|
||||
const [loadingMap, setLoadingMap] = useState({});
|
||||
const [productCount, setProductCount] = useState("10");
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
const [toastActive, setToastActive] = useState(false);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [status, setStatus] = useState(actionData?.status || "");
|
||||
const [processId, setProcessId] = useState(actionData?.processId || null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [totalProducts, setTotalProducts] = useState(0);
|
||||
const [processedProducts, setProcessedProducts] = useState(0);
|
||||
const [currentProduct, setCurrentProduct] = useState(null);
|
||||
const [results, setResults] = useState([]);
|
||||
const [detail, setDetail] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.processId) {
|
||||
setProcessId(actionData.processId);
|
||||
setStatus(actionData.status || "processing");
|
||||
setToastActive(true);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
const checkStatus = async () => {
|
||||
setPolling(true);
|
||||
try {
|
||||
const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`);
|
||||
const data = await response.json();
|
||||
|
||||
setStatus(data.status);
|
||||
setDetail(data.detail);
|
||||
setProgress(data.progress);
|
||||
setTotalProducts(data.stats.total);
|
||||
setProcessedProducts(data.stats.processed);
|
||||
setCurrentProduct(data.current);
|
||||
|
||||
if (data.results) {
|
||||
setResults(data.results);
|
||||
}
|
||||
|
||||
// Continue polling if still processing
|
||||
if (data.status !== 'done' && data.status !== 'error') {
|
||||
setTimeout(checkStatus, 2000);
|
||||
} else {
|
||||
setPolling(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setPolling(false);
|
||||
setStatus('error');
|
||||
setDetail('Failed to check status');
|
||||
console.error('Error checking status:', error);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (status?.includes("processing") && processId) {
|
||||
interval = setInterval(checkStatus, 5000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [status, processId]);
|
||||
|
||||
const toggleAllBrands = async () => {
|
||||
for (const brand of brands) {
|
||||
await toggleBrandItems(brand.id);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoad && brands.length > 0) {
|
||||
toggleAllBrands();
|
||||
setInitialLoad(false);
|
||||
}
|
||||
}, [brands, initialLoad]);
|
||||
|
||||
const toggleBrandItems = async (brandId) => {
|
||||
const isExpanded = expandedBrand === brandId;
|
||||
if (isExpanded) {
|
||||
setExpandedBrand(null);
|
||||
} else {
|
||||
setExpandedBrand(brandId);
|
||||
if (!itemsMap[brandId]) {
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||
try {
|
||||
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
const dataitems = data.items
|
||||
const validItems = Array.isArray(dataitems)
|
||||
? dataitems.filter(item => item && item.id && item.attributes)
|
||||
: [];
|
||||
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
|
||||
} catch (err) {
|
||||
console.error("Error fetching items:", err);
|
||||
setItemsMap((prev) => ({ ...prev, [brandId]: [] }));
|
||||
}
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast
|
||||
content={status.includes("completed") ?
|
||||
"Products imported successfully!" :
|
||||
`Status: ${status}`}
|
||||
onDismiss={() => setToastActive(false)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const [filters, setFilters] = useState({ make: '', model: '', year: '', drive: '', baseModel: '' });
|
||||
|
||||
const handleFilterChange = (field) => (value) => {
|
||||
setFilters((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const applyFitmentFilters = (items) => {
|
||||
return items.filter((item) => {
|
||||
const tags = item?.attributes?.fitmmentTags || {};
|
||||
return (
|
||||
(!filters.make || tags.make?.includes(filters.make)) &&
|
||||
(!filters.model || tags.model?.includes(filters.model)) &&
|
||||
(!filters.year || tags.year?.includes(filters.year)) &&
|
||||
(!filters.drive || tags.drive?.includes(filters.drive)) &&
|
||||
(!filters.baseModel || tags.baseModel?.includes(filters.baseModel))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Frame>
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
{brands.length === 0 ? (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<p>No brands selected yet.</p>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
) : (
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<IndexTable
|
||||
resourceName={{ singular: "brand", plural: "brands" }}
|
||||
itemCount={brands.length}
|
||||
headings={[
|
||||
{ title: "Brand ID" },
|
||||
{ title: "Logo" },
|
||||
{ title: "Action" },
|
||||
{ title: "Products Count" },
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
{brands.map((brand, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
|
||||
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Thumbnail
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
/>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
|
||||
</IndexTable.Row>
|
||||
)
|
||||
})}
|
||||
</IndexTable>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
|
||||
{brands.map((brand) => {
|
||||
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
|
||||
const uniqueTags = {
|
||||
make: new Set(),
|
||||
model: new Set(),
|
||||
year: new Set(),
|
||||
drive: new Set(),
|
||||
baseModel: new Set(),
|
||||
};
|
||||
|
||||
(itemsMap[brand.id] || []).forEach(item => {
|
||||
const tags = item?.attributes?.fitmmentTags || {};
|
||||
Object.keys(uniqueTags).forEach(key => {
|
||||
(tags[key] || []).forEach(val => uniqueTags[key].add(val));
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
expandedBrand === brand.id &&
|
||||
|
||||
(
|
||||
<Layout.Section fullWidth key={brand.id + "-expanded"}>
|
||||
<Card sectioned>
|
||||
{processId && (
|
||||
<div style={{ marginBottom: "1rem", padding: "1rem", border: "1px solid #e1e1e1", borderRadius: "4px" }}>
|
||||
<p>
|
||||
<strong>Process ID:</strong> {processId}
|
||||
</p>
|
||||
|
||||
<div style={{ margin: "1rem 0" }}>
|
||||
<p>
|
||||
<strong>Status:</strong> {status || "—"}
|
||||
</p>
|
||||
|
||||
{progress > 0 && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={
|
||||
status === 'error' ? 'critical' :
|
||||
status === 'done' ? 'success' : 'highlight'
|
||||
}
|
||||
/>
|
||||
<p style={{ marginTop: "0.25rem", fontSize: "0.85rem" }}>
|
||||
{processedProducts} of {totalProducts} products processed
|
||||
{currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === 'done' && results.length > 0 && (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<p>
|
||||
<strong>Results:</strong> {results.length} products processed successfully
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div style={{ marginTop: "1rem", color: "#ff4d4f" }}>
|
||||
<strong>Error:</strong> {detail}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={checkStatus}
|
||||
loading={polling}
|
||||
style={{ marginTop: "1rem" }}
|
||||
>
|
||||
{status === 'done' ? 'View Results' : 'Check Status'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="brandId" value={brand.id} />
|
||||
<TextField
|
||||
label="Number of products to add"
|
||||
type="number"
|
||||
name="productCount"
|
||||
value={productCount}
|
||||
onChange={(value) => setProductCount(value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
submit
|
||||
primary
|
||||
style={{ marginTop: "1rem" }}
|
||||
loading={status?.includes("processing")}
|
||||
>
|
||||
Add First {productCount} Products to Store
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<Card title="Filter Products by Fitment Tags" sectioned>
|
||||
<Layout>
|
||||
<Layout.Section oneThird>
|
||||
<Select
|
||||
label="Make"
|
||||
options={[{ label: 'All', value: '' }, ...Array.from(uniqueTags.make).map(m => ({ label: m, value: m }))]}
|
||||
onChange={handleFilterChange('make')}
|
||||
value={filters.make}
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section oneThird>
|
||||
<Select
|
||||
label="Model"
|
||||
options={[{ label: 'All', value: '' }, ...Array.from(uniqueTags.model).map(m => ({ label: m, value: m }))]}
|
||||
onChange={handleFilterChange('model')}
|
||||
value={filters.model}
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section oneThird>
|
||||
<Select
|
||||
label="Year"
|
||||
options={[{ label: 'All', value: '' }, ...Array.from(uniqueTags.year).map(y => ({ label: y, value: y }))]}
|
||||
onChange={handleFilterChange('year')}
|
||||
value={filters.year}
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section oneThird>
|
||||
<Select
|
||||
label="Drive"
|
||||
options={[{ label: 'All', value: '' }, ...Array.from(uniqueTags.drive).map(d => ({ label: d, value: d }))]}
|
||||
onChange={handleFilterChange('drive')}
|
||||
value={filters.drive}
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section oneThird>
|
||||
<Select
|
||||
label="Base Model"
|
||||
options={[{ label: 'All', value: '' }, ...Array.from(uniqueTags.baseModel).map(b => ({ label: b, value: b }))]}
|
||||
onChange={handleFilterChange('baseModel')}
|
||||
value={filters.baseModel}
|
||||
/>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Card>
|
||||
|
||||
{filteredItems.map((item) => (
|
||||
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
|
||||
<Layout>
|
||||
<Layout.Section oneThird>
|
||||
<Thumbnail
|
||||
source={
|
||||
item?.attributes?.thumbnail ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={item?.attributes?.product_name || 'Product image'}
|
||||
size="large"
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
|
||||
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}</p>
|
||||
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
|
||||
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</Layout>
|
||||
{toastMarkup}
|
||||
</Page>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { json } from "@remix-run/node";
|
||||
// app/routes/store-credentials.jsx
|
||||
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
@ -13,50 +15,131 @@ import {
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
const SCOPES = [
|
||||
"read_inventory",
|
||||
"read_products",
|
||||
"write_inventory",
|
||||
"write_products",
|
||||
"read_publications",
|
||||
"write_publications",
|
||||
].join(",");
|
||||
const REDIRECT_URI = "https://backend.data4autos.com/auth/callback";
|
||||
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// Fetch shop info and stored credentials
|
||||
const gqlResponse = await admin.graphql(`
|
||||
const resp = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") {
|
||||
value
|
||||
}
|
||||
metafield(namespace: "turn14", key: "credentials") { value }
|
||||
}
|
||||
}
|
||||
`);
|
||||
const shopData = await gqlResponse.json();
|
||||
|
||||
const shopName = shopData?.data?.shop?.name || "Unknown Shop";
|
||||
const metafieldRaw = shopData?.data?.shop?.metafield?.value;
|
||||
|
||||
const { data } = await resp.json();
|
||||
let creds = {};
|
||||
if (metafieldRaw) {
|
||||
try {
|
||||
creds = JSON.parse(metafieldRaw);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored credentials:", err);
|
||||
if (data.shop.metafield?.value) {
|
||||
try { creds = JSON.parse(data.shop.metafield.value); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return json({ shopName, creds });
|
||||
//creds = {};
|
||||
return json({
|
||||
shopName: data.shop.name,
|
||||
shopId: data.shop.id,
|
||||
savedCreds: creds,
|
||||
});
|
||||
};
|
||||
|
||||
// export const action = async ({ request }) => {
|
||||
// const formData = await request.formData();
|
||||
// const { admin } = await authenticate.admin(request);
|
||||
|
||||
// // ——— Handle Shopify-install trigger ———
|
||||
// if (formData.get("install_shopify") === "1") {
|
||||
// const shopName = formData.get("shop_name");
|
||||
// const stateNonce = Math.random().toString(36).slice(2);
|
||||
// const installUrl =
|
||||
// `https://${shopName}.myshopify.com/admin/oauth/authorize` +
|
||||
// `?client_id=${CLIENT_ID}` +
|
||||
// `&scope=${SCOPES}` +
|
||||
// `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
||||
// `&state=${stateNonce}` +
|
||||
// `&grant_options%5B%5D=per-user`;
|
||||
|
||||
// // return the URL instead of redirecting
|
||||
// return json({ confirmationUrl: installUrl });
|
||||
// }
|
||||
|
||||
|
||||
// // ——— Otherwise handle Turn14 token exchange ———
|
||||
// const clientId = formData.get("client_id");
|
||||
// const clientSecret = formData.get("client_secret");
|
||||
// const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
// const shopId = (await shopInfo.json()).data.shop.id;
|
||||
|
||||
// let tokenData;
|
||||
// try {
|
||||
// const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({
|
||||
// grant_type: "client_credentials",
|
||||
// client_id: clientId,
|
||||
// client_secret: clientSecret,
|
||||
// }),
|
||||
// });
|
||||
// tokenData = await tokenRes.json();
|
||||
// if (!tokenRes.ok) {
|
||||
// throw new Error(tokenData.error || "Failed to fetch Turn14 token");
|
||||
// }
|
||||
// } catch (err) {
|
||||
// return json({ success: false, error: err.message });
|
||||
// }
|
||||
|
||||
// // upsert as Shopify metafield
|
||||
// const creds = {
|
||||
// clientId,
|
||||
// clientSecret,
|
||||
// accessToken: tokenData.access_token,
|
||||
// expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
|
||||
// };
|
||||
// const mutation = `
|
||||
// mutation {
|
||||
// metafieldsSet(metafields: [{
|
||||
// ownerId: "${shopId}",
|
||||
// namespace: "turn14",
|
||||
// key: "credentials",
|
||||
// type: "json",
|
||||
// value: "${JSON.stringify(creds).replace(/"/g, '\\"')}"
|
||||
// }]) {
|
||||
// userErrors { message }
|
||||
// }
|
||||
// }
|
||||
// `;
|
||||
// const saveRes = await admin.graphql(mutation);
|
||||
// const saveJson = await saveRes.json();
|
||||
// const errs = saveJson.data.metafieldsSet.userErrors;
|
||||
// if (errs.length) {
|
||||
// return json({ success: false, error: errs[0].message });
|
||||
// }
|
||||
|
||||
// return json({ success: true, creds });
|
||||
// };
|
||||
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const clientId = formData.get("client_id") || "";
|
||||
const clientSecret = formData.get("client_secret") || "";
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// Fetch shop ID
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json())?.data?.shop?.id;
|
||||
// ——— Turn14 token exchange ———
|
||||
const clientId = formData.get("client_id");
|
||||
const clientSecret = formData.get("client_secret");
|
||||
const shopResp = await admin.graphql(`{ shop { id name } }`);
|
||||
const shopJson = await shopResp.json();
|
||||
const shopId = shopJson.data.shop.id;
|
||||
const shopName = shopJson.data.shop.name;
|
||||
|
||||
// Get Turn14 token
|
||||
let tokenData;
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
@ -67,100 +150,88 @@ export const action = async ({ request }) => {
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
tokenData = await tokenRes.json();
|
||||
if (!tokenRes.ok) {
|
||||
return json({
|
||||
success: false,
|
||||
error: tokenData.error || "Failed to fetch access token",
|
||||
});
|
||||
throw new Error(tokenData.error || "Failed to fetch Turn14 token");
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
|
||||
const credentials = {
|
||||
// ——— Upsert to Shopify metafield ———
|
||||
const creds = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
expiresAt,
|
||||
accessToken: tokenData.access_token,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
|
||||
};
|
||||
|
||||
// Upsert as metafield in Shopify
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [
|
||||
{
|
||||
ownerId: "${shopId}"
|
||||
namespace: "turn14"
|
||||
key: "credentials"
|
||||
type: "json"
|
||||
value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
|
||||
}
|
||||
]) {
|
||||
metafields {
|
||||
key
|
||||
value
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
metafieldsSet(metafields: [{
|
||||
ownerId: "${shopId}",
|
||||
namespace: "turn14",
|
||||
key: "credentials",
|
||||
type: "json",
|
||||
value: "${JSON.stringify(creds).replace(/"/g, '\\"')}"
|
||||
}]) {
|
||||
userErrors { message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const result = await saveRes.json();
|
||||
|
||||
if (result?.data?.metafieldsSet?.userErrors?.length) {
|
||||
return json({
|
||||
success: false,
|
||||
error: result.data.metafieldsSet.userErrors[0].message,
|
||||
});
|
||||
const saveJson = await saveRes.json();
|
||||
const errs = saveJson.data.metafieldsSet.userErrors;
|
||||
if (errs.length) {
|
||||
return json({ success: false, error: errs[0].message });
|
||||
}
|
||||
|
||||
// ——— Build the Shopify OAuth URL and return it ———
|
||||
const stateNonce = Math.random().toString(36).slice(2);
|
||||
const installUrl =
|
||||
`https://${shopName}.myshopify.com/admin/oauth/authorize` +
|
||||
`?client_id=${CLIENT_ID}` +
|
||||
`&scope=${SCOPES}` +
|
||||
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
||||
`&state=${stateNonce}`
|
||||
//+ `&grant_options%5B%5D=per-user`;
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
confirmationUrl: installUrl,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Turn14 token fetch failed:", err);
|
||||
return json({
|
||||
success: false,
|
||||
error: "Network or unexpected error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
|
||||
|
||||
export default function StoreCredentials() {
|
||||
const { shopName, shopId, savedCreds } = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
const savedCreds = loaderData?.creds || {};
|
||||
const shopName = loaderData?.shopName || "Shop";
|
||||
|
||||
const [clientId, setClientId] = useState(actionData?.clientId || savedCreds.clientId || "");
|
||||
const [clientSecret, setClientSecret] = useState(actionData?.clientSecret || savedCreds.clientSecret || "");
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
useEffect(() => {
|
||||
if (actionData?.confirmationUrl) {
|
||||
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, [actionData?.confirmationUrl]);
|
||||
|
||||
|
||||
const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || "");
|
||||
const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || "");
|
||||
const connected = actionData?.success || Boolean(savedCreds.accessToken);
|
||||
|
||||
return (
|
||||
<Page title="Data4Autos Turn14 API Settings">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Page title="Data4Autos Turn14 Integration">
|
||||
<TitleBar title="Turn14 & Shopify Connect" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Connected Shop:</strong> {shopName}</p>
|
||||
<p><strong>Shop:</strong> {shopName}</p>
|
||||
</TextContainer>
|
||||
|
||||
{/* —— TURN14 FORM —— */}
|
||||
<Form method="post">
|
||||
<input type="hidden" name="shop_name" value={shopName} />
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
@ -179,29 +250,31 @@ export default function SettingsPage({ standalone = true }) {
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect
|
||||
Connect Turn14
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<InlineError message={`❌ ${actionData.error}`} fieldID="client_id" />
|
||||
<InlineError message={actionData.error} fieldID="client_id" />
|
||||
</TextContainer>
|
||||
)}
|
||||
|
||||
{displayToken && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<p style={{ color: "green" }}>✅ Connection Successful</p>
|
||||
{/* <code style={{
|
||||
background: "#f4f4f4",
|
||||
padding: "10px",
|
||||
display: "block",
|
||||
marginTop: "8px",
|
||||
wordWrap: "break-word"
|
||||
}}>
|
||||
{displayToken}
|
||||
</code> */}
|
||||
{connected && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
|
||||
<p style={{ color: "green" }}>✅ Turn14 connected successfully!</p>
|
||||
|
||||
{/* —— SHOPIFY INSTALL FORM —— */}
|
||||
{/* <Form method="post">
|
||||
<input type="hidden" name="shop_name" value={shopName} />
|
||||
<input type="hidden" name="install_shopify" value="1" />
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect to Shopify
|
||||
</Button>
|
||||
</div>
|
||||
</Form> */}
|
||||
</TextContainer>
|
||||
)}
|
||||
</Card>
|
||||
@ -210,220 +283,3 @@ export default function SettingsPage({ standalone = true }) {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
TextField,
|
||||
Button,
|
||||
InlineError,
|
||||
BlockStack,
|
||||
Text,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const gqlResponse = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const shopData = await gqlResponse.json();
|
||||
const shopName = shopData?.data?.shop?.name || "Unknown Shop";
|
||||
const metafieldRaw = shopData?.data?.shop?.metafield?.value;
|
||||
|
||||
let creds = {};
|
||||
if (metafieldRaw) {
|
||||
try {
|
||||
creds = JSON.parse(metafieldRaw);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored credentials:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ shopName, creds });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const clientId = formData.get("client_id") || "";
|
||||
const clientSecret = formData.get("client_secret") || "";
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json())?.data?.shop?.id;
|
||||
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return json({
|
||||
success: false,
|
||||
error: tokenData.error || "Failed to fetch access token",
|
||||
});
|
||||
}
|
||||
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
|
||||
const credentials = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [
|
||||
{
|
||||
ownerId: "${shopId}"
|
||||
namespace: "turn14"
|
||||
key: "credentials"
|
||||
type: "json"
|
||||
value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
|
||||
}
|
||||
]) {
|
||||
metafields {
|
||||
key
|
||||
value
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const result = await saveRes.json();
|
||||
|
||||
if (result?.data?.metafieldsSet?.userErrors?.length) {
|
||||
return json({
|
||||
success: false,
|
||||
error: result.data.metafieldsSet.userErrors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Turn14 token fetch failed:", err);
|
||||
return json({
|
||||
success: false,
|
||||
error: "Network or unexpected error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
const savedCreds = loaderData?.creds || {};
|
||||
const shopName = loaderData?.shopName || "Shop";
|
||||
|
||||
const [clientId, setClientId] = useState(
|
||||
actionData?.clientId || savedCreds.clientId || ""
|
||||
);
|
||||
const [clientSecret, setClientSecret] = useState(
|
||||
actionData?.clientSecret || savedCreds.clientSecret || ""
|
||||
);
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
|
||||
const content = (
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<BlockStack gap="300">
|
||||
<Text variant="bodyMd"><strong>Connected Shop:</strong> {shopName}</Text>
|
||||
|
||||
<Form method="post">
|
||||
<BlockStack gap="200">
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<Button submit primary>
|
||||
Generate Access Token
|
||||
</Button>
|
||||
</BlockStack>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<InlineError
|
||||
message={`❌ ${actionData.error}`}
|
||||
fieldID="client_id"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayToken && (
|
||||
<BlockStack gap="100">
|
||||
<Text variant="bodySm" fontWeight="semibold" tone="success">
|
||||
✅ Access token:
|
||||
</Text>
|
||||
<code
|
||||
style={{
|
||||
background: "#f4f4f4",
|
||||
padding: "10px",
|
||||
display: "block",
|
||||
wordWrap: "break-word",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{displayToken}
|
||||
</code>
|
||||
</BlockStack>
|
||||
)}
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
return standalone ? <Page title="Turn14 API Settings">{content}</Page> : content;
|
||||
}
|
||||
*/
|
||||
95
app/routes/app.testing.jsx
Normal file
95
app/routes/app.testing.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
Text,
|
||||
BlockStack,
|
||||
Link,
|
||||
Button,
|
||||
Collapsible,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function HelpPage() {
|
||||
const [openIndex, setOpenIndex] = useState(null);
|
||||
|
||||
const toggle = useCallback((index) => {
|
||||
setOpenIndex((prev) => (prev === index ? null : index));
|
||||
}, []);
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
title: "📌 How do I connect my Turn14 account?",
|
||||
content:
|
||||
"Go to the Settings page, enter your Turn14 Client ID and Secret, then click 'Save & Connect'. A green badge will confirm successful connection.",
|
||||
},
|
||||
{
|
||||
title: "📦 Where can I import brands from?",
|
||||
content:
|
||||
"Use the 'Brands' tab in the left menu to view and import available brands from Turn14 into your Shopify store.",
|
||||
},
|
||||
{
|
||||
title: "🔄 How do I sync brand collections?",
|
||||
content:
|
||||
"In the 'Manage Brands' section, select the brands and hit 'Sync to Shopify'. A manual collection will be created or updated.",
|
||||
},
|
||||
{
|
||||
title: "🔐 Is my Turn14 API key secure?",
|
||||
content:
|
||||
"Yes. The credentials are stored using Shopify’s encrypted storage (metafields), ensuring they are safe and secure.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="Testing" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<BlockStack gap="400">
|
||||
<Text variant="headingLg" as="h1">
|
||||
Need Help? You’re in the Right Place!
|
||||
</Text>
|
||||
<Text>
|
||||
This section covers frequently asked questions about the Data4Autos
|
||||
Turn14 integration app.
|
||||
</Text>
|
||||
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index}>
|
||||
<Button
|
||||
onClick={() => toggle(index)}
|
||||
fullWidth
|
||||
disclosure={openIndex === index}
|
||||
variant="plain"
|
||||
>
|
||||
{faq.title}
|
||||
</Button>
|
||||
<Collapsible open={openIndex === index}>
|
||||
<Text as="p" tone="subdued" padding="200">
|
||||
{faq.content}
|
||||
</Text>
|
||||
</Collapsible>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Text tone="subdued">
|
||||
Still have questions? Email us at{" "}
|
||||
<Link url="mailto:support@data4autos.com">
|
||||
support@data4autos.com
|
||||
</Link>
|
||||
</Text>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@ -14,8 +14,8 @@ import {
|
||||
} 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";
|
||||
import { getTurn14AccessTokenFromMetafield } from "../../utils/turn14Token.server";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
279
app/routes/backup/app.brands copy 3.jsx
Normal file
279
app/routes/backup/app.brands copy 3.jsx
Normal file
@ -0,0 +1,279 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -14,8 +14,8 @@ import {
|
||||
} 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";
|
||||
import { getTurn14AccessTokenFromMetafield } from "../../utils/turn14Token.server";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
382
app/routes/backup/app.brands_140725.jsx
Normal file
382
app/routes/backup/app.brands_140725.jsx
Normal file
@ -0,0 +1,382 @@
|
||||
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 ? (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
358
app/routes/backup/app.brands_new.jsx
Normal file
358
app/routes/backup/app.brands_new.jsx
Normal file
@ -0,0 +1,358 @@
|
||||
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 }
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// for (const brand of selectedBrands) {
|
||||
// const exists = existingCollections.find(
|
||||
// (c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||
// );
|
||||
// if (!exists) {
|
||||
// const escapedName = brand.name.replace(/"/g, '\\"');
|
||||
// // Only build the image block if there's a logo URL:
|
||||
// const imageBlock = brand.logo
|
||||
// ? `
|
||||
// image: {
|
||||
// altText: "${escapedName} Logo",
|
||||
// src: "${brand.logo}"
|
||||
// }
|
||||
// `
|
||||
// : "";
|
||||
|
||||
// await admin.graphql(`
|
||||
// mutation {
|
||||
// collectionCreate(input: {
|
||||
// title: "${escapedName}",
|
||||
// descriptionHtml: "Products from brand ${escapedName}"
|
||||
// ${imageBlock}
|
||||
// }) {
|
||||
// collection { id }
|
||||
// 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 { 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>
|
||||
))}
|
||||
</div>
|
||||
</Layout.Section>
|
||||
|
||||
|
||||
</Layout>
|
||||
{toastMarkup}
|
||||
</Page>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
231
app/routes/backup/app.managebrand copy.jsx
Normal file
231
app/routes/backup/app.managebrand copy.jsx
Normal file
@ -0,0 +1,231 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
IndexTable,
|
||||
Card,
|
||||
Thumbnail,
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
TextField,
|
||||
Banner,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(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;
|
||||
|
||||
let brands = [];
|
||||
try {
|
||||
brands = JSON.parse(rawValue);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to parse metafield value:", err);
|
||||
}
|
||||
|
||||
return json({ brands, accessToken });
|
||||
};
|
||||
|
||||
export default function ManageBrandProducts() {
|
||||
const actionData = useActionData();
|
||||
const { brands, accessToken } = useLoaderData();
|
||||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||
const [itemsMap, setItemsMap] = useState({});
|
||||
const [loadingMap, setLoadingMap] = useState({});
|
||||
const [productCount, setProductCount] = useState("0");
|
||||
|
||||
|
||||
|
||||
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
|
||||
// Function to toggle all brands
|
||||
const toggleAllBrands = async () => {
|
||||
for (const brand of brands) {
|
||||
await toggleBrandItems(brand.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Run on initial load
|
||||
useEffect(() => {
|
||||
if (initialLoad && brands.length > 0) {
|
||||
toggleAllBrands();
|
||||
setInitialLoad(false);
|
||||
}
|
||||
}, [brands, initialLoad]);
|
||||
|
||||
|
||||
|
||||
const toggleBrandItems = async (brandId) => {
|
||||
const isExpanded = expandedBrand === brandId;
|
||||
if (isExpanded) {
|
||||
setExpandedBrand(null);
|
||||
} else {
|
||||
setExpandedBrand(brandId);
|
||||
|
||||
if (!itemsMap[brandId]) {
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||
try {
|
||||
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
|
||||
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
setProductCount(data.length)
|
||||
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
|
||||
} catch (err) {
|
||||
console.error("Error fetching items:", err);
|
||||
}
|
||||
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||
} else {
|
||||
setProductCount(itemsMap[brandId].length)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
{brands.length === 0 ? (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<p>No brands selected yet.</p>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
) : (
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<IndexTable
|
||||
resourceName={{ singular: "brand", plural: "brands" }}
|
||||
itemCount={brands.length}
|
||||
headings={[
|
||||
{ title: "Brand ID" },
|
||||
{ title: "Logo" },
|
||||
{ title: "Action" },
|
||||
{ title: "Products Count" },
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
{brands.map((brand, index) => (
|
||||
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
|
||||
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Thumbnail
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
/>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
|
||||
</IndexTable.Row>
|
||||
))}
|
||||
</IndexTable>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
|
||||
{brands.map(
|
||||
(brand) =>
|
||||
expandedBrand === brand.id && (
|
||||
<Layout.Section fullWidth key={brand.id + "-expanded"}>
|
||||
<Card sectioned>
|
||||
{actionData?.success && (
|
||||
<Banner title="✅ Product created!" status="success">
|
||||
<p>
|
||||
{actionData.results.map((r) => (
|
||||
<span key={r.variant.id}>
|
||||
Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
|
||||
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="brandId" value={brand.id} />
|
||||
<TextField
|
||||
label="Number of products to add"
|
||||
type="number"
|
||||
name="productCount"
|
||||
value={productCount}
|
||||
onChange={setProductCount}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button submit primary style={{ marginTop: "1rem" }}>
|
||||
Add First {productCount} Products to Store
|
||||
</Button>
|
||||
</Form>
|
||||
Total Products Count : {(itemsMap[brand.id] || []).length}
|
||||
{(
|
||||
itemsMap[brand.id] && itemsMap[brand.id].length > 0
|
||||
? itemsMap[brand.id]
|
||||
: []
|
||||
).map((item) => (
|
||||
<Card key={item?.id} title={item?.attributes.product_name} sectioned>
|
||||
<Layout>
|
||||
<Layout.Section oneThird>
|
||||
<Thumbnail
|
||||
source={
|
||||
item?.attributes.thumbnail ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={item?.attributes.product_name}
|
||||
size="large"
|
||||
/>
|
||||
</Layout.Section>
|
||||
<Layout.Section>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Part Number:</strong> {item?.attributes.part_number}</p>
|
||||
<p><strong>Category:</strong> {item?.attributes.category} > {item?.attributes.subcategory}</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)
|
||||
)}
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@ -14,12 +14,12 @@ import {
|
||||
Banner,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const res = await admin.graphql(`{
|
||||
@ -49,7 +49,7 @@ export const action = async ({ request }) => {
|
||||
const rawCount = formData.get("productCount");
|
||||
const productCount = parseInt(rawCount, 10) || 10;
|
||||
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
// Fetch items from Turn14 API
|
||||
@ -12,11 +12,11 @@ import {
|
||||
TextField,
|
||||
} from "@shopify/polaris";
|
||||
import { useState } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const res = await admin.graphql(`
|
||||
@ -444,12 +444,12 @@ import {
|
||||
TextField,
|
||||
Banner,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server");
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const res = await admin.graphql(`{
|
||||
429
app/routes/backup/app.settings copy.jsx
Normal file
429
app/routes/backup/app.settings copy.jsx
Normal file
@ -0,0 +1,429 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
TextField,
|
||||
Button,
|
||||
TextContainer,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// Fetch shop info and stored credentials
|
||||
const gqlResponse = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const shopData = await gqlResponse.json();
|
||||
|
||||
const shopName = shopData?.data?.shop?.name || "Unknown Shop";
|
||||
const metafieldRaw = shopData?.data?.shop?.metafield?.value;
|
||||
|
||||
let creds = {};
|
||||
if (metafieldRaw) {
|
||||
try {
|
||||
creds = JSON.parse(metafieldRaw);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored credentials:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ shopName, creds });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const clientId = formData.get("client_id") || "";
|
||||
const clientSecret = formData.get("client_secret") || "";
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// Fetch shop ID
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json())?.data?.shop?.id;
|
||||
|
||||
// Get Turn14 token
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return json({
|
||||
success: false,
|
||||
error: tokenData.error || "Failed to fetch access token",
|
||||
});
|
||||
}
|
||||
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
|
||||
const credentials = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
// Upsert as metafield in Shopify
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [
|
||||
{
|
||||
ownerId: "${shopId}"
|
||||
namespace: "turn14"
|
||||
key: "credentials"
|
||||
type: "json"
|
||||
value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
|
||||
}
|
||||
]) {
|
||||
metafields {
|
||||
key
|
||||
value
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const result = await saveRes.json();
|
||||
|
||||
if (result?.data?.metafieldsSet?.userErrors?.length) {
|
||||
return json({
|
||||
success: false,
|
||||
error: result.data.metafieldsSet.userErrors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Turn14 token fetch failed:", err);
|
||||
return json({
|
||||
success: false,
|
||||
error: "Network or unexpected error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
const savedCreds = loaderData?.creds || {};
|
||||
const shopName = loaderData?.shopName || "Shop";
|
||||
|
||||
const [clientId, setClientId] = useState(actionData?.clientId || savedCreds.clientId || "");
|
||||
const [clientSecret, setClientSecret] = useState(actionData?.clientSecret || savedCreds.clientSecret || "");
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
|
||||
return (
|
||||
<Page title="Data4Autos Turn14 API Settings">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Connected Shop:</strong> {shopName}</p>
|
||||
</TextContainer>
|
||||
|
||||
<Form method="post">
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<InlineError message={`❌ ${actionData.error}`} fieldID="client_id" />
|
||||
</TextContainer>
|
||||
)}
|
||||
|
||||
{displayToken && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<p style={{ color: "green" }}>✅ Connection Successful</p>
|
||||
{/* <code style={{
|
||||
background: "#f4f4f4",
|
||||
padding: "10px",
|
||||
display: "block",
|
||||
marginTop: "8px",
|
||||
wordWrap: "break-word"
|
||||
}}>
|
||||
{displayToken}
|
||||
</code> */}
|
||||
</TextContainer>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
TextField,
|
||||
Button,
|
||||
InlineError,
|
||||
BlockStack,
|
||||
Text,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const gqlResponse = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const shopData = await gqlResponse.json();
|
||||
const shopName = shopData?.data?.shop?.name || "Unknown Shop";
|
||||
const metafieldRaw = shopData?.data?.shop?.metafield?.value;
|
||||
|
||||
let creds = {};
|
||||
if (metafieldRaw) {
|
||||
try {
|
||||
creds = JSON.parse(metafieldRaw);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored credentials:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ shopName, creds });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const clientId = formData.get("client_id") || "";
|
||||
const clientSecret = formData.get("client_secret") || "";
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json())?.data?.shop?.id;
|
||||
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return json({
|
||||
success: false,
|
||||
error: tokenData.error || "Failed to fetch access token",
|
||||
});
|
||||
}
|
||||
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
|
||||
const credentials = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [
|
||||
{
|
||||
ownerId: "${shopId}"
|
||||
namespace: "turn14"
|
||||
key: "credentials"
|
||||
type: "json"
|
||||
value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
|
||||
}
|
||||
]) {
|
||||
metafields {
|
||||
key
|
||||
value
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const result = await saveRes.json();
|
||||
|
||||
if (result?.data?.metafieldsSet?.userErrors?.length) {
|
||||
return json({
|
||||
success: false,
|
||||
error: result.data.metafieldsSet.userErrors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Turn14 token fetch failed:", err);
|
||||
return json({
|
||||
success: false,
|
||||
error: "Network or unexpected error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
const savedCreds = loaderData?.creds || {};
|
||||
const shopName = loaderData?.shopName || "Shop";
|
||||
|
||||
const [clientId, setClientId] = useState(
|
||||
actionData?.clientId || savedCreds.clientId || ""
|
||||
);
|
||||
const [clientSecret, setClientSecret] = useState(
|
||||
actionData?.clientSecret || savedCreds.clientSecret || ""
|
||||
);
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
|
||||
const content = (
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<BlockStack gap="300">
|
||||
<Text variant="bodyMd"><strong>Connected Shop:</strong> {shopName}</Text>
|
||||
|
||||
<Form method="post">
|
||||
<BlockStack gap="200">
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<Button submit primary>
|
||||
Generate Access Token
|
||||
</Button>
|
||||
</BlockStack>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<InlineError
|
||||
message={`❌ ${actionData.error}`}
|
||||
fieldID="client_id"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayToken && (
|
||||
<BlockStack gap="100">
|
||||
<Text variant="bodySm" fontWeight="semibold" tone="success">
|
||||
✅ Access token:
|
||||
</Text>
|
||||
<code
|
||||
style={{
|
||||
background: "#f4f4f4",
|
||||
padding: "10px",
|
||||
display: "block",
|
||||
wordWrap: "break-word",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{displayToken}
|
||||
</code>
|
||||
</BlockStack>
|
||||
)}
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
return standalone ? <Page title="Turn14 API Settings">{content}</Page> : content;
|
||||
}
|
||||
*/
|
||||
208
app/routes/backup/app.settings_working_bak.jsx
Normal file
208
app/routes/backup/app.settings_working_bak.jsx
Normal file
@ -0,0 +1,208 @@
|
||||
// app/routes/store-credentials.jsx
|
||||
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
TextField,
|
||||
Button,
|
||||
TextContainer,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../../shopify.server";
|
||||
|
||||
const SCOPES = [
|
||||
"read_inventory",
|
||||
"read_products",
|
||||
"write_inventory",
|
||||
"write_products",
|
||||
"read_publications",
|
||||
"write_publications",
|
||||
].join(",");
|
||||
const REDIRECT_URI = "https://backend.dine360.ca/auth/callback";
|
||||
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const resp = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") { value }
|
||||
}
|
||||
}
|
||||
`);
|
||||
const { data } = await resp.json();
|
||||
let creds = {};
|
||||
if (data.shop.metafield?.value) {
|
||||
try { creds = JSON.parse(data.shop.metafield.value); } catch { }
|
||||
}
|
||||
creds = {};
|
||||
return json({
|
||||
shopName: data.shop.name,
|
||||
shopId: data.shop.id,
|
||||
savedCreds: creds,
|
||||
});
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
// ——— Handle Shopify-install trigger ———
|
||||
if (formData.get("install_shopify") === "1") {
|
||||
const shopName = formData.get("shop_name");
|
||||
const stateNonce = Math.random().toString(36).slice(2);
|
||||
const installUrl =
|
||||
`https://${shopName}.myshopify.com/admin/oauth/authorize` +
|
||||
`?client_id=${CLIENT_ID}` +
|
||||
`&scope=${SCOPES}` +
|
||||
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
||||
`&state=${stateNonce}` +
|
||||
`&grant_options%5B%5D=per-user`;
|
||||
|
||||
// return the URL instead of redirecting
|
||||
return json({ confirmationUrl: installUrl });
|
||||
}
|
||||
|
||||
|
||||
// ——— Otherwise handle Turn14 token exchange ———
|
||||
const clientId = formData.get("client_id");
|
||||
const clientSecret = formData.get("client_secret");
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json()).data.shop.id;
|
||||
|
||||
let tokenData;
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
tokenData = await tokenRes.json();
|
||||
if (!tokenRes.ok) {
|
||||
throw new Error(tokenData.error || "Failed to fetch Turn14 token");
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
// upsert as Shopify metafield
|
||||
const creds = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken: tokenData.access_token,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
|
||||
};
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [{
|
||||
ownerId: "${shopId}",
|
||||
namespace: "turn14",
|
||||
key: "credentials",
|
||||
type: "json",
|
||||
value: "${JSON.stringify(creds).replace(/"/g, '\\"')}"
|
||||
}]) {
|
||||
userErrors { message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const saveJson = await saveRes.json();
|
||||
const errs = saveJson.data.metafieldsSet.userErrors;
|
||||
if (errs.length) {
|
||||
return json({ success: false, error: errs[0].message });
|
||||
}
|
||||
|
||||
return json({ success: true, creds });
|
||||
};
|
||||
|
||||
export default function StoreCredentials() {
|
||||
const { shopName, shopId, savedCreds } = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.confirmationUrl) {
|
||||
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, [actionData?.confirmationUrl]);
|
||||
|
||||
|
||||
const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || "");
|
||||
const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || "");
|
||||
const connected = actionData?.success || Boolean(savedCreds.accessToken);
|
||||
|
||||
return (
|
||||
<Page title="Data4Autos Turn14 Integration">
|
||||
<TitleBar title="Turn14 & Shopify Connect" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Shop:</strong> {shopName}</p>
|
||||
</TextContainer>
|
||||
|
||||
{/* —— TURN14 FORM —— */}
|
||||
<Form method="post">
|
||||
<input type="hidden" name="shop_name" value={shopName} />
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect Turn14
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<InlineError message={actionData.error} fieldID="client_id" />
|
||||
</TextContainer>
|
||||
)}
|
||||
|
||||
{connected && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
|
||||
<p style={{ color: "green" }}>✅ Turn14 connected successfully!</p>
|
||||
|
||||
{/* —— SHOPIFY INSTALL FORM —— */}
|
||||
<Form method="post">
|
||||
<input type="hidden" name="shop_name" value={shopName} />
|
||||
<input type="hidden" name="install_shopify" value="1" />
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect to Shopify
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</TextContainer>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
82
package-lock.json
generated
82
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"@shopify/polaris": "^12.27.0",
|
||||
"@shopify/shopify-app-remix": "^3.7.0",
|
||||
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
|
||||
"axios": "^1.10.0",
|
||||
"dotenv": "^17.0.0",
|
||||
"isbot": "^5.1.0",
|
||||
"prisma": "^6.2.1",
|
||||
@ -5290,6 +5291,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/auto-bind": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz",
|
||||
@ -5325,6 +5332,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@ -5932,6 +5950,18 @@
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@ -6375,6 +6405,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -6773,7 +6812,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
@ -8016,6 +8054,26 @@
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@ -8056,6 +8114,22 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
@ -12296,6 +12370,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"@shopify/polaris": "^12.27.0",
|
||||
"@shopify/shopify-app-remix": "^3.7.0",
|
||||
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
|
||||
"axios": "^1.10.0",
|
||||
"dotenv": "^17.0.0",
|
||||
"isbot": "^5.1.0",
|
||||
"prisma": "^6.2.1",
|
||||
|
||||
@ -20,7 +20,7 @@ api_version = "2025-04"
|
||||
uri = "/webhooks/app/uninstalled"
|
||||
|
||||
[access_scopes]
|
||||
scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications"
|
||||
scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications,read_fulfillments,write_fulfillments"
|
||||
|
||||
[auth]
|
||||
redirect_urls = ["https://backend.dine360.ca/auth/callback"] # Update this line as well
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user