all updates for frontend baseline

This commit is contained in:
Manesh 2025-08-29 02:47:12 +00:00
parent 40e05059c8
commit 90bedec2db
17 changed files with 4114 additions and 427 deletions

BIN
app/assets/turn14-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

234
app/routes/app.$.jsx Normal file
View File

@ -0,0 +1,234 @@
// import { Page, Card, Text } from "@shopify/polaris";
// export default function NotFound() {
// return (
// <Page>
// <Card>
// <Text variant="headingLg" as="h1">
// 404 - Page Not Found
// </Text>
// <Text as="p">
// The page you are looking for does not exist.
// </Text>
// </Card>
// </Page>
// );
// }
import {Page, Card, Text, Button, Box, BlockStack, InlineStack, Link} from "@shopify/polaris";
export default function NotFound() {
return (
<Page fullWidth>
{/* Animated gradient background */}
<div className="notfound-gradient">
<div className="gradient-overlay" />
<div className="gradient-blur" />
</div>
{/* Content layer */}
<Box minHeight="80vh" padding="400" position="relative" zIndex="1">
<Box
maxWidth="720px"
marginInline="auto"
paddingBlockStart="400"
paddingBlockEnd="600"
>
{/* Glassy card with subtle border glow */}
<div className="glass-wrap">
<Card>
<Box padding="600">
<BlockStack gap="400" align="center">
{/* Decorative SVG */}
<div className="hero-icon" aria-hidden="true">
<svg
width="140"
height="140"
viewBox="0 0 140 140"
role="img"
aria-label="Lost page"
>
<defs>
<linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="rgba(99, 102, 241, 1)"/>
<stop offset="100%" stopColor="rgba(236, 72, 153, 1)"/>
</linearGradient>
</defs>
<circle cx="70" cy="70" r="64" fill="url(#g1)" opacity="0.15"/>
<g transform="translate(20,22)">
<text
x="50"
y="62"
textAnchor="middle"
fontSize="56"
fontWeight="700"
fill="url(#g1)"
className="shimmer"
>
404
</text>
<circle cx="94" cy="94" r="6" fill="url(#g1)" className="floaty"/>
<circle cx="16" cy="18" r="4" fill="url(#g1)" className="floaty delay"/>
</g>
</svg>
</div>
<Text as="h1" variant="heading2xl" alignment="center">
404 Page not found
</Text>
<Text as="p" variant="bodyLg" tone="subdued" alignment="center">
The page youre looking for doesnt exist or may have moved.
</Text>
{/* Actions */}
<InlineStack gap="300" align="center">
<Button
variant="primary"
url="/"
onClick={(e) => {
// keep SPA routers happyadjust if you use react-router/next/navigation
}}
>
Go to dashboard
</Button>
<Button onClick={() => window.history.length > 1 ? window.history.back() : (window.location.href = "/")}>
Go back
</Button>
<Button variant="tertiary" onClick={() => window.location.reload()}>
Reload
</Button>
</InlineStack>
{/* Helpful links (optional) */}
<InlineStack gap="300" align="center">
<Text as="span" tone="subdued">
Or check{" "}
<Link url="/help" removeUnderline>Help Center</Link>{" "}
or{" "}
<Link url="/contact" removeUnderline>Contact support</Link>.
</Text>
</InlineStack>
</BlockStack>
</Box>
</Card>
</div>
</Box>
</Box>
{/* component-scoped styles */}
<style jsx>{`
.notfound-gradient {
position: fixed;
inset: 0;
overflow: hidden;
z-index: 0;
pointer-events: none;
}
.gradient-overlay {
position: absolute;
inset: -20%;
background: linear-gradient(120deg,
#6366f1 0%,
#8b5cf6 25%,
#ec4899 50%,
#f97316 75%,
#22d3ee 100%
);
animation: gradientShift 14s ease-in-out infinite alternate;
filter: saturate(1.1) contrast(1.05);
opacity: 0.25;
}
.gradient-blur {
position: absolute;
inset: 0;
backdrop-filter: blur(48px);
-webkit-backdrop-filter: blur(48px);
}
.glass-wrap {
position: relative;
border-radius: 16px;
overflow: hidden;
background: rgba(255, 255, 255, 0.65);
box-shadow:
0 10px 30px rgba(17, 24, 39, 0.12),
inset 0 0 0 1px rgba(99, 102, 241, 0.12);
}
.glass-wrap::before {
content: "";
position: absolute;
inset: -1px;
border-radius: 18px;
background: conic-gradient(
from 180deg at 50% 50%,
rgba(99,102,241,0.45),
rgba(236,72,153,0.45),
rgba(34,211,238,0.45),
rgba(99,102,241,0.45)
);
filter: blur(18px);
opacity: 0.35;
z-index: -1;
animation: rotateGlow 10s linear infinite;
}
.hero-icon {
transform: translateZ(0);
will-change: transform, opacity;
}
.floaty {
animation: float 6s ease-in-out infinite;
transform-origin: center;
}
.floaty.delay {
animation-delay: 1.5s;
}
.shimmer {
fill: url(#g1);
animation: shimmer 2.8s ease-in-out infinite;
}
/* Animations */
@keyframes gradientShift {
0% { transform: translate3d(-6%, -6%, 0) scale(1.05); }
100% { transform: translate3d(6%, 6%, 0) scale(1.05); }
}
@keyframes rotateGlow {
to { transform: rotate(360deg); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
@keyframes shimmer {
0%, 100% { opacity: 0.85; }
50% { opacity: 1; }
}
/* Buttons: subtle interactive motion */
:global(button.Polaris-Button) {
transition: transform 160ms ease, box-shadow 160ms ease;
}
:global(button.Polaris-Button:hover) {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(99, 102, 241, 0.15);
}
:global(button.Polaris-Button:active) {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.12);
}
/* Respect dark mode if your app toggles a dark surface */
@media (prefers-color-scheme: dark) {
.glass-wrap {
background: rgba(17, 24, 39, 0.6);
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.35),
inset 0 0 0 1px rgba(148, 163, 184, 0.12);
}
}
`}</style>
</Page>
);
}

View File

@ -14,9 +14,12 @@ import {
Button, Button,
Modal, Modal,
TextField, TextField,
Box,
Link,
} from "@shopify/polaris"; } from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react"; import { TitleBar } from "@shopify/app-bridge-react";
import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists
import turn14DistributorLogo from "../assets/turn14-logo.png";
import { authenticate } from "../shopify.server"; // Shopify server authentication import { authenticate } from "../shopify.server"; // Shopify server authentication
import { Form } from "@remix-run/react"; import { Form } from "@remix-run/react";
@ -45,17 +48,25 @@ export const loader = async ({ request }) => {
const result = await resp.json(); const result = await resp.json();
const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null; const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null;
const { session } = await authenticate.admin(request);
const shop = session.shop;
// For new users, there's no subscription. We will show a "Not subscribed" message. // For new users, there's no subscription. We will show a "Not subscribed" message.
if (!subscription) { if (!subscription) {
return json({ redirectToBilling: true, subscription: null }); return json({ redirectToBilling: true, subscription: null,shop });
} }
// If no active or trial subscription, return redirect signal // If no active or trial subscription, return redirect signal
if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") {
return json({ redirectToBilling: true, subscription }); return json({ redirectToBilling: true, subscription ,shop });
} }
return json({ redirectToBilling: false, subscription }); return json({ redirectToBilling: false, subscription,shop });
}; };
// Action to create subscription // Action to create subscription
@ -79,7 +90,7 @@ export const action = async ({ request }) => {
} }
], ],
trialDays: 7, # trialDays is a top-level argument! trialDays: 7, # trialDays is a top-level argument!
test: true test: false
) { ) {
confirmationUrl confirmationUrl
appSubscription { appSubscription {
@ -113,6 +124,7 @@ export default function Index() {
const [activeModal, setActiveModal] = useState(false); const [activeModal, setActiveModal] = useState(false);
const subscription = loaderData?.subscription; const subscription = loaderData?.subscription;
const shop = loaderData?.shop;
// useEffect(() => { // useEffect(() => {
// console.log("Action data:", actionData); // console.log("Action data:", actionData);
@ -132,6 +144,22 @@ export default function Index() {
const openModal = () => setActiveModal(true); const openModal = () => setActiveModal(true);
const closeModal = () => setActiveModal(false); const closeModal = () => setActiveModal(false);
// const items = [
// { icon: "", text: "Manage API settings", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/settings" },
// { icon: "🏷", text: "Browse and import available brands", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/brands" },
// { icon: "📦", text: "Sync brand collections to Shopify", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/managebrand" },
// { icon: "🔐", text: "Handle secure Turn14 login credentials", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/help" },
// ];
const shopDomain = (shop || "").split(".")[0];; // from the GraphQL query above
const items = [
{ icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` },
{ icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` },
{ icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` },
{ icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` },
];
return ( return (
<Page> <Page>
<TitleBar title="Data4Autos Turn14 Integration" /> <TitleBar title="Data4Autos Turn14 Integration" />
@ -139,52 +167,100 @@ export default function Index() {
<Layout.Section> <Layout.Section>
<Card padding="500"> <Card padding="500">
<BlockStack gap="400"> <BlockStack gap="400">
<InlineStack gap="200" align="center">
<Image <BlockStack gap="400" align="center">
source={data4autosLogo} {/* Centered Heading */}
alt="Data4Autos Logo" <Text variant="headingLg" as="h1" alignment="center">
width={120}
/>
<Text variant="headingLg" as="h1">
Welcome to your Turn14 Dashboard Welcome to your Turn14 Dashboard
</Text> </Text>
</InlineStack>
{/* Logos Row */}
<InlineStack gap="800" align="center" blockAlign="center">
<Image
source={data4autosLogo}
alt="Data4Autos Logo"
width={120}
/>
<Image
source={turn14DistributorLogo}
alt="Turn14 Distributors Logo"
width={200}
/>
</InlineStack>
</BlockStack>
<Divider /> <Divider />
<BlockStack gap="200"> <BlockStack gap="800">
<Text variant="bodyMd"> <Text variant="headingMd" as="h3">
🚀 <b>Data4Autos Turn14 Integration</b> gives you the power to sync 🚀 Data4Autos Turn14 Integration gives you the power to sync
product brands, manage collections, and automate catalog setup directly from product brands, manage collections, and automate catalog setup directly from
Turn14 to your Shopify store. Turn14 to your Shopify store.
</Text> </Text>
<InlineStack gap="400">
<Text as="h3" variant="headingLg" fontWeight="medium">
{/* 🔧 */}
Use the left sidebar to:
</Text>
<Box
paddingBlockStart="800" // top padding
paddingBlockEnd="800" // bottom padding
<Text variant="bodyMd"> style={{
🔧 Use the left sidebar to: display: "grid",
</Text> gridTemplateColumns: "repeat(4, 1fr)",
<BlockStack gap="100"> gap: "1rem",
<Text as="span"> Manage API settings</Text> }}
<Text as="span">🏷 Browse and import available brands</Text> >
<Text as="span">📦 Sync brand collections to Shopify</Text> {items.map((item, index) => (
<Text as="span">🔐 Handle secure Turn14 login credentials</Text> <Card key={index} padding="500" background="bg-surface-secondary">
</BlockStack> <BlockStack align="center" gap="200">
<Text as="p" fontWeight="bold" alignment="center" tone="subdued" variant="bodyMd">
<span style={{ fontSize: "2rem" }}>{item.icon}</span>
</Text>
<a href={item?.link} target="_blank" style={{ textDecoration: "none", color: "primary" }}>
<Text as="h6" alignment="center" fontWeight="bold" variant="headingMd">
{item.text}
</Text>
</a>
</BlockStack>
</Card>
))}
</Box>
</InlineStack>
</BlockStack>
<Divider /> <Divider />
<InlineStack align="center"> <BlockStack gap="400">
{/* Status Badge */}
<InlineStack align="center" gap="400">
<Badge tone="success">Status: Connected</Badge> <Badge tone="success">Status: Connected</Badge>
<Text tone="subdued">Shopify x Turn14</Text> <Text tone="subdued">Shopify × Turn14</Text>
</InlineStack> </InlineStack>
{/* Support Info */}
<Text tone="subdued" alignment="center"> <Text tone="subdued" alignment="center">
Need help? Contact us at{" "} Need help? Contact us at{" "}
<a href="mailto:support@data4autos.com">support@data4autos.com</a> <a href="mailto:support@data4autos.com">
support@data4autos.com
</a>
</Text> </Text>
<Button onClick={openModal}> {/* CTA Button */}
{loaderData?.redirectToBilling ? "Proceed to Billing" : "View Subscription Details"} <Button
size="large"
variant="primary"
onClick={openModal}
fullWidth
>
{loaderData?.redirectToBilling
? "Proceed to Billing"
: "View Subscription Details"}
</Button> </Button>
</BlockStack> </BlockStack>
</BlockStack> </BlockStack>
</Card> </Card>
</Layout.Section> </Layout.Section>

View File

@ -11,12 +11,32 @@ import {
Spinner, Spinner,
Toast, Toast,
Frame, Frame,
Text,
} from "@shopify/polaris"; } from "@shopify/polaris";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TitleBar } from "@shopify/app-bridge-react"; import { TitleBar } from "@shopify/app-bridge-react";
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
import { authenticate } from "../shopify.server"; import { authenticate } from "../shopify.server";
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1; // true if shop exists, false otherwise
} catch (err) {
console.error("Error checking shop:", err);
return false; // default to false if error
}
}
export const loader = async ({ request }) => { export const loader = async ({ request }) => {
const accessToken = await getTurn14AccessTokenFromMetafield(request); const accessToken = await getTurn14AccessTokenFromMetafield(request);
const { admin } = await authenticate.admin(request); const { admin } = await authenticate.admin(request);
@ -49,7 +69,37 @@ export const loader = async ({ request }) => {
const gql = await gqlRaw.json(); 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 });
const res = await admin.graphql(`{
shop {
metafield(namespace: "turn14", key: "selected_brands") {
value
}
}
}`);
const data = await res.json();
const rawValue = data?.data?.shop?.metafield?.value;
let brands = [];
try {
brands = JSON.parse(rawValue);
} catch (err) {
console.error("❌ Failed to parse metafield value:", err);
}
const { session } = await authenticate.admin(request);
const shop = session.shop;
return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop });
}; };
export const action = async ({ request }) => { export const action = async ({ request }) => {
@ -87,16 +137,21 @@ export const action = async ({ request }) => {
}; };
export default function BrandsPage() { export default function BrandsPage() {
const { brands, collections } = useLoaderData(); const { brands, collections, selectedBrandsFromShopify, shop } = useLoaderData();
// console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`);
const actionData = useActionData() || {}; const actionData = useActionData() || {};
const [selectedIdsold, setSelectedIdsold] = useState([]) const [selectedIdsold, setSelectedIdsold] = useState([])
const [selectedIds, setSelectedIds] = useState(() => { // const [selectedIds, setSelectedIds] = useState(() => {
const titles = new Set(collections.map(c => c.title.toLowerCase())); // const titles = new Set(collections.map(c => c.title.toLowerCase()));
return brands // return brands
.filter(b => titles.has(b.name.toLowerCase())) // .filter(b => titles.has(b.name.toLowerCase()))
.map(b => b.id); // .map(b => b.id);
}); // });
const [selectedIds, setSelectedIds] = useState(() => {
return selectedBrandsFromShopify.map(b => b.id);
});
// console.log("Selected IDS : ", selectedIds)
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [filteredBrands, setFilteredBrands] = useState(brands); const [filteredBrands, setFilteredBrands] = useState(brands);
const [toastActive, setToastActive] = useState(false); const [toastActive, setToastActive] = useState(false);
@ -104,9 +159,33 @@ export default function BrandsPage() {
const [status, setStatus] = useState(actionData.status || ""); const [status, setStatus] = useState(actionData.status || "");
const [Turn14Enabled, setTurn14Enabled] = useState(null); // null | true | false
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => { useEffect(() => {
const selids = selectedIds const selids = selectedIds
console.log("Selected IDS : ", selids) // console.log("Selected IDS : ", selids)
setSelectedIdsold(selids) setSelectedIdsold(selids)
}, [toastActive]); }, [toastActive]);
@ -156,13 +235,13 @@ export default function BrandsPage() {
}; };
var isSubmitting; var isSubmitting;
console.log("actionData", actionData); // console.log("actionData", actionData);
if (actionData.status) { if (actionData.status) {
isSubmitting = !actionData.status && !actionData.error && !actionData.processId; isSubmitting = !actionData.status && !actionData.error && !actionData.processId;
} else { } else {
isSubmitting = false; isSubmitting = false;
} }
console.log("isSubmitting", isSubmitting); // console.log("isSubmitting", isSubmitting);
const toastMarkup = toastActive ? ( const toastMarkup = toastActive ? (
<Toast <Toast
@ -173,60 +252,158 @@ export default function BrandsPage() {
const selectedBrands = brands.filter(b => selectedIds.includes(b.id)); const selectedBrands = brands.filter(b => selectedIds.includes(b.id));
const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id)); const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id));
console.log("123456", selectedOldBrands)
const shopDomain = (shop || "").split(".")[0];
const items = [
{ icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` },
{ icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` },
{ icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` },
{ icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` },
];
// If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen
if (Turn14Enabled === false) {
return ( return (
<Frame> <Frame>
<Page title="Data4Autos Turn14 Brands List"> <Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" /> <TitleBar title="Data4Autos Turn14 Integration" background="critical" />
<Layout> <Layout>
<Layout.Section> <Layout.Section>
<Form method="post"> <Card>
<input <div style={{ padding: 24, textAlign: "center" }}>
type="hidden" <Text as="h1" variant="headingLg">
name="selectedBrands" Turn14 isnt connected yet
value={JSON.stringify(selectedBrands)} </Text>
/> <div style={{ marginTop: 8 }}>
<input <Text as="p" variant="bodyMd">
type="hidden" This shop hasnt been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
name="selectedOldBrands" </Text>
value={JSON.stringify(selectedOldBrands)} </div>
/>
<Button primary submit disabled={selectedIds.length === 0 || isSubmitting}> {/* Primary actions */}
{isSubmitting ? <Spinner size="small" /> : "Save Collections"} <div style={{ marginTop: 24, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
</Button> <a href={items[0].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
</Form> <Text as="h6" variant="headingMd" fontWeight="bold">
{items[0].icon} {items[0].text}
</Text>
</a>
<a href={items[3].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[3].icon} {items[3].text}
</Text>
</a>
</div>
<div style={{ marginTop: 28 }}>
<Text as="p" variant="bodySm" tone="subdued">
Once connected, youll be able to browse brands and sync collections.
</Text>
</div>
{/* Secondary links */}
<div style={{ marginTop: 20, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
<a href={items[1].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[1].icon} {items[1].text}
</Text>
</a>
<a href={items[2].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[2].icon} {items[2].text}
</Text>
</a>
</div>
</div>
</Card>
</Layout.Section> </Layout.Section>
</Layout>
</Page>
</Frame>
);
}
// console.log("Selected Brands:", selectedBrands)
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" background="primary" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", padding: "20px 0px", position: "fixed", zIndex: 2, background: "rgb(241 241 241)", width: "100%", top: 0, }}>
<Text as="h1" variant="headingLg">
Data4Autos Turn14 Brands List
</Text>
<br />
</div>
<div>
<p>
<strong>Turn 14 Status:</strong>{" "}
{Turn14Enabled === true
? "✅ Turn14 x Shopify Connected!"
: Turn14Enabled === false
? "❌ Turn14 x Shopify Connection Doesn't Exists"
: "Checking..."}
</p>
</div>
<Layout >
<Layout.Section> <Layout.Section>
<div style={{ display: "flex", gap: 16, alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap", position: "fixed", zIndex: 2, background: "white", padding: "20px", width: "97%", marginTop: "40px", backgroundColor: "#00d1ff" }}>
{(actionData.processId || false) && ( {/* Left side - Search + Select All */}
<div style={{ marginTop: 16 }}> <div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<p> {(actionData?.processId || false) && (
<strong>Process ID:</strong> {actionData.processId} <div>
</p> <p>
<p> <strong>Process ID:</strong> {actionData.processId}
<strong>Status:</strong> {status || "—"} </p>
</p> <p>
<Button onClick={checkStatus} loading={polling}> <strong>Status:</strong> {status || "—"}
Check Status </p>
</Button> <Button onClick={checkStatus} loading={polling}>
</div> Check Status
)} </Button>
<TextField </div>
label="Search brands" )}
value={search}
onChange={setSearch}
placeholder="Type brand name…"
autoComplete="off"
/>
<Checkbox
label="Select All"
checked={allFilteredSelected}
onChange={toggleSelectAll}
/>
<TextField
labelHidden
label="Search brands"
value={search}
onChange={setSearch}
placeholder="Type brand name…"
autoComplete="off"
/>
<Checkbox
label="Select All"
checked={allFilteredSelected}
onChange={toggleSelectAll}
/>
</div>
{/* Right side - Save Button */}
<Form method="post" style={{ display: "flex", alignItems: "center", gap: 8 }}>
<input
type="hidden"
name="selectedBrands"
value={JSON.stringify(selectedBrands)}
/>
<input
type="hidden"
name="selectedOldBrands"
value={JSON.stringify(selectedOldBrands)}
/>
{/* <Button primary submit disabled={selectedIds.length === 0 || isSubmitting}> */}
<Button primary submit disabled={isSubmitting} size="large" variant="primary">
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
</Button>
</Form>
</div> </div>
</Layout.Section> </Layout.Section>
@ -236,30 +413,39 @@ export default function BrandsPage() {
display: "grid", display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
gap: 16, gap: 16,
marginTop: "120px"
}} }}
> >
{filteredBrands.map(brand => ( {filteredBrands.map((brand) => (
<Card key={brand.id} sectioned> <Card key={brand.id} sectioned>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}> <div style={{ position: "relative", textAlign: "center" }}>
<Checkbox {/* Checkbox in top-right corner */}
label="" <div style={{ position: "absolute", top: 0, right: 0 }}>
checked={selectedIds.includes(brand.id)} <Checkbox
onChange={() => toggleSelect(brand.id)} label=""
/> checked={selectedIds.includes(brand.id)}
<Thumbnail onChange={() => toggleSelect(brand.id)}
source={ />
brand.logo || </div>
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
} {/* Brand image */}
alt={brand.name} <div style={{ display: "flex", justifyContent: "center" }}>
size="small" <Thumbnail
/> source={
<span>{brand.name}</span> brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="large"
/>
</div>
{/* Brand name */}
<div style={{ marginTop: "15px", fontWeight: "600", fontSize: "16px", lineHeight: "26px" }}>
{brand.name}
</div>
</div> </div>
</Card> </Card>
))} ))}
</div> </div>
</Layout.Section> </Layout.Section>
</Layout> </Layout>

View File

@ -0,0 +1,321 @@
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
Page,
Layout,
Card,
TextField,
Checkbox,
Button,
Thumbnail,
Spinner,
Toast,
Frame,
Text,
} from "@shopify/polaris";
import { useEffect, useState } from "react";
import { TitleBar } from "@shopify/app-bridge-react";
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
const accessToken = await getTurn14AccessTokenFromMetafield(request);
const { admin } = await authenticate.admin(request);
// fetch brands
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const brandJson = await brandRes.json();
if (!brandRes.ok) {
return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 });
}
// fetch Shopify collections
const gqlRaw = await admin.graphql(`
{
collections(first: 100) {
edges {
node {
id
title
}
}
}
}
`);
const gql = await gqlRaw.json();
const collections = gql?.data?.collections?.edges.map(e => e.node) || [];
const res = await admin.graphql(`{
shop {
metafield(namespace: "turn14", key: "selected_brands") {
value
}
}
}`);
const data = await res.json();
const rawValue = data?.data?.shop?.metafield?.value;
let brands = [];
try {
brands = JSON.parse(rawValue);
} catch (err) {
console.error("❌ Failed to parse metafield value:", err);
}
return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [] });
};
export const action = async ({ request }) => {
const formData = await request.formData();
const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
const selectedOldBrands = JSON.parse(formData.get("selectedOldBrands") || "[]");
const { session } = await authenticate.admin(request);
const shop = session.shop; // "veloxautomotive.myshopify.com"
selectedBrands.forEach(brand => {
delete brand.pricegroups;
});
selectedOldBrands.forEach(brand => {
delete brand.pricegroups;
});
const resp = await fetch("https://backend.data4autos.com/managebrands", {
method: "POST",
headers: {
"Content-Type": "application/json",
"shop-domain": shop,
},
body: JSON.stringify({ shop, selectedBrands, selectedOldBrands }),
});
if (!resp.ok) {
const err = await resp.text();
return json({ error: err }, { status: resp.status });
}
const { processId, status } = await resp.json();
return json({ processId, status });
};
export default function BrandsPage() {
const { brands, collections, selectedBrandsFromShopify } = useLoaderData();
console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`);
const actionData = useActionData() || {};
const [selectedIdsold, setSelectedIdsold] = useState([])
// const [selectedIds, setSelectedIds] = useState(() => {
// const titles = new Set(collections.map(c => c.title.toLowerCase()));
// return brands
// .filter(b => titles.has(b.name.toLowerCase()))
// .map(b => b.id);
// });
const [selectedIds, setSelectedIds] = useState(() => {
return selectedBrandsFromShopify.map(b => b.id);
});
console.log("Selected IDS : ", selectedIds)
const [search, setSearch] = useState("");
const [filteredBrands, setFilteredBrands] = useState(brands);
const [toastActive, setToastActive] = useState(false);
const [polling, setPolling] = useState(false);
const [status, setStatus] = useState(actionData.status || "");
useEffect(() => {
const selids = selectedIds
console.log("Selected IDS : ", selids)
setSelectedIdsold(selids)
}, [toastActive]);
useEffect(() => {
const term = search.toLowerCase();
setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term)));
}, [search, brands]);
useEffect(() => {
if (actionData.status) {
setStatus(actionData.status);
setToastActive(true);
}
}, [actionData.status]);
const checkStatus = async () => {
if (!actionData.processId) return;
setPolling(true);
const resp = await fetch(
`https://backend.data4autos.com/managebrands/status/${actionData.processId}`,
{ headers: { "shop-domain": window.shopify.shop || "" } }
);
const jsonBody = await resp.json();
setStatus(
jsonBody.status + (jsonBody.detail ? ` (${jsonBody.detail})` : "")
);
setPolling(false);
};
const toggleSelect = id =>
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
const allFilteredSelected =
filteredBrands.length > 0 &&
filteredBrands.every(b => selectedIds.includes(b.id));
const toggleSelectAll = () => {
const ids = filteredBrands.map(b => b.id);
if (allFilteredSelected) {
setSelectedIds(prev => prev.filter(id => !ids.includes(id)));
} else {
setSelectedIds(prev => Array.from(new Set([...prev, ...ids])));
}
};
var isSubmitting;
console.log("actionData", actionData);
if (actionData.status) {
isSubmitting = !actionData.status && !actionData.error && !actionData.processId;
} else {
isSubmitting = false;
}
console.log("isSubmitting", isSubmitting);
const toastMarkup = toastActive ? (
<Toast
content="Collections updated successfully!"
onDismiss={() => setToastActive(false)}
/>
) : null;
const selectedBrands = brands.filter(b => selectedIds.includes(b.id));
const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id));
console.log("Selected Brands:", selectedBrands)
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" background="primary" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", padding: "20px 0px", position: "fixed", zIndex: 2, background: "rgb(241 241 241)", width: "100%", top: 0, }}>
<Text as="h1" variant="headingLg">
Data4Autos Turn14 Brands List
</Text>
</div>
<Layout >
<Layout.Section>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap", position: "fixed", zIndex: 2, background: "white", padding: "20px", width: "97%", marginTop: "40px", backgroundColor:"#00d1ff" }}>
{/* Left side - Search + Select All */}
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
{(actionData?.processId || false) && (
<div>
<p>
<strong>Process ID:</strong> {actionData.processId}
</p>
<p>
<strong>Status:</strong> {status || "—"}
</p>
<Button onClick={checkStatus} loading={polling}>
Check Status
</Button>
</div>
)}
<TextField
labelHidden
label="Search brands"
value={search}
onChange={setSearch}
placeholder="Type brand name…"
autoComplete="off"
/>
<Checkbox
label="Select All"
checked={allFilteredSelected}
onChange={toggleSelectAll}
/>
</div>
{/* Right side - Save Button */}
<Form method="post" style={{ display: "flex", alignItems: "center", gap: 8 }}>
<input
type="hidden"
name="selectedBrands"
value={JSON.stringify(selectedBrands)}
/>
<input
type="hidden"
name="selectedOldBrands"
value={JSON.stringify(selectedOldBrands)}
/>
{/* <Button primary submit disabled={selectedIds.length === 0 || isSubmitting}> */}
<Button primary submit disabled={isSubmitting} size="large" variant="primary">
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
</Button>
</Form>
</div>
</Layout.Section>
<Layout.Section>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
gap: 16,
marginTop: "120px"
}}
>
{filteredBrands.map((brand) => (
<Card key={brand.id} sectioned>
<div style={{ position: "relative", textAlign: "center" }}>
{/* Checkbox in top-right corner */}
<div style={{ position: "absolute", top: 0, right: 0 }}>
<Checkbox
label=""
checked={selectedIds.includes(brand.id)}
onChange={() => toggleSelect(brand.id)}
/>
</div>
{/* Brand image */}
<div style={{ display: "flex", justifyContent: "center" }}>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="large"
/>
</div>
{/* Brand name */}
<div style={{ marginTop: "15px", fontWeight: "600", fontSize: "16px", lineHeight: "26px" }}>
{brand.name}
</div>
</div>
</Card>
))}
</div>
</Layout.Section>
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}

View File

@ -63,23 +63,53 @@ export default function HelpPage() {
</Text> </Text>
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<div key={index}> <div
<Button key={index}
style={{
border: "1px solid #E1E3E5",
borderRadius: "8px",
marginBottom: "0px",
overflow: "hidden",
boxShadow: "0 1px 3px rgba(0,0,0,0.05)",
}}
>
{/* Header */}
<div
style={{
background: "#F6F6F7",
padding: "0.75rem 1rem",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
onClick={() => toggle(index)} onClick={() => toggle(index)}
fullWidth
disclosure={openIndex === index}
variant="plain"
> >
{faq.title} <Text variant="bodyLg" fontWeight="bold">
</Button> {faq.title}
<Collapsible open={openIndex === index}>
<Text as="p" tone="subdued" padding="200">
{faq.content}
</Text> </Text>
<span style={{ transform: openIndex === index ? "rotate(90deg)" : "rotate(0deg)", transition: "0.2s" }}>
</span>
</div>
{/* Collapsible Body */}
<Collapsible open={openIndex === index}>
<div
style={{
padding: "1rem",
background: "#FFFFFF",
}}
>
<Text as="p" tone="subdued">
{faq.content}
</Text>
</div>
</Collapsible> </Collapsible>
</div> </div>
))} ))}
<Text tone="subdued"> <Text tone="subdued">
Still have questions? Email us at{" "} Still have questions? Email us at{" "}
<Link url="mailto:support@data4autos.com"> <Link url="mailto:support@data4autos.com">

View File

@ -63,7 +63,7 @@ export default function App() {
<Link to="/app/brands">🏷 Brands</Link> <Link to="/app/brands">🏷 Brands</Link>
<Link to="/app/managebrand">📦 Manage Brands</Link> <Link to="/app/managebrand">📦 Manage Brands</Link>
<Link to="/app/help">🆘 Help</Link> <Link to="/app/help">🆘 Help</Link>
<Link to="/app/testing">🆘 Testing</Link> {/* <Link to="/app/testing">🆘 Testing</Link> */}
</NavMenu> </NavMenu>
<Outlet /> <Outlet />
</AppProvider> </AppProvider>

View File

@ -17,10 +17,28 @@ import {
Frame, Frame,
Select, Select,
ProgressBar, ProgressBar,
Checkbox,
Text,
} from "@shopify/polaris"; } from "@shopify/polaris";
import { authenticate } from "../shopify.server"; import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react"; import { TitleBar } from "@shopify/app-bridge-react";
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1; // true if shop exists, false otherwise
} catch (err) {
console.error("Error checking shop:", err);
return false; // default to false if error
}
}
export const loader = async ({ request }) => { export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request); const { admin } = await authenticate.admin(request);
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
@ -43,9 +61,166 @@ export const loader = async ({ request }) => {
console.error("❌ Failed to parse metafield value:", err); console.error("❌ Failed to parse metafield value:", err);
} }
return json({ brands, accessToken });
const { session } = await authenticate.admin(request);
const shop = session.shop;
return json({ brands, accessToken, shop });
}; };
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
export const action = async ({ request }) => { export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request); const { admin } = await authenticate.admin(request);
const formData = await request.formData(); const formData = await request.formData();
@ -88,7 +263,7 @@ export const action = async ({ request }) => {
export default function ManageBrandProducts() { export default function ManageBrandProducts() {
const actionData = useActionData(); const actionData = useActionData();
const { brands, accessToken } = useLoaderData(); const { shop, brands, accessToken } = useLoaderData();
const [expandedBrand, setExpandedBrand] = useState(null); const [expandedBrand, setExpandedBrand] = useState(null);
const [itemsMap, setItemsMap] = useState({}); const [itemsMap, setItemsMap] = useState({});
const [loadingMap, setLoadingMap] = useState({}); const [loadingMap, setLoadingMap] = useState({});
@ -105,6 +280,27 @@ export default function ManageBrandProducts() {
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [detail, setDetail] = useState(""); const [detail, setDetail] = useState("");
const [filterregulatstock, setfilterregulatstock] = useState(false)
const [Turn14Enabled, setTurn14Enabled] = useState("12345"); // null | true | false
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => { useEffect(() => {
if (actionData?.processId) { if (actionData?.processId) {
setProcessId(actionData.processId); setProcessId(actionData.processId);
@ -218,28 +414,133 @@ export default function ManageBrandProducts() {
setFilters((prev) => ({ ...prev, [field]: value })); setFilters((prev) => ({ ...prev, [field]: value }));
}; };
const applyFitmentFilters = (items) => { const applyFitmentFilters = (items) => {
return items.filter((item) => { return items.filter((item) => {
const tags = item?.attributes?.fitmmentTags || {}; const tags = item?.attributes?.fitmmentTags || {};
return ( const productName = item?.attributes?.product_name || '';
(!filters.make || tags.make?.includes(filters.make)) && const brand = item?.attributes?.brand || '';
(!filters.model || tags.model?.includes(filters.model)) && const partDescription = item?.attributes?.part_description || '';
(!filters.year || tags.year?.includes(filters.year)) && const descriptions = item?.attributes?.descriptions || [];
(!filters.drive || tags.drive?.includes(filters.drive)) &&
(!filters.baseModel || tags.baseModel?.includes(filters.baseModel)) const makeMatch = !filters.make || tags.make?.includes(filters.make) || productName.includes(filters.make) || brand.includes(filters.make) || descriptions.some((desc) => desc.description.includes(filters.make));
);
const modelMatch = !filters.model || tags.model?.includes(filters.model) || productName.includes(filters.model) || brand.includes(filters.model) || descriptions.some((desc) => desc.description.includes(filters.model));
// console.log(`Model check result: ${modelMatch}`);
const yearMatch = !filters.year || tags.year?.includes(filters.year) || productName.includes(filters.year) || brand.includes(filters.year) || descriptions.some((desc) => desc.description.includes(filters.year));
/// console.log(`Year check result: ${yearMatch}`);
const driveMatch = !filters.drive || tags.drive?.includes(filters.drive) || productName.includes(filters.drive) || brand.includes(filters.drive) || descriptions.some((desc) => desc.description.includes(filters.drive));
// console.log(`Drive check result: ${driveMatch}`);
const baseModelMatch = !filters.baseModel || tags.baseModel?.includes(filters.baseModel) || productName.includes(filters.baseModel) || brand.includes(filters.baseModel) || descriptions.some((desc) => desc.description.includes(filters.baseModel));
// console.log(`Base Model check result: ${baseModelMatch}`);
// Combine all the conditions
var isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch// && item.attributes.regular_stock
if (filterregulatstock) {
isMatch = isMatch && item?.attributes?.regular_stock
}
return isMatch;
}); });
}; };
const selectedProductIds = [] const selectedProductIds = []
const shopDomain = (shop || "").split(".")[0];
const items = [
{ icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` },
{ icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` },
{ icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` },
{ icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` },
];
// If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen
if (Turn14Enabled === false) {
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" background="critical" />
<Layout>
<Layout.Section>
<Card>
<div style={{ padding: 24, textAlign: "center" }}>
<Text as="h1" variant="headingLg">
Turn14 isnt connected yet
</Text>
<div style={{ marginTop: 8 }}>
<Text as="p" variant="bodyMd">
This shop hasnt been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
</Text>
</div>
{/* Primary actions */}
<div style={{ marginTop: 24, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
<a href={items[0].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[0].icon} {items[0].text}
</Text>
</a>
<a href={items[3].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[3].icon} {items[3].text}
</Text>
</a>
</div>
<div style={{ marginTop: 28 }}>
<Text as="p" variant="bodySm" tone="subdued">
Once connected, youll be able to browse brands and sync collections.
</Text>
</div>
{/* Secondary links */}
<div style={{ marginTop: 20, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
<a href={items[1].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[1].icon} {items[1].text}
</Text>
</a>
<a href={items[2].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
<Text as="p" variant="bodyMd">
{items[2].icon} {items[2].text}
</Text>
</a>
</div>
</div>
</Card>
</Layout.Section>
</Layout>
</Page>
</Frame>
);
}
return ( return (
<Frame> <Frame>
<Page title="Data4Autos Turn14 Manage Brand Products"> <Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" /> <TitleBar title="Data4Autos Turn14 Integration" />
<Layout> <Layout>
<p>
<strong>Turn 14 Status:</strong>{" "}
{Turn14Enabled === true
? "✅ Turn14 x Shopify Connected!"
: Turn14Enabled === false
? "❌ Turn14 x Shopify Connection Doesn't Exists"
: "Checking..."}
</p>
{brands.length === 0 ? ( {brands.length === 0 ? (
<Layout.Section> <Layout.Section>
<Card sectioned> <Card sectioned>
@ -253,10 +554,11 @@ export default function ManageBrandProducts() {
resourceName={{ singular: "brand", plural: "brands" }} resourceName={{ singular: "brand", plural: "brands" }}
itemCount={brands.length} itemCount={brands.length}
headings={[ headings={[
{ title: "Brand ID" }, { title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand ID</div> },
{ title: "Logo" }, { title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Name</div> },
{ title: "Action" }, { title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Logo</div> },
{ title: "Products Count" }, { title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Action</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Products Count</div> },
]} ]}
selectable={false} selectable={false}
> >
@ -266,6 +568,7 @@ export default function ManageBrandProducts() {
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}> <IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
<IndexTable.Cell>{brand.id}</IndexTable.Cell> <IndexTable.Cell>{brand.id}</IndexTable.Cell>
<IndexTable.Cell>{brand.name}</IndexTable.Cell>
<IndexTable.Cell> <IndexTable.Cell>
<Thumbnail <Thumbnail
source={ source={
@ -273,15 +576,31 @@ export default function ManageBrandProducts() {
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png" "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
} }
alt={brand.name} alt={brand.name}
size="small" size="medium"
/> />
</IndexTable.Cell> </IndexTable.Cell>
<IndexTable.Cell> <IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)}> <Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
{expandedBrand === brand.id ? "Hide Products" : "Show Products"} {expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button> </Button>
</IndexTable.Cell> </IndexTable.Cell>
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell> <IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29", // light teal background
color: "#00d1ff", // dark teal text
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center"
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row> </IndexTable.Row>
) )
})} })}
@ -292,7 +611,7 @@ export default function ManageBrandProducts() {
{brands.map((brand) => { {brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []); const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id)); // console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id));
const uniqueTags = { const uniqueTags = {
make: new Set(), make: new Set(),
model: new Set(), model: new Set(),
@ -314,8 +633,8 @@ export default function ManageBrandProducts() {
( (
<Layout.Section fullWidth key={brand.id + "-expanded"}> <Layout.Section fullWidth key={brand.id + "-expanded"}>
<Card sectioned> {processId && (
{processId && ( <Card sectioned>
<div style={{ marginBottom: "1rem", padding: "1rem", border: "1px solid #e1e1e1", borderRadius: "4px" }}> <div style={{ marginBottom: "1rem", padding: "1rem", border: "1px solid #e1e1e1", borderRadius: "4px" }}>
<p> <p>
<strong>Process ID:</strong> {processId} <strong>Process ID:</strong> {processId}
@ -359,15 +678,14 @@ export default function ManageBrandProducts() {
<Button <Button
onClick={checkStatus} onClick={checkStatus}
loading={polling} loading={polling} variant="primary" size="large"
style={{ marginTop: "1rem" }} style={{ marginTop: "1rem" }}
> >
{status === 'done' ? 'View Results' : 'Check Status'} {status === 'done' ? 'View Results' : 'Check Status'}
</Button> </Button>
</div> </div>
)} </Card>
</Card> )}
<Card title={`Items from ${brand.name}`} sectioned> <Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? ( {loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" /> <Spinner accessibilityLabel="Loading items" size="small" />
@ -380,63 +698,79 @@ export default function ManageBrandProducts() {
value={JSON.stringify(filteredItems.map((item) => item.id))} value={JSON.stringify(filteredItems.map((item) => item.id))}
/> />
<input type="hidden" name="brandId" value={brand.id} /> <input type="hidden" name="brandId" value={brand.id} />
<TextField <div style={{ display: "flex", gap: "1rem", alignItems: "end" }}>
label="Number of products in Selected Filter Make" <TextField
type="number" label="Number of products in Selected Filter Make"
name="productCount" type="number"
value={filteredItems.length} name="productCount"
onChange={(value) => setProductCount(value)} value={filteredItems.length}
autoComplete="off" onChange={(value) => setProductCount(value)}
/> autoComplete="off"
<Button />
submit
primary <Checkbox
style={{ marginTop: "1rem" }} label="Filter Only the Regular Stock"
loading={status?.includes("processing")} checked={filterregulatstock}
> onChange={() => { setfilterregulatstock(!filterregulatstock) }}
Add First {filteredItems.length} Products from {filters.make} to Store />
</Button> <Button
submit
primary variant="primary" size="large"
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {filteredItems.length} Products from {filters.make} to Store
</Button>
</div>
</Form> </Form>
<Card title="Filter Products by Fitment Tags" sectioned> <div style={{ padding: "20px 0px" }}>
<Layout> <Card title="Filter Products by Fitment Tags" sectioned >
<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>
</Card>
{filteredItems.map((item) => (
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
<Layout> <Layout>
<Layout.Section oneThird> <Layout.Section oneThird>
<Thumbnail <Select
source={ label="Make"
item?.attributes?.thumbnail || options={[{ label: 'All', value: '' }, ...Array.from(makes_list).map(m => ({ label: m, value: m }))]}
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png" onChange={handleFilterChange('make')}
} value={filters.make}
alt={item?.attributes?.product_name || 'Product image'}
size="large"
/> />
</Layout.Section> </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'} &gt; {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> </Layout>
</Card> </Card>
))} </div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}>
{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'} &gt; {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>
</div> </div>
)} )}
</Card> </Card>

View File

@ -0,0 +1,477 @@
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 selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]");
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,
selectedProductIds
}),
});
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))
);
});
};
const selectedProductIds = []
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<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: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand ID</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Name</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Logo</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Action</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Products Count</div> },
]}
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>{brand.name}</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="medium"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29", // light teal background
color: "#00d1ff", // dark teal text
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center"
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row>
)
})}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.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"}>
{processId && (
<Card sectioned>
<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} variant="primary" size="large"
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="selectedProductIds"
value={JSON.stringify(filteredItems.map((item) => item.id))}
/>
<input type="hidden" name="brandId" value={brand.id} />
<div style={{ display: "flex", gap: "1rem", alignItems: "end" }}>
<TextField
label="Number of products in Selected Filter Make"
type="number"
name="productCount"
value={filteredItems.length}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Button
submit
primary variant="primary" size="large"
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {filteredItems.length} Products from {filters.make} to Store
</Button>
</div>
</Form>
<div style={{ padding: "20px 0px" }}>
<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>
</Card>
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}>
{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'} &gt; {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>
</div>
)}
</Card>
</Layout.Section>
)
)
})}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}

View File

@ -0,0 +1,681 @@
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 });
};
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
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 selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]");
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,
selectedProductIds
}),
});
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 || {};
const productName = item?.attributes?.product_name || '';
const brand = item?.attributes?.brand || '';
const partDescription = item?.attributes?.part_description || '';
const descriptions = item?.attributes?.descriptions || [];
// // Logging the item being checked and the filters
// console.log("Checking item:", item.id); // Log the item's ID or some unique identifier
// console.log("Filters being applied:", filters);
// // Log the values for each field being checked
// console.log("Checking tags:", tags);
// console.log("Checking product name:", productName);
// console.log("Checking brand:", brand);
// console.log("Checking part description:", partDescription);
// console.log("Checking descriptions:", descriptions.map((desc) => desc.description));
// Create the result for each check
const makeMatch = !filters.make || tags.make?.includes(filters.make) || productName.includes(filters.make) || brand.includes(filters.make) || descriptions.some((desc) => desc.description.includes(filters.make));
// console.log(`Make check result: ${makeMatch}`);
const modelMatch = !filters.model || tags.model?.includes(filters.model) || productName.includes(filters.model) || brand.includes(filters.model) || descriptions.some((desc) => desc.description.includes(filters.model));
// console.log(`Model check result: ${modelMatch}`);
const yearMatch = !filters.year || tags.year?.includes(filters.year) || productName.includes(filters.year) || brand.includes(filters.year) || descriptions.some((desc) => desc.description.includes(filters.year));
/// console.log(`Year check result: ${yearMatch}`);
const driveMatch = !filters.drive || tags.drive?.includes(filters.drive) || productName.includes(filters.drive) || brand.includes(filters.drive) || descriptions.some((desc) => desc.description.includes(filters.drive));
// console.log(`Drive check result: ${driveMatch}`);
const baseModelMatch = !filters.baseModel || tags.baseModel?.includes(filters.baseModel) || productName.includes(filters.baseModel) || brand.includes(filters.baseModel) || descriptions.some((desc) => desc.description.includes(filters.baseModel));
// console.log(`Base Model check result: ${baseModelMatch}`);
// Combine all the conditions
const isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch;
// Log the result of the check (whether item matches the filter or not)
// console.log(`Item ${item.id} match: ${isMatch}`);
// Return the item if it matches
return isMatch;
});
};
// 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))
// );
// });
// };
const selectedProductIds = []
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<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: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand ID</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Name</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Logo</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Action</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Products Count</div> },
]}
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>{brand.name}</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="medium"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29", // light teal background
color: "#00d1ff", // dark teal text
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center"
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row>
)
})}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.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"}>
{processId && (
<Card sectioned>
<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} variant="primary" size="large"
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="selectedProductIds"
value={JSON.stringify(filteredItems.map((item) => item.id))}
/>
<input type="hidden" name="brandId" value={brand.id} />
<div style={{ display: "flex", gap: "1rem", alignItems: "end" }}>
<TextField
label="Number of products in Selected Filter Make"
type="number"
name="productCount"
value={filteredItems.length}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Button
submit
primary variant="primary" size="large"
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {filteredItems.length} Products from {filters.make} to Store
</Button>
</div>
</Form>
<div style={{ padding: "20px 0px" }}>
<Card title="Filter Products by Fitment Tags" sectioned >
<Layout>
<Layout.Section oneThird>
<Select
label="Make"
options={[{ label: 'All', value: '' }, ...Array.from(makes_list).map(m => ({ label: m, value: m }))]}
onChange={handleFilterChange('make')}
value={filters.make}
/>
</Layout.Section>
</Layout>
</Card>
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}>
{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'} &gt; {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>
</div>
)}
</Card>
</Layout.Section>
)
)
})}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}

View File

@ -0,0 +1,736 @@
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,
Checkbox,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react";
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1; // true if shop exists, false otherwise
} catch (err) {
console.error("Error checking shop:", err);
return false; // default to false if error
}
}
export const loader = async ({ request }) => {
const { 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);
}
const { session } = await authenticate.admin(request);
const shop = session.shop;
return json({ brands, accessToken, shop });
};
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
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 selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]");
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,
selectedProductIds
}),
});
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 { shop, 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("");
const [filterregulatstock, setfilterregulatstock] = useState(false)
const [Turn14Enabled, setTurn14Enabled] = useState("12345"); // null | true | false
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => {
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 || {};
const productName = item?.attributes?.product_name || '';
const brand = item?.attributes?.brand || '';
const partDescription = item?.attributes?.part_description || '';
const descriptions = item?.attributes?.descriptions || [];
// // Logging the item being checked and the filters
// console.log("Checking item:", item.id); // Log the item's ID or some unique identifier
// console.log("Filters being applied:", filters);
// // Log the values for each field being checked
// console.log("Checking tags:", tags);
// console.log("Checking product name:", productName);
// console.log("Checking brand:", brand);
// console.log("Checking part description:", partDescription);
// console.log("Checking descriptions:", descriptions.map((desc) => desc.description));
// Create the result for each check
const makeMatch = !filters.make || tags.make?.includes(filters.make) || productName.includes(filters.make) || brand.includes(filters.make) || descriptions.some((desc) => desc.description.includes(filters.make));
// console.log(`Make check result: ${makeMatch}`);
const modelMatch = !filters.model || tags.model?.includes(filters.model) || productName.includes(filters.model) || brand.includes(filters.model) || descriptions.some((desc) => desc.description.includes(filters.model));
// console.log(`Model check result: ${modelMatch}`);
const yearMatch = !filters.year || tags.year?.includes(filters.year) || productName.includes(filters.year) || brand.includes(filters.year) || descriptions.some((desc) => desc.description.includes(filters.year));
/// console.log(`Year check result: ${yearMatch}`);
const driveMatch = !filters.drive || tags.drive?.includes(filters.drive) || productName.includes(filters.drive) || brand.includes(filters.drive) || descriptions.some((desc) => desc.description.includes(filters.drive));
// console.log(`Drive check result: ${driveMatch}`);
const baseModelMatch = !filters.baseModel || tags.baseModel?.includes(filters.baseModel) || productName.includes(filters.baseModel) || brand.includes(filters.baseModel) || descriptions.some((desc) => desc.description.includes(filters.baseModel));
// console.log(`Base Model check result: ${baseModelMatch}`);
// Combine all the conditions
var isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch// && item.attributes.regular_stock
if (filterregulatstock) {
isMatch = isMatch && item?.attributes?.regular_stock
}
// Log the result of the check (whether item matches the filter or not)
// console.log(`Item ${item.id} match: ${isMatch}`);
// Return the item if it matches
return isMatch;
});
};
// 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))
// );
// });
// };
const selectedProductIds = []
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<p>
<strong>Turn 14 Status:</strong> {Turn14Enabled}
</p>
{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: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand ID</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Name</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Logo</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Action</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Products Count</div> },
]}
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>{brand.name}</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="medium"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29", // light teal background
color: "#00d1ff", // dark teal text
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center"
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row>
)
})}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
// console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.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"}>
{processId && (
<Card sectioned>
<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} variant="primary" size="large"
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="selectedProductIds"
value={JSON.stringify(filteredItems.map((item) => item.id))}
/>
<input type="hidden" name="brandId" value={brand.id} />
<div style={{ display: "flex", gap: "1rem", alignItems: "end" }}>
<TextField
label="Number of products in Selected Filter Make"
type="number"
name="productCount"
value={filteredItems.length}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Checkbox
label="Filter Only the Regular Stock"
checked={filterregulatstock}
onChange={() => { setfilterregulatstock(!filterregulatstock) }}
/>
<Button
submit
primary variant="primary" size="large"
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {filteredItems.length} Products from {filters.make} to Store
</Button>
</div>
</Form>
<div style={{ padding: "20px 0px" }}>
<Card title="Filter Products by Fitment Tags" sectioned >
<Layout>
<Layout.Section oneThird>
<Select
label="Make"
options={[{ label: 'All', value: '' }, ...Array.from(makes_list).map(m => ({ label: m, value: m }))]}
onChange={handleFilterChange('make')}
value={filters.make}
/>
</Layout.Section>
</Layout>
</Card>
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}>
{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'} &gt; {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>
</div>
)}
</Card>
</Layout.Section>
)
)
})}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}

View File

@ -0,0 +1,311 @@
// 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,
Text,
BlockStack,
InlineStack,
Box,
} 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.data4autos.com/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 const action = async ({ request }) => {
const formData = await request.formData();
const { admin } = await authenticate.admin(request);
// 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;
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 to 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 });
}
// 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,
confirmationUrl: installUrl,
});
};
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 >
<TitleBar title="Turn14 & Shopify Connect" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", paddingBottom: "20px" }}>
<Text as="h1" variant="headingLg">
Data4Autos Turn14 Integration
</Text>
</div>
<Layout>
<Layout.Section>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<Box maxWidth="450px" width="100%" marginInline="auto" >
<Card sectioned padding="600">
<BlockStack gap="400">
<TextContainer spacing="tight">
<Text variant="headingLg" align="center" fontWeight="medium">Shop: {shopName}</Text>
</TextContainer>
{/* —— TURN14 FORM —— */}
<Form method="post">
<BlockStack gap="400" >
<BlockStack gap="200">
<input type="hidden" name="shop_name" value={shopName} />
<TextField
label="Turn14 Client ID"
name="client_id"
value={clientId}
onChange={setClientId}
autoComplete="off"
requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<TextField
label="Turn14 Client Secret"
name="client_secret"
value={clientSecret}
onChange={setClientSecret}
autoComplete="off"
requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<Button submit primary size="large"
variant="primary">
Connect Turn14
</Button>
</BlockStack>
</BlockStack>
</Form>
</BlockStack>
{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", paddingTop: "5px" }}> 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>
</Box>
</div>
</Layout.Section>
</Layout >
</Page >
);
}

View File

@ -1,8 +1,8 @@
// app/routes/store-credentials.jsx // app/routes/store-credentials.jsx
import { json, redirect } from "@remix-run/node"; import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react"; import { useLoaderData, useActionData, Form } from "@remix-run/react";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {
Page, Page,
Layout, Layout,
@ -11,6 +11,12 @@ import {
Button, Button,
TextContainer, TextContainer,
InlineError, InlineError,
Text,
BlockStack,
Box,
Select,
Banner,
InlineStack,
} from "@shopify/polaris"; } from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react"; import { TitleBar } from "@shopify/app-bridge-react";
import { authenticate } from "../shopify.server"; import { authenticate } from "../shopify.server";
@ -22,10 +28,13 @@ const SCOPES = [
"write_products", "write_products",
"read_publications", "read_publications",
"write_publications", "write_publications",
"read_fulfillments",
"write_fulfillments","read_locations","write_locations"
].join(","); ].join(",");
const REDIRECT_URI = "https://backend.data4autos.com/auth/callback"; const REDIRECT_URI = "https://backend.data4autos.com/auth/callback";
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
// ===== LOADER =====
export const loader = async ({ request }) => { export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request); const { admin } = await authenticate.admin(request);
const resp = await admin.graphql(` const resp = await admin.graphql(`
@ -34,111 +43,84 @@ export const loader = async ({ request }) => {
id id
name name
metafield(namespace: "turn14", key: "credentials") { value } metafield(namespace: "turn14", key: "credentials") { value }
pricing: metafield(namespace: "turn14", key: "pricing_config") { value }
} }
} }
`); `);
const { data } = await resp.json(); const { data } = await resp.json();
let creds = {}; let creds = {};
if (data.shop.metafield?.value) { if (data.shop.metafield?.value) {
try { creds = JSON.parse(data.shop.metafield.value); } catch { } try { creds = JSON.parse(data.shop.metafield.value); } catch { }
} }
//creds = {}; creds = {};
let savedPricing = { priceType: "map", percentage: 0 };
if (data.shop.pricing?.value) {
try {
const p = JSON.parse(data.shop.pricing.value);
savedPricing.priceType = (p.priceType || "map").toLowerCase();
savedPricing.percentage = Number(p.percentage) || 0;
} catch { }
}
return json({ return json({
shopName: data.shop.name, shopName: data.shop.name,
shopId: data.shop.id, shopId: data.shop.id,
savedCreds: creds, savedCreds: creds,
savedPricing,
}); });
}; };
// export const action = async ({ request }) => { // ===== ACTION =====
// 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 }) => { export const action = async ({ request }) => {
const formData = await request.formData(); const formData = await request.formData();
const intent = formData.get("intent"); // "connect_turn14" | "save_pricing"
const { admin } = await authenticate.admin(request); const { admin } = await authenticate.admin(request);
// Turn14 token exchange // we need shop id either way
const clientId = formData.get("client_id");
const clientSecret = formData.get("client_secret");
const shopResp = await admin.graphql(`{ shop { id name } }`); const shopResp = await admin.graphql(`{ shop { id name } }`);
const shopJson = await shopResp.json(); const shopJson = await shopResp.json();
const shopId = shopJson.data.shop.id; const shopId = shopJson.data.shop.id;
const shopName = shopJson.data.shop.name; const shopName = shopJson.data.shop.name;
if (intent === "save_pricing") {
// --- save pricing_config metafield directly ---
const priceTypeRaw = (formData.get("price_type") || "map").toString().toLowerCase();
const percentageRaw = Number(formData.get("percentage") || 0);
const priceType = ["map", "percentage"].includes(priceTypeRaw) ? priceTypeRaw : "map";
const percentage = Number.isFinite(percentageRaw) ? percentageRaw : 0;
const cfg = { priceType, percentage };
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "pricing_config",
type: "json",
value: "${JSON.stringify(cfg).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, pricingSaved: false, error: errs[0].message });
}
return json({ success: true, pricingSaved: true, savedPricing: cfg });
}
// default / legacy: connect Turn14 flow
// const clientId = formData.get("client_id");
// const clientSecret = formData.get("client_secret");
const clientId = formData.get("demo_client_id");
const clientSecret = formData.get("demo_client_secret");
let tokenData; let tokenData;
try { try {
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
@ -158,7 +140,6 @@ export const action = async ({ request }) => {
return json({ success: false, error: err.message }); return json({ success: false, error: err.message });
} }
// Upsert to Shopify metafield
const creds = { const creds = {
clientId, clientId,
clientSecret, clientSecret,
@ -185,99 +166,180 @@ export const action = async ({ request }) => {
return json({ success: false, error: errs[0].message }); 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 stateNonce = Math.random().toString(36).slice(2);
const installUrl = const installUrl =
`https://${shopName}.myshopify.com/admin/oauth/authorize` + `https://${shopName}.myshopify.com/admin/oauth/authorize` +
`?client_id=${CLIENT_ID}` + `?client_id=${CLIENT_ID}` +
`&scope=${SCOPES}` + `&scope=${SCOPES}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&state=${stateNonce}` `&state=${stateNonce}`;
//+ `&grant_options%5B%5D=per-user`;
return json({ return json({
success: true, success: true,
confirmationUrl: installUrl, confirmationUrl: installUrl,
creds,
}); });
}; };
// ===== COMPONENT =====
export default function StoreCredentials() { export default function StoreCredentials() {
const { shopName, shopId, savedCreds } = useLoaderData(); const { shopName, savedCreds, savedPricing } = useLoaderData();
const actionData = useActionData(); const actionData = useActionData();
// open Shopify install after Connect Turn14
useEffect(() => { useEffect(() => {
if (actionData?.confirmationUrl) { if (actionData?.confirmationUrl) {
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
} }
}, [actionData?.confirmationUrl]); }, [actionData?.confirmationUrl]);
const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || ""); const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || "");
const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || ""); const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || "");
const connected = actionData?.success || Boolean(savedCreds.accessToken); const connected = actionData?.success || Boolean(savedCreds.accessToken);
// Pricing UI state (seed from loader or last action)
const initialPriceType = useMemo(
() => (actionData?.savedPricing?.priceType || savedPricing?.priceType || "map"),
[actionData?.savedPricing?.priceType, savedPricing?.priceType]
);
const initialPercentage = useMemo(
() => Number(actionData?.savedPricing?.percentage ?? savedPricing?.percentage ?? 0),
[actionData?.savedPricing?.percentage, savedPricing?.percentage]
);
const [priceType, setPriceType] = useState(initialPriceType);
const [percentage, setPercentage] = useState(initialPercentage);
const pricingSavedOk = actionData?.pricingSaved && actionData?.success && !actionData?.error;
const pricingError = actionData?.pricingSaved === false ? actionData?.error : null;
return ( return (
<Page title="Data4Autos Turn14 Integration"> <Page>
<TitleBar title="Turn14 & Shopify Connect" /> <TitleBar title="Turn14 & Shopify Connect" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", paddingBottom: "20px" }}>
<Text as="h1" variant="headingLg">Data4Autos Turn14 Integration</Text>
</div>
<Layout> <Layout>
<Layout.Section> <Layout.Section>
<Card sectioned> <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<TextContainer spacing="tight"> <Box maxWidth="520px" width="100%" marginInline="auto">
<p><strong>Shop:</strong> {shopName}</p> <Card sectioned padding="600">
</TextContainer> <BlockStack gap="400">
<TextContainer spacing="tight">
<Text variant="headingLg" align="center" fontWeight="medium">Shop: {shopName}</Text>
</TextContainer>
{/* —— TURN14 FORM —— */} {/* —— TURN14 FORM —— */}
<Form method="post"> <Form method="post">
<input type="hidden" name="shop_name" value={shopName} /> <input type="hidden" name="intent" value="connect_turn14" />
<TextField <input type="hidden" name="demo_client_id" value="671f15b6973625885ee392122113d9ed54103ddd" />
label="Turn14 Client ID" <input type="hidden" name="demo_client_secret" value="98907c3b3d92235c99338bd0ce307144b2f66874" />
name="client_id" <BlockStack gap="400">
value={clientId} <BlockStack gap="200">
onChange={setClientId} <TextField
autoComplete="off" label="Turn14 Client ID"
requiredIndicator name="client_id"
/> // value={clientId}
<TextField value={"********************************************************"}
label="Turn14 Client Secret" onChange={setClientId}
name="client_secret" autoComplete="off"
value={clientSecret} // requiredIndicator
onChange={setClientSecret} padding="200"
autoComplete="off" />
requiredIndicator </BlockStack>
/> <BlockStack gap="200">
<div style={{ marginTop: "1rem" }}> <TextField
<Button submit primary> label="Turn14 Client Secret"
Connect Turn14 name="client_secret"
</Button> // value={clientSecret}
</div> value={"********************************************************"}
</Form> onChange={setClientSecret}
autoComplete="off"
// requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<Button submit primary size="large" variant="primary">
Connect Turn14 With Demo Credentials
</Button>
</BlockStack>
{actionData?.error && ( <BlockStack gap="200">
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}> <Button submit primary size="large" variant="primary">
<InlineError message={actionData.error} fieldID="client_id" /> Connect Turn14
</TextContainer> </Button>
)} </BlockStack>
</BlockStack>
</Form>
{connected && ( {actionData?.error && !actionData?.pricingSaved && (
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}> <TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
<p style={{ color: "green" }}> Turn14 connected successfully!</p> <InlineError message={actionData.error} fieldID="client_id" />
</TextContainer>
)}
{/* —— SHOPIFY INSTALL FORM —— */} {(actionData?.success || Boolean(savedCreds.accessToken)) && (
{/* <Form method="post"> <TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
<input type="hidden" name="shop_name" value={shopName} /> <p style={{ color: "green", paddingTop: "5px" }}> Turn14 connected successfully!</p>
<input type="hidden" name="install_shopify" value="1" /> </TextContainer>
<div style={{ marginTop: "1rem" }}> )}
<Button submit primary>
Connect to Shopify {/* —— PRICING CONFIG (direct save via this route) —— */}
</Button> {(actionData?.success || Boolean(savedCreds.accessToken)) && (
</div> <Card title="Pricing configuration" sectioned>
</Form> */} <BlockStack gap="400">
</TextContainer> <Form method="post">
)} <input type="hidden" name="intent" value="save_pricing" />
</Card>
<Select
label="Price type"
options={[
{ label: "MAP (no change)", value: "map" },
{ label: "MAP + % profit", value: "percentage" },
]}
value={priceType}
onChange={(val) => setPriceType(val)}
name="price_type"
/>
{priceType === "percentage" && (
<TextField
type="number"
label="Percentage"
helpText="Add this percentage on top of MAP."
value={String(percentage)}
onChange={(val) => setPercentage(val)}
autoComplete="off"
suffix="%"
min={0}
name="percentage"
/>
)}
<div style={{paddingTop:"15px", textAlign:"end"}}>
<Button submit primary variant="primary" size="large" >Save pricing</Button>
</div>
</Form>
{pricingSavedOk && (
<Banner tone="success">
<p>Pricing configuration saved.</p>
</Banner>
)}
{pricingError && (
<Banner tone="critical">
<p>{pricingError}</p>
</Banner>
)}
</BlockStack>
</Card>
)}
</BlockStack>
</Card>
</Box>
</div>
</Layout.Section> </Layout.Section>
</Layout> </Layout>
</Page> </Page>

View File

View File

@ -0,0 +1,334 @@
// app/routes/store-credentials.jsx
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
import { useEffect, useMemo, useState } from "react";
import {
Page,
Layout,
Card,
TextField,
Button,
TextContainer,
InlineError,
Text,
BlockStack,
Box,
Select,
Banner,
InlineStack,
} 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",
"read_fulfillments",
"write_fulfillments","read_locations","write_locations"
].join(",");
const REDIRECT_URI = "https://backend.data4autos.com/auth/callback";
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
// ===== LOADER =====
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 }
pricing: metafield(namespace: "turn14", key: "pricing_config") { value }
}
}
`);
const { data } = await resp.json();
let creds = {};
if (data.shop.metafield?.value) {
try { creds = JSON.parse(data.shop.metafield.value); } catch { }
}
let savedPricing = { priceType: "map", percentage: 0 };
if (data.shop.pricing?.value) {
try {
const p = JSON.parse(data.shop.pricing.value);
savedPricing.priceType = (p.priceType || "map").toLowerCase();
savedPricing.percentage = Number(p.percentage) || 0;
} catch { }
}
return json({
shopName: data.shop.name,
shopId: data.shop.id,
savedCreds: creds,
savedPricing,
});
};
// ===== ACTION =====
export const action = async ({ request }) => {
const formData = await request.formData();
const intent = formData.get("intent"); // "connect_turn14" | "save_pricing"
const { admin } = await authenticate.admin(request);
// we need shop id either way
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;
if (intent === "save_pricing") {
// --- save pricing_config metafield directly ---
const priceTypeRaw = (formData.get("price_type") || "map").toString().toLowerCase();
const percentageRaw = Number(formData.get("percentage") || 0);
const priceType = ["map", "percentage"].includes(priceTypeRaw) ? priceTypeRaw : "map";
const percentage = Number.isFinite(percentageRaw) ? percentageRaw : 0;
const cfg = { priceType, percentage };
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "pricing_config",
type: "json",
value: "${JSON.stringify(cfg).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, pricingSaved: false, error: errs[0].message });
}
return json({ success: true, pricingSaved: true, savedPricing: cfg });
}
// default / legacy: connect Turn14 flow
const clientId = formData.get("client_id");
const clientSecret = formData.get("client_secret");
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 });
}
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 });
}
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}`;
return json({
success: true,
confirmationUrl: installUrl,
creds,
});
};
// ===== COMPONENT =====
export default function StoreCredentials() {
const { shopName, savedCreds, savedPricing } = useLoaderData();
const actionData = useActionData();
// open Shopify install after Connect Turn14
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);
// Pricing UI state (seed from loader or last action)
const initialPriceType = useMemo(
() => (actionData?.savedPricing?.priceType || savedPricing?.priceType || "map"),
[actionData?.savedPricing?.priceType, savedPricing?.priceType]
);
const initialPercentage = useMemo(
() => Number(actionData?.savedPricing?.percentage ?? savedPricing?.percentage ?? 0),
[actionData?.savedPricing?.percentage, savedPricing?.percentage]
);
const [priceType, setPriceType] = useState(initialPriceType);
const [percentage, setPercentage] = useState(initialPercentage);
const pricingSavedOk = actionData?.pricingSaved && actionData?.success && !actionData?.error;
const pricingError = actionData?.pricingSaved === false ? actionData?.error : null;
return (
<Page>
<TitleBar title="Turn14 & Shopify Connect" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", paddingBottom: "20px" }}>
<Text as="h1" variant="headingLg">Data4Autos Turn14 Integration</Text>
</div>
<Layout>
<Layout.Section>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<Box maxWidth="520px" width="100%" marginInline="auto">
<Card sectioned padding="600">
<BlockStack gap="400">
<TextContainer spacing="tight">
<Text variant="headingLg" align="center" fontWeight="medium">Shop: {shopName}</Text>
</TextContainer>
{/* —— TURN14 FORM —— */}
<Form method="post">
<input type="hidden" name="intent" value="connect_turn14" />
<BlockStack gap="400">
<BlockStack gap="200">
<TextField
label="Turn14 Client ID"
name="client_id"
value={clientId}
onChange={setClientId}
autoComplete="off"
requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<TextField
label="Turn14 Client Secret"
name="client_secret"
value={clientSecret}
onChange={setClientSecret}
autoComplete="off"
requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<Button submit primary size="large" variant="primary">
Connect Turn14
</Button>
</BlockStack>
</BlockStack>
</Form>
{actionData?.error && !actionData?.pricingSaved && (
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
<InlineError message={actionData.error} fieldID="client_id" />
</TextContainer>
)}
{(actionData?.success || Boolean(savedCreds.accessToken)) && (
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
<p style={{ color: "green", paddingTop: "5px" }}> Turn14 connected successfully!</p>
</TextContainer>
)}
{/* —— PRICING CONFIG (direct save via this route) —— */}
{(actionData?.success || Boolean(savedCreds.accessToken)) && (
<Card title="Pricing configuration" sectioned>
<BlockStack gap="400">
<Form method="post">
<input type="hidden" name="intent" value="save_pricing" />
<Select
label="Price type"
options={[
{ label: "MAP (no change)", value: "map" },
{ label: "MAP + % profit", value: "percentage" },
]}
value={priceType}
onChange={(val) => setPriceType(val)}
name="price_type"
/>
{priceType === "percentage" && (
<TextField
type="number"
label="Percentage"
helpText="Add this percentage on top of MAP."
value={String(percentage)}
onChange={(val) => setPercentage(val)}
autoComplete="off"
suffix="%"
min={0}
name="percentage"
/>
)}
<div style={{paddingTop:"15px", textAlign:"end"}}>
<Button submit primary variant="primary" size="large" >Save pricing</Button>
</div>
</Form>
{pricingSavedOk && (
<Banner tone="success">
<p>Pricing configuration saved.</p>
</Banner>
)}
{pricingError && (
<Banner tone="critical">
<p>{pricingError}</p>
</Banner>
)}
</BlockStack>
</Card>
)}
</BlockStack>
</Card>
</Box>
</div>
</Layout.Section>
</Layout>
</Page>
);
}

View File

@ -1,95 +0,0 @@
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 Shopifys 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? Youre 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>
);
}

View File

@ -1,7 +1,7 @@
client_id = "b7534c980967bad619cfdb9d3f837cfa" client_id = "b7534c980967bad619cfdb9d3f837cfa"
name = "turn14-test" name = "turn14-test"
handle = "d4a-turn14" handle = "d4a-turn14"
application_url = "https://shopify.data4autos.com" # Update this line application_url = "https://shop.data4autos.com" # Update this line
embedded = true embedded = true
[build] [build]