all updates for frontend baseline
This commit is contained in:
parent
40e05059c8
commit
90bedec2db
BIN
app/assets/turn14-logo.png
Normal file
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
234
app/routes/app.$.jsx
Normal 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 you’re looking for doesn’t exist or may have moved.
|
||||
</Text>
|
||||
|
||||
{/* Actions */}
|
||||
<InlineStack gap="300" align="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
url="/"
|
||||
onClick={(e) => {
|
||||
// keep SPA routers happy—adjust 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>
|
||||
);
|
||||
}
|
||||
@ -14,9 +14,12 @@ import {
|
||||
Button,
|
||||
Modal,
|
||||
TextField,
|
||||
Box,
|
||||
Link,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
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 { Form } from "@remix-run/react";
|
||||
@ -45,17 +48,25 @@ export const loader = async ({ request }) => {
|
||||
const result = await resp.json();
|
||||
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.
|
||||
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 (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
|
||||
@ -79,7 +90,7 @@ export const action = async ({ request }) => {
|
||||
}
|
||||
],
|
||||
trialDays: 7, # ✅ trialDays is a top-level argument!
|
||||
test: true
|
||||
test: false
|
||||
) {
|
||||
confirmationUrl
|
||||
appSubscription {
|
||||
@ -113,6 +124,7 @@ export default function Index() {
|
||||
const [activeModal, setActiveModal] = useState(false);
|
||||
|
||||
const subscription = loaderData?.subscription;
|
||||
const shop = loaderData?.shop;
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log("Action data:", actionData);
|
||||
@ -132,6 +144,22 @@ export default function Index() {
|
||||
const openModal = () => setActiveModal(true);
|
||||
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 (
|
||||
<Page>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
@ -139,52 +167,100 @@ export default function Index() {
|
||||
<Layout.Section>
|
||||
<Card padding="500">
|
||||
<BlockStack gap="400">
|
||||
<InlineStack gap="200" align="center">
|
||||
<Image
|
||||
source={data4autosLogo}
|
||||
alt="Data4Autos Logo"
|
||||
width={120}
|
||||
/>
|
||||
<Text variant="headingLg" as="h1">
|
||||
|
||||
<BlockStack gap="400" align="center">
|
||||
{/* Centered Heading */}
|
||||
<Text variant="headingLg" as="h1" alignment="center">
|
||||
Welcome to your Turn14 Dashboard
|
||||
</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 />
|
||||
|
||||
<BlockStack gap="200">
|
||||
<Text variant="bodyMd">
|
||||
🚀 <b>Data4Autos Turn14 Integration</b> gives you the power to sync
|
||||
<BlockStack gap="800">
|
||||
<Text variant="headingMd" as="h3">
|
||||
🚀 Data4Autos Turn14 Integration gives you the power to sync
|
||||
product brands, manage collections, and automate catalog setup directly from
|
||||
Turn14 to your Shopify store.
|
||||
</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">
|
||||
🔧 Use the left sidebar to:
|
||||
</Text>
|
||||
<BlockStack gap="100">
|
||||
<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>
|
||||
<Text as="span">🔐 Handle secure Turn14 login credentials</Text>
|
||||
</BlockStack>
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<Card key={index} padding="500" background="bg-surface-secondary">
|
||||
<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>
|
||||
<Text tone="subdued">Shopify x Turn14</Text>
|
||||
<Text tone="subdued">Shopify × Turn14</Text>
|
||||
</InlineStack>
|
||||
|
||||
{/* Support Info */}
|
||||
<Text tone="subdued" alignment="center">
|
||||
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>
|
||||
|
||||
<Button onClick={openModal}>
|
||||
{loaderData?.redirectToBilling ? "Proceed to Billing" : "View Subscription Details"}
|
||||
{/* CTA Button */}
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
onClick={openModal}
|
||||
fullWidth
|
||||
>
|
||||
{loaderData?.redirectToBilling
|
||||
? "Proceed to Billing"
|
||||
: "View Subscription Details"}
|
||||
</Button>
|
||||
</BlockStack>
|
||||
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
|
||||
@ -11,12 +11,32 @@ import {
|
||||
Spinner,
|
||||
Toast,
|
||||
Frame,
|
||||
Text,
|
||||
} from "@shopify/polaris";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function checkShopExists(shop) {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
|
||||
);
|
||||
const data = await resp.json();
|
||||
return data.status === 1; // ✅ true if shop exists, false otherwise
|
||||
} catch (err) {
|
||||
console.error("Error checking shop:", err);
|
||||
return false; // default to false if error
|
||||
}
|
||||
}
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
const { admin } = await authenticate.admin(request);
|
||||
@ -49,7 +69,37 @@ export const loader = async ({ request }) => {
|
||||
const gql = await gqlRaw.json();
|
||||
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 }) => {
|
||||
@ -87,16 +137,21 @@ export const action = async ({ request }) => {
|
||||
};
|
||||
|
||||
export default function BrandsPage() {
|
||||
const { brands, collections } = useLoaderData();
|
||||
const { brands, collections, selectedBrandsFromShopify, shop } = useLoaderData();
|
||||
// console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`);
|
||||
const actionData = useActionData() || {};
|
||||
const [selectedIdsold, setSelectedIdsold] = useState([])
|
||||
const [selectedIds, setSelectedIds] = useState(() => {
|
||||
const titles = new Set(collections.map(c => c.title.toLowerCase()));
|
||||
return brands
|
||||
.filter(b => titles.has(b.name.toLowerCase()))
|
||||
.map(b => b.id);
|
||||
});
|
||||
// const [selectedIds, setSelectedIds] = useState(() => {
|
||||
// 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);
|
||||
@ -104,9 +159,33 @@ export default function BrandsPage() {
|
||||
const [status, setStatus] = useState(actionData.status || "");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const [Turn14Enabled, setTurn14Enabled] = useState(null); // null | true | false
|
||||
|
||||
useEffect(() => {
|
||||
if (!shop) {
|
||||
console.log("⚠️ shop is undefined or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await checkShopExists(shop);
|
||||
console.log("✅ API status result:", result, "| shop:", shop);
|
||||
setTurn14Enabled(result);
|
||||
})();
|
||||
}, [shop]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const selids = selectedIds
|
||||
console.log("Selected IDS : ", selids)
|
||||
// console.log("Selected IDS : ", selids)
|
||||
setSelectedIdsold(selids)
|
||||
}, [toastActive]);
|
||||
|
||||
@ -156,13 +235,13 @@ export default function BrandsPage() {
|
||||
};
|
||||
|
||||
var isSubmitting;
|
||||
console.log("actionData", actionData);
|
||||
// console.log("actionData", actionData);
|
||||
if (actionData.status) {
|
||||
isSubmitting = !actionData.status && !actionData.error && !actionData.processId;
|
||||
} else {
|
||||
isSubmitting = false;
|
||||
}
|
||||
console.log("isSubmitting", isSubmitting);
|
||||
// console.log("isSubmitting", isSubmitting);
|
||||
|
||||
const toastMarkup = toastActive ? (
|
||||
<Toast
|
||||
@ -173,60 +252,158 @@ export default function BrandsPage() {
|
||||
|
||||
const selectedBrands = brands.filter(b => selectedIds.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 (
|
||||
<Frame>
|
||||
<Page title="Data4Autos Turn14 Brands List">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
|
||||
<Page fullWidth>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" background="critical" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Form method="post">
|
||||
<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}>
|
||||
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
||||
</Button>
|
||||
</Form>
|
||||
<Card>
|
||||
<div style={{ padding: 24, textAlign: "center" }}>
|
||||
<Text as="h1" variant="headingLg">
|
||||
Turn14 isn’t connected yet
|
||||
</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Primary actions */}
|
||||
<div style={{ marginTop: 24, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
|
||||
<a href={items[0].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="h6" variant="headingMd" fontWeight="bold">
|
||||
{items[0].icon} {items[0].text}
|
||||
</Text>
|
||||
</a>
|
||||
<a href={items[3].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="h6" variant="headingMd" fontWeight="bold">
|
||||
{items[3].icon} {items[3].text}
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<Text as="p" variant="bodySm" tone="subdued">
|
||||
Once connected, you’ll be able to browse brands and sync collections.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Secondary links */}
|
||||
<div style={{ marginTop: 20, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
|
||||
<a href={items[1].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
{items[1].icon} {items[1].text}
|
||||
</Text>
|
||||
</a>
|
||||
<a href={items[2].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
{items[2].icon} {items[2].text}
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// console.log("Selected Brands:", selectedBrands)
|
||||
return (
|
||||
<Frame>
|
||||
<Page fullWidth>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" background="primary" />
|
||||
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", padding: "20px 0px", position: "fixed", zIndex: 2, background: "rgb(241 241 241)", width: "100%", top: 0, }}>
|
||||
<Text as="h1" variant="headingLg">
|
||||
Data4Autos Turn14 Brands List
|
||||
</Text>
|
||||
<br />
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Turn 14 Status:</strong>{" "}
|
||||
{Turn14Enabled === true
|
||||
? "✅ Turn14 x Shopify Connected!"
|
||||
: Turn14Enabled === false
|
||||
? "❌ Turn14 x Shopify Connection Doesn't Exists"
|
||||
: "Checking..."}
|
||||
</p>
|
||||
</div>
|
||||
<Layout >
|
||||
|
||||
|
||||
|
||||
<Layout.Section>
|
||||
<div style={{ display: "flex", 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) && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p>
|
||||
<strong>Process ID:</strong> {actionData.processId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Status:</strong> {status || "—"}
|
||||
</p>
|
||||
<Button onClick={checkStatus} loading={polling}>
|
||||
Check Status
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
label="Search brands"
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder="Type brand name…"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Select All"
|
||||
checked={allFilteredSelected}
|
||||
onChange={toggleSelectAll}
|
||||
/>
|
||||
{/* 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>
|
||||
|
||||
@ -236,30 +413,39 @@ export default function BrandsPage() {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
|
||||
gap: 16,
|
||||
marginTop: "120px"
|
||||
}}
|
||||
>
|
||||
{filteredBrands.map(brand => (
|
||||
{filteredBrands.map((brand) => (
|
||||
<Card key={brand.id} sectioned>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Checkbox
|
||||
label=""
|
||||
checked={selectedIds.includes(brand.id)}
|
||||
onChange={() => toggleSelect(brand.id)}
|
||||
/>
|
||||
<Thumbnail
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
/>
|
||||
<span>{brand.name}</span>
|
||||
<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>
|
||||
|
||||
321
app/routes/app.brands_2408.jsx
Normal file
321
app/routes/app.brands_2408.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -63,23 +63,53 @@ export default function HelpPage() {
|
||||
</Text>
|
||||
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index}>
|
||||
<Button
|
||||
<div
|
||||
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)}
|
||||
fullWidth
|
||||
disclosure={openIndex === index}
|
||||
variant="plain"
|
||||
>
|
||||
{faq.title}
|
||||
</Button>
|
||||
<Collapsible open={openIndex === index}>
|
||||
<Text as="p" tone="subdued" padding="200">
|
||||
{faq.content}
|
||||
<Text variant="bodyLg" fontWeight="bold">
|
||||
{faq.title}
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
<Text tone="subdued">
|
||||
Still have questions? Email us at{" "}
|
||||
<Link url="mailto:support@data4autos.com">
|
||||
|
||||
@ -63,7 +63,7 @@ export default function App() {
|
||||
<Link to="/app/brands">🏷️ Brands</Link>
|
||||
<Link to="/app/managebrand">📦 Manage Brands</Link>
|
||||
<Link to="/app/help">🆘 Help</Link>
|
||||
<Link to="/app/testing">🆘 Testing</Link>
|
||||
{/* <Link to="/app/testing">🆘 Testing</Link> */}
|
||||
</NavMenu>
|
||||
<Outlet />
|
||||
</AppProvider>
|
||||
|
||||
@ -17,10 +17,28 @@ import {
|
||||
Frame,
|
||||
Select,
|
||||
ProgressBar,
|
||||
Checkbox,
|
||||
Text,
|
||||
} 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");
|
||||
@ -43,9 +61,166 @@ export const loader = async ({ request }) => {
|
||||
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 }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const formData = await request.formData();
|
||||
@ -88,7 +263,7 @@ export const action = async ({ request }) => {
|
||||
|
||||
export default function ManageBrandProducts() {
|
||||
const actionData = useActionData();
|
||||
const { brands, accessToken } = useLoaderData();
|
||||
const { shop, brands, accessToken } = useLoaderData();
|
||||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||
const [itemsMap, setItemsMap] = useState({});
|
||||
const [loadingMap, setLoadingMap] = useState({});
|
||||
@ -105,6 +280,27 @@ export default function ManageBrandProducts() {
|
||||
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);
|
||||
@ -218,28 +414,133 @@ export default function ManageBrandProducts() {
|
||||
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 productName = item?.attributes?.product_name || '';
|
||||
const brand = item?.attributes?.brand || '';
|
||||
const partDescription = item?.attributes?.part_description || '';
|
||||
const descriptions = item?.attributes?.descriptions || [];
|
||||
|
||||
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 shopDomain = (shop || "").split(".")[0];
|
||||
|
||||
const items = [
|
||||
{ icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` },
|
||||
{ icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` },
|
||||
{ icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` },
|
||||
{ icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` },
|
||||
];
|
||||
|
||||
// If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen
|
||||
if (Turn14Enabled === false) {
|
||||
return (
|
||||
<Frame>
|
||||
<Page fullWidth>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" background="critical" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<div style={{ padding: 24, textAlign: "center" }}>
|
||||
<Text as="h1" variant="headingLg">
|
||||
Turn14 isn’t connected yet
|
||||
</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Primary actions */}
|
||||
<div style={{ marginTop: 24, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
|
||||
<a href={items[0].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="h6" variant="headingMd" fontWeight="bold">
|
||||
{items[0].icon} {items[0].text}
|
||||
</Text>
|
||||
</a>
|
||||
<a href={items[3].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="h6" variant="headingMd" fontWeight="bold">
|
||||
{items[3].icon} {items[3].text}
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<Text as="p" variant="bodySm" tone="subdued">
|
||||
Once connected, you’ll be able to browse brands and sync collections.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Secondary links */}
|
||||
<div style={{ marginTop: 20, display: "flex", gap: 16, justifyContent: "center", flexWrap: "wrap" }}>
|
||||
<a href={items[1].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
{items[1].icon} {items[1].text}
|
||||
</Text>
|
||||
</a>
|
||||
<a href={items[2].link} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }}>
|
||||
<Text as="p" variant="bodyMd">
|
||||
{items[2].icon} {items[2].text}
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Frame>
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<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 ? (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
@ -253,10 +554,11 @@ export default function ManageBrandProducts() {
|
||||
resourceName={{ singular: "brand", plural: "brands" }}
|
||||
itemCount={brands.length}
|
||||
headings={[
|
||||
{ title: "Brand ID" },
|
||||
{ title: "Logo" },
|
||||
{ title: "Action" },
|
||||
{ title: "Products Count" },
|
||||
{ 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}
|
||||
>
|
||||
@ -266,6 +568,7 @@ export default function ManageBrandProducts() {
|
||||
|
||||
<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={
|
||||
@ -273,15 +576,31 @@ export default function ManageBrandProducts() {
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="small"
|
||||
size="medium"
|
||||
/>
|
||||
</IndexTable.Cell>
|
||||
<IndexTable.Cell>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</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>
|
||||
)
|
||||
})}
|
||||
@ -292,7 +611,7 @@ export default function ManageBrandProducts() {
|
||||
|
||||
{brands.map((brand) => {
|
||||
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 = {
|
||||
make: new Set(),
|
||||
model: new Set(),
|
||||
@ -314,8 +633,8 @@ export default function ManageBrandProducts() {
|
||||
|
||||
(
|
||||
<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" }}>
|
||||
<p>
|
||||
<strong>Process ID:</strong> {processId}
|
||||
@ -359,15 +678,14 @@ export default function ManageBrandProducts() {
|
||||
|
||||
<Button
|
||||
onClick={checkStatus}
|
||||
loading={polling}
|
||||
loading={polling} variant="primary" size="large"
|
||||
style={{ marginTop: "1rem" }}
|
||||
>
|
||||
{status === 'done' ? 'View Results' : 'Check Status'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
</Card>
|
||||
)}
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
@ -380,63 +698,79 @@ export default function ManageBrandProducts() {
|
||||
value={JSON.stringify(filteredItems.map((item) => item.id))}
|
||||
/>
|
||||
<input type="hidden" name="brandId" value={brand.id} />
|
||||
<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
|
||||
style={{ marginTop: "1rem" }}
|
||||
loading={status?.includes("processing")}
|
||||
>
|
||||
Add First {filteredItems.length} Products from {filters.make} to Store
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{filteredItems.map((item) => (
|
||||
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
|
||||
<div style={{ padding: "20px 0px" }}>
|
||||
<Card title="Filter Products by Fitment Tags" 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"
|
||||
<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.Section>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
|
||||
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}</p>
|
||||
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
|
||||
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
|
||||
</Layout>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
</div>
|
||||
<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'} > {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>
|
||||
|
||||
477
app/routes/app.managebrand_130825.jsx
Normal file
477
app/routes/app.managebrand_130825.jsx
Normal 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'} > {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>
|
||||
);
|
||||
}
|
||||
681
app/routes/app.managebrand_200825.jsx
Normal file
681
app/routes/app.managebrand_200825.jsx
Normal 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'} > {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>
|
||||
);
|
||||
}
|
||||
736
app/routes/app.managebrand_2408.jsx
Normal file
736
app/routes/app.managebrand_2408.jsx
Normal 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'} > {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>
|
||||
);
|
||||
}
|
||||
311
app/routes/app.settings copy.jsx
Normal file
311
app/routes/app.settings copy.jsx
Normal 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 >
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
// 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 { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
@ -11,6 +11,12 @@ import {
|
||||
Button,
|
||||
TextContainer,
|
||||
InlineError,
|
||||
Text,
|
||||
BlockStack,
|
||||
Box,
|
||||
Select,
|
||||
Banner,
|
||||
InlineStack,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
@ -22,10 +28,13 @@ const SCOPES = [
|
||||
"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(`
|
||||
@ -34,111 +43,84 @@ export const loader = async ({ request }) => {
|
||||
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 { }
|
||||
}
|
||||
//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({
|
||||
shopName: data.shop.name,
|
||||
shopId: data.shop.id,
|
||||
savedCreds: creds,
|
||||
savedPricing,
|
||||
});
|
||||
};
|
||||
|
||||
// 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 });
|
||||
// };
|
||||
|
||||
|
||||
// ===== 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);
|
||||
|
||||
// ——— Turn14 token exchange ———
|
||||
const clientId = formData.get("client_id");
|
||||
const clientSecret = formData.get("client_secret");
|
||||
// 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");
|
||||
|
||||
const clientId = formData.get("demo_client_id");
|
||||
const clientSecret = formData.get("demo_client_secret");
|
||||
|
||||
let tokenData;
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
|
||||
// ——— Upsert to Shopify metafield ———
|
||||
const creds = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
@ -185,99 +166,180 @@ export const action = async ({ request }) => {
|
||||
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`;
|
||||
`&state=${stateNonce}`;
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
confirmationUrl: installUrl,
|
||||
creds,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ===== COMPONENT =====
|
||||
export default function StoreCredentials() {
|
||||
const { shopName, shopId, savedCreds } = useLoaderData();
|
||||
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 title="Data4Autos Turn14 Integration">
|
||||
<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>
|
||||
<Card sectioned>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Shop:</strong> {shopName}</p>
|
||||
</TextContainer>
|
||||
<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="shop_name" value={shopName} />
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button submit primary>
|
||||
Connect Turn14
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{/* —— TURN14 FORM —— */}
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="connect_turn14" />
|
||||
<input type="hidden" name="demo_client_id" value="671f15b6973625885ee392122113d9ed54103ddd" />
|
||||
<input type="hidden" name="demo_client_secret" value="98907c3b3d92235c99338bd0ce307144b2f66874" />
|
||||
<BlockStack gap="400">
|
||||
<BlockStack gap="200">
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
// value={clientId}
|
||||
value={"********************************************************"}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
// requiredIndicator
|
||||
padding="200"
|
||||
/>
|
||||
</BlockStack>
|
||||
<BlockStack gap="200">
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
// value={clientSecret}
|
||||
value={"********************************************************"}
|
||||
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 && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<InlineError message={actionData.error} fieldID="client_id" />
|
||||
</TextContainer>
|
||||
)}
|
||||
<BlockStack gap="200">
|
||||
<Button submit primary size="large" variant="primary">
|
||||
Connect Turn14
|
||||
</Button>
|
||||
</BlockStack>
|
||||
</BlockStack>
|
||||
</Form>
|
||||
|
||||
{connected && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
|
||||
<p style={{ color: "green" }}>✅ Turn14 connected successfully!</p>
|
||||
{actionData?.error && !actionData?.pricingSaved && (
|
||||
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
|
||||
<InlineError message={actionData.error} fieldID="client_id" />
|
||||
</TextContainer>
|
||||
)}
|
||||
|
||||
{/* —— 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>
|
||||
{(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>
|
||||
|
||||
0
app/routes/app.settings_2222.jsx
Normal file
0
app/routes/app.settings_2222.jsx
Normal file
334
app/routes/app.settings_2508.jsx
Normal file
334
app/routes/app.settings_2508.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 Shopify’s encrypted storage (metafields), ensuring they are safe and secure.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="Testing" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card>
|
||||
<BlockStack gap="400">
|
||||
<Text variant="headingLg" as="h1">
|
||||
Need Help? You’re in the Right Place!
|
||||
</Text>
|
||||
<Text>
|
||||
This section covers frequently asked questions about the Data4Autos
|
||||
Turn14 integration app.
|
||||
</Text>
|
||||
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index}>
|
||||
<Button
|
||||
onClick={() => toggle(index)}
|
||||
fullWidth
|
||||
disclosure={openIndex === index}
|
||||
variant="plain"
|
||||
>
|
||||
{faq.title}
|
||||
</Button>
|
||||
<Collapsible open={openIndex === index}>
|
||||
<Text as="p" tone="subdued" padding="200">
|
||||
{faq.content}
|
||||
</Text>
|
||||
</Collapsible>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Text tone="subdued">
|
||||
Still have questions? Email us at{" "}
|
||||
<Link url="mailto:support@data4autos.com">
|
||||
support@data4autos.com
|
||||
</Link>
|
||||
</Text>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
client_id = "b7534c980967bad619cfdb9d3f837cfa"
|
||||
name = "turn14-test"
|
||||
handle = "d4a-turn14"
|
||||
application_url = "https://shopify.data4autos.com" # Update this line
|
||||
application_url = "https://shop.data4autos.com" # Update this line
|
||||
embedded = true
|
||||
|
||||
[build]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user