12-07-2025 backup
This commit is contained in:
parent
c58cc7c616
commit
fa0d6eb57c
608
app/routes/app._index copy.jsx
Normal file
608
app/routes/app._index copy.jsx
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
/* import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
BlockStack,
|
||||||
|
InlineStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Link,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
await authenticate.admin(request);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const shopify = useAppBridge();
|
||||||
|
const isLoading =
|
||||||
|
["loading", "submitting"].includes(fetcher.state) &&
|
||||||
|
fetcher.formMethod === "POST";
|
||||||
|
const productId = fetcher.data?.product?.id.replace(
|
||||||
|
"gid://shopify/Product/",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
function TabsInsideOfACardExample() {
|
||||||
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(selectedTabIndex) => setSelected(selectedTabIndex),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "settings", content: "⚙️ Settings" },
|
||||||
|
{ id: "brands", content: "🏷️ Brands" },
|
||||||
|
{ id: "manage", content: "📦 Manage Brands" },
|
||||||
|
{ id: "help", content: "🆘 Help" },
|
||||||
|
{ id: "login", content: "🔐 Login" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LegacyCard>
|
||||||
|
<Tabs tabs={tabs} selected={selected} onSelect={handleTabChange}>
|
||||||
|
<LegacyCard.Section title={tabs[selected].content}>
|
||||||
|
<p>Tab {selected} selected</p>
|
||||||
|
</LegacyCard.Section>
|
||||||
|
</Tabs>
|
||||||
|
</LegacyCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productId) {
|
||||||
|
shopify.toast.show("Product created");
|
||||||
|
}
|
||||||
|
}, [productId, shopify]);
|
||||||
|
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar
|
||||||
|
title="Data4Autos Turn 14 Integration"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BlockStack gap="500">
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<BlockStack gap="500">
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<InlineStack gap="300">
|
||||||
|
<Link url="/app/settings" removeUnderline>
|
||||||
|
<Button>Go to Settings Page</Button>
|
||||||
|
</Link>
|
||||||
|
</InlineStack>
|
||||||
|
<InlineStack gap="300">
|
||||||
|
<Link url="/app/brands" removeUnderline>
|
||||||
|
<Button>Go to Brands Page</Button>
|
||||||
|
</Link>
|
||||||
|
</InlineStack>
|
||||||
|
<InlineStack gap="300">
|
||||||
|
<Link url="/app/managebrand" removeUnderline>
|
||||||
|
<Button>Go to Brands Manage Page</Button>
|
||||||
|
</Link>
|
||||||
|
</InlineStack>
|
||||||
|
</BlockStack>
|
||||||
|
</BlockStack>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</BlockStack>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* //woking code
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
BlockStack,
|
||||||
|
InlineStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Link,
|
||||||
|
LegacyCard,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const shopify = useAppBridge();
|
||||||
|
const isLoading =
|
||||||
|
["loading", "submitting"].includes(fetcher.state) &&
|
||||||
|
fetcher.formMethod === "POST";
|
||||||
|
const productId = fetcher.data?.product?.id?.replace(
|
||||||
|
"gid://shopify/Product/",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Temporarily disabling toast to avoid crash
|
||||||
|
// You can safely add App Bridge toast later
|
||||||
|
}, [productId, shopify]);
|
||||||
|
|
||||||
|
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
||||||
|
|
||||||
|
// Tabs logic
|
||||||
|
const [selectedTab, setSelectedTab] = useState(0);
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(selectedTabIndex) => setSelectedTab(selectedTabIndex),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: "settings-tab",
|
||||||
|
content: "⚙️ Settings",
|
||||||
|
panelID: "settings-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brands-tab",
|
||||||
|
content: "🏷️ Brands",
|
||||||
|
panelID: "brands-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "manage-tab",
|
||||||
|
content: "📦 Manage Brands",
|
||||||
|
panelID: "manage-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "help-tab",
|
||||||
|
content: "🆘 Help",
|
||||||
|
panelID: "help-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "login-tab",
|
||||||
|
content: "🔐 Login",
|
||||||
|
panelID: "login-content",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar title="Data4Autos Turn 14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
||||||
|
<LegacyCard.Section title={tabs[selectedTab].content}>
|
||||||
|
{selectedTab === 0 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Configure Turn14 integration settings.</Text>
|
||||||
|
<Link url="/app/settings">
|
||||||
|
<Button>Go to Settings Page</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
{selectedTab === 1 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>View available brands from Turn14.</Text>
|
||||||
|
<Link url="/app/brands">
|
||||||
|
<Button>Go to Brands Page</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
{selectedTab === 2 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Manage your synced brand collections.</Text>
|
||||||
|
<Link url="/app/managebrand">
|
||||||
|
<Button>Go to Manage Brands</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
{selectedTab === 3 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Help and documentation links.</Text>
|
||||||
|
<Button url="https://data4autos.com/help" external>
|
||||||
|
Open Help Docs
|
||||||
|
</Button>
|
||||||
|
<Button url="mailto:support@data4autos.com" tone="critical">
|
||||||
|
Contact Support
|
||||||
|
</Button>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
{selectedTab === 4 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Login to manage Turn14 credentials.</Text>
|
||||||
|
<Link url="/app/login">
|
||||||
|
<Button>Go to Login</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
</LegacyCard.Section>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
/* import { Page, Layout, Card, Text, BlockStack } from "@shopify/polaris";
|
||||||
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text variant="headingLg" as="h2">
|
||||||
|
Welcome to your Turn14 integration dashboard!
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Use the navigation in the left sidebar to manage settings, view brands,
|
||||||
|
sync collections, and more.
|
||||||
|
</Text>
|
||||||
|
</BlockStack>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
BlockStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
InlineStack,
|
||||||
|
Image,
|
||||||
|
Divider,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
|
import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
<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">
|
||||||
|
Welcome to your Turn14 Dashboard
|
||||||
|
</Text>
|
||||||
|
</InlineStack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text variant="bodyMd">
|
||||||
|
🚀 <b>Data4Autos Turn14 Integration</b> gives you the power to sync
|
||||||
|
product brands, manage collections, and automate catalog setup directly from
|
||||||
|
Turn14 to your Shopify store.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<InlineStack align="center">
|
||||||
|
<Badge tone="success">Status: Connected</Badge>
|
||||||
|
<Text tone="subdued">Shopify x Turn14</Text>
|
||||||
|
</InlineStack>
|
||||||
|
|
||||||
|
<Text tone="subdued" alignment="center">
|
||||||
|
Need help? Contact us at{" "}
|
||||||
|
<a href="mailto:support@data4autos.com">support@data4autos.com</a>
|
||||||
|
</Text>
|
||||||
|
</BlockStack>
|
||||||
|
</BlockStack>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
LegacyCard,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const shopify = useAppBridge();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const productId = fetcher.data?.product?.id?.replace(
|
||||||
|
"gid://shopify/Product/",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// You can add toast messages here if needed
|
||||||
|
}, [productId, shopify]);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: "settings-tab",
|
||||||
|
content: "⚙️ Settings",
|
||||||
|
panelID: "settings-content",
|
||||||
|
to: "/app/settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brands-tab",
|
||||||
|
content: "🏷️ Brands",
|
||||||
|
panelID: "brands-content",
|
||||||
|
to: "/app/brands",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "manage-tab",
|
||||||
|
content: "📦 Manage Brands",
|
||||||
|
panelID: "manage-content",
|
||||||
|
to: "/app/managebrand",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "help-tab",
|
||||||
|
content: "🆘 Help",
|
||||||
|
panelID: "help-content",
|
||||||
|
to: "/app/help",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "login-tab",
|
||||||
|
content: "🔐 Login",
|
||||||
|
panelID: "login-content",
|
||||||
|
to: "/app/login",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState(0);
|
||||||
|
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(selectedTabIndex) => {
|
||||||
|
setSelectedTab(selectedTabIndex);
|
||||||
|
navigate(tabs[selectedTabIndex].to);
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar title="Data4Autos Turn 14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
||||||
|
<LegacyCard.Section title={tabs[selectedTab].content}>
|
||||||
|
<p>Redirecting to {tabs[selectedTab].content}...</p>
|
||||||
|
</LegacyCard.Section>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useFetcher } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
BlockStack,
|
||||||
|
InlineStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Link,
|
||||||
|
LegacyCard,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
import SettingsPage from "./app.settings"; // Adjust the path if needed
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const shopify = useAppBridge();
|
||||||
|
const isLoading =
|
||||||
|
["loading", "submitting"].includes(fetcher.state) &&
|
||||||
|
fetcher.formMethod === "POST";
|
||||||
|
const productId = fetcher.data?.product?.id?.replace(
|
||||||
|
"gid://shopify/Product/",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Toast placeholder (disabled)
|
||||||
|
}, [productId, shopify]);
|
||||||
|
|
||||||
|
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState(0);
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(selectedTabIndex) => setSelectedTab(selectedTabIndex),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: "settings-tab",
|
||||||
|
content: "⚙️ Settings",
|
||||||
|
panelID: "settings-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brands-tab",
|
||||||
|
content: "🏷️ Brands",
|
||||||
|
panelID: "brands-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "manage-tab",
|
||||||
|
content: "📦 Manage Brands",
|
||||||
|
panelID: "manage-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "help-tab",
|
||||||
|
content: "🆘 Help",
|
||||||
|
panelID: "help-content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "login-tab",
|
||||||
|
content: "🔐 Login",
|
||||||
|
panelID: "login-content",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<TitleBar title="Data4Autos Turn 14 Integration" />
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
||||||
|
<LegacyCard.Section title={tabs[selectedTab].content}>
|
||||||
|
{selectedTab === 0 && (
|
||||||
|
<SettingsPage standalone={false} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedTab === 1 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>View available brands from Turn14.</Text>
|
||||||
|
<Link url="/app/brands">
|
||||||
|
<Button>Go to Brands Page</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedTab === 2 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Manage your synced brand collections.</Text>
|
||||||
|
<Link url="/app/managebrand">
|
||||||
|
<Button>Go to Manage Brands</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedTab === 3 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Help and documentation links.</Text>
|
||||||
|
<Button url="https://data4autos.com/help" external>
|
||||||
|
Open Help Docs
|
||||||
|
</Button>
|
||||||
|
<Button url="mailto:support@data4autos.com" tone="critical">
|
||||||
|
Contact Support
|
||||||
|
</Button>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedTab === 4 && (
|
||||||
|
<BlockStack gap="200">
|
||||||
|
<Text>Login to manage Turn14 credentials.</Text>
|
||||||
|
<Link url="/app/login">
|
||||||
|
<Button>Go to Login</Button>
|
||||||
|
</Link>
|
||||||
|
</BlockStack>
|
||||||
|
)}
|
||||||
|
</LegacyCard.Section>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
@ -1,293 +1,6 @@
|
|||||||
/* import { useEffect, useState, useCallback } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useFetcher } from "@remix-run/react";
|
import { json } from "@remix-run/node";
|
||||||
import {
|
import { useLoaderData, useActionData, useSubmit } from "@remix-run/react";
|
||||||
Page,
|
|
||||||
Layout,
|
|
||||||
Card,
|
|
||||||
Tabs,
|
|
||||||
Button,
|
|
||||||
BlockStack,
|
|
||||||
InlineStack,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Link,
|
|
||||||
} from "@shopify/polaris";
|
|
||||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
|
||||||
import { authenticate } from "../shopify.server";
|
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
|
||||||
await authenticate.admin(request);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const action = async ({ request }) => {
|
|
||||||
const { admin } = await authenticate.admin(request);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
const shopify = useAppBridge();
|
|
||||||
const isLoading =
|
|
||||||
["loading", "submitting"].includes(fetcher.state) &&
|
|
||||||
fetcher.formMethod === "POST";
|
|
||||||
const productId = fetcher.data?.product?.id.replace(
|
|
||||||
"gid://shopify/Product/",
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
function TabsInsideOfACardExample() {
|
|
||||||
const [selected, setSelected] = useState(0);
|
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
|
||||||
(selectedTabIndex) => setSelected(selectedTabIndex),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ id: "settings", content: "⚙️ Settings" },
|
|
||||||
{ id: "brands", content: "🏷️ Brands" },
|
|
||||||
{ id: "manage", content: "📦 Manage Brands" },
|
|
||||||
{ id: "help", content: "🆘 Help" },
|
|
||||||
{ id: "login", content: "🔐 Login" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LegacyCard>
|
|
||||||
<Tabs tabs={tabs} selected={selected} onSelect={handleTabChange}>
|
|
||||||
<LegacyCard.Section title={tabs[selected].content}>
|
|
||||||
<p>Tab {selected} selected</p>
|
|
||||||
</LegacyCard.Section>
|
|
||||||
</Tabs>
|
|
||||||
</LegacyCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (productId) {
|
|
||||||
shopify.toast.show("Product created");
|
|
||||||
}
|
|
||||||
}, [productId, shopify]);
|
|
||||||
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TitleBar
|
|
||||||
title="Data4Autos Turn 14 Integration"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BlockStack gap="500">
|
|
||||||
<Layout>
|
|
||||||
<Layout.Section>
|
|
||||||
<Card>
|
|
||||||
<BlockStack gap="500">
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<InlineStack gap="300">
|
|
||||||
<Link url="/app/settings" removeUnderline>
|
|
||||||
<Button>Go to Settings Page</Button>
|
|
||||||
</Link>
|
|
||||||
</InlineStack>
|
|
||||||
<InlineStack gap="300">
|
|
||||||
<Link url="/app/brands" removeUnderline>
|
|
||||||
<Button>Go to Brands Page</Button>
|
|
||||||
</Link>
|
|
||||||
</InlineStack>
|
|
||||||
<InlineStack gap="300">
|
|
||||||
<Link url="/app/managebrand" removeUnderline>
|
|
||||||
<Button>Go to Brands Manage Page</Button>
|
|
||||||
</Link>
|
|
||||||
</InlineStack>
|
|
||||||
</BlockStack>
|
|
||||||
</BlockStack>
|
|
||||||
</Card>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
|
||||||
</BlockStack>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* //woking code
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { useFetcher } from "@remix-run/react";
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Layout,
|
|
||||||
Card,
|
|
||||||
Tabs,
|
|
||||||
Button,
|
|
||||||
BlockStack,
|
|
||||||
InlineStack,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Link,
|
|
||||||
LegacyCard,
|
|
||||||
} from "@shopify/polaris";
|
|
||||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
|
||||||
import { authenticate } from "../shopify.server";
|
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
|
||||||
await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const action = async ({ request }) => {
|
|
||||||
const { admin } = await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
const shopify = useAppBridge();
|
|
||||||
const isLoading =
|
|
||||||
["loading", "submitting"].includes(fetcher.state) &&
|
|
||||||
fetcher.formMethod === "POST";
|
|
||||||
const productId = fetcher.data?.product?.id?.replace(
|
|
||||||
"gid://shopify/Product/",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Temporarily disabling toast to avoid crash
|
|
||||||
// You can safely add App Bridge toast later
|
|
||||||
}, [productId, shopify]);
|
|
||||||
|
|
||||||
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
|
||||||
|
|
||||||
// Tabs logic
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
const handleTabChange = useCallback(
|
|
||||||
(selectedTabIndex) => setSelectedTab(selectedTabIndex),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
id: "settings-tab",
|
|
||||||
content: "⚙️ Settings",
|
|
||||||
panelID: "settings-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "brands-tab",
|
|
||||||
content: "🏷️ Brands",
|
|
||||||
panelID: "brands-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "manage-tab",
|
|
||||||
content: "📦 Manage Brands",
|
|
||||||
panelID: "manage-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "help-tab",
|
|
||||||
content: "🆘 Help",
|
|
||||||
panelID: "help-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "login-tab",
|
|
||||||
content: "🔐 Login",
|
|
||||||
panelID: "login-content",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TitleBar title="Data4Autos Turn 14 Integration" />
|
|
||||||
<Layout>
|
|
||||||
<Layout.Section>
|
|
||||||
<Card>
|
|
||||||
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
|
||||||
<LegacyCard.Section title={tabs[selectedTab].content}>
|
|
||||||
{selectedTab === 0 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Configure Turn14 integration settings.</Text>
|
|
||||||
<Link url="/app/settings">
|
|
||||||
<Button>Go to Settings Page</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
{selectedTab === 1 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>View available brands from Turn14.</Text>
|
|
||||||
<Link url="/app/brands">
|
|
||||||
<Button>Go to Brands Page</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
{selectedTab === 2 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Manage your synced brand collections.</Text>
|
|
||||||
<Link url="/app/managebrand">
|
|
||||||
<Button>Go to Manage Brands</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
{selectedTab === 3 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Help and documentation links.</Text>
|
|
||||||
<Button url="https://data4autos.com/help" external>
|
|
||||||
Open Help Docs
|
|
||||||
</Button>
|
|
||||||
<Button url="mailto:support@data4autos.com" tone="critical">
|
|
||||||
Contact Support
|
|
||||||
</Button>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
{selectedTab === 4 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Login to manage Turn14 credentials.</Text>
|
|
||||||
<Link url="/app/login">
|
|
||||||
<Button>Go to Login</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
</LegacyCard.Section>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
} */
|
|
||||||
|
|
||||||
|
|
||||||
/* import { Page, Layout, Card, Text, BlockStack } from "@shopify/polaris";
|
|
||||||
import { TitleBar } from "@shopify/app-bridge-react";
|
|
||||||
import { authenticate } from "../shopify.server";
|
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
|
||||||
await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
|
||||||
<Layout>
|
|
||||||
<Layout.Section>
|
|
||||||
<Card>
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text variant="headingLg" as="h2">
|
|
||||||
Welcome to your Turn14 integration dashboard!
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
Use the navigation in the left sidebar to manage settings, view brands,
|
|
||||||
sync collections, and more.
|
|
||||||
</Text>
|
|
||||||
</BlockStack>
|
|
||||||
</Card>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Page,
|
Page,
|
||||||
Layout,
|
Layout,
|
||||||
@ -298,11 +11,127 @@ import {
|
|||||||
InlineStack,
|
InlineStack,
|
||||||
Image,
|
Image,
|
||||||
Divider,
|
Divider,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
} from "@shopify/polaris";
|
} from "@shopify/polaris";
|
||||||
import { TitleBar } from "@shopify/app-bridge-react";
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists
|
import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists
|
||||||
|
import { authenticate } from "../shopify.server"; // Shopify server authentication
|
||||||
|
|
||||||
|
import { Form } from "@remix-run/react";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Loader to check subscription status
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
|
||||||
|
// Query the current subscription status
|
||||||
|
const resp = await admin.graphql(`
|
||||||
|
query {
|
||||||
|
currentAppInstallation {
|
||||||
|
activeSubscriptions {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
trialDays
|
||||||
|
createdAt
|
||||||
|
currentPeriodEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result = await resp.json();
|
||||||
|
const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null;
|
||||||
|
|
||||||
|
// For new users, there's no subscription. We will show a "Not subscribed" message.
|
||||||
|
if (!subscription) {
|
||||||
|
return json({ redirectToBilling: true, subscription: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no active or trial subscription, return redirect signal
|
||||||
|
if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") {
|
||||||
|
return json({ redirectToBilling: true, subscription });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ redirectToBilling: false, subscription });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action to create subscription
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
console.log("Creating subscription...");
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
|
||||||
|
const createRes = await admin.graphql(`
|
||||||
|
mutation {
|
||||||
|
appSubscriptionCreate(
|
||||||
|
name: "Pro Plan",
|
||||||
|
returnUrl: "https://your-app.com/after-billing",
|
||||||
|
lineItems: [
|
||||||
|
{
|
||||||
|
plan: {
|
||||||
|
appRecurringPricingDetails: {
|
||||||
|
price: { amount: 19.99, currencyCode: USD },
|
||||||
|
interval: EVERY_30_DAYS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
trialDays: 7, # ✅ trialDays is a top-level argument!
|
||||||
|
test: true
|
||||||
|
) {
|
||||||
|
confirmationUrl
|
||||||
|
appSubscription {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
trialDays
|
||||||
|
}
|
||||||
|
userErrors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const data = await createRes.json();
|
||||||
|
console.log("Subscription creation response:", data);
|
||||||
|
if (data.errors || !data.data.appSubscriptionCreate.confirmationUrl) {
|
||||||
|
return json({ errors: ["Failed to create subscription."] }, { status: 400 });
|
||||||
|
}
|
||||||
|
console.log("Subscription created successfully:", data.data.appSubscriptionCreate.confirmationUrl);
|
||||||
|
return json({
|
||||||
|
confirmationUrl: data.data.appSubscriptionCreate.confirmationUrl
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
const actionData = useActionData();
|
||||||
|
const loaderData = useLoaderData();
|
||||||
|
const submit = useSubmit(); // Use submit to trigger the action
|
||||||
|
const [activeModal, setActiveModal] = useState(false);
|
||||||
|
|
||||||
|
const subscription = loaderData?.subscription;
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("Action data:", actionData);
|
||||||
|
// // If we have a confirmation URL, redirect to it
|
||||||
|
// if (actionData?.confirmationUrl) {
|
||||||
|
// window.location.href = actionData.confirmationUrl; // Redirect to Shopify's billing confirmation page
|
||||||
|
// }
|
||||||
|
// }, [actionData]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionData?.confirmationUrl) {
|
||||||
|
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
|
||||||
|
setActiveModal(false); // close the modal
|
||||||
|
}
|
||||||
|
}, [actionData]);
|
||||||
|
const openModal = () => setActiveModal(true);
|
||||||
|
const closeModal = () => setActiveModal(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
@ -351,258 +180,63 @@ export default function Index() {
|
|||||||
Need help? Contact us at{" "}
|
Need help? Contact us at{" "}
|
||||||
<a href="mailto:support@data4autos.com">support@data4autos.com</a>
|
<a href="mailto:support@data4autos.com">support@data4autos.com</a>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<Button onClick={openModal}>
|
||||||
|
{loaderData?.redirectToBilling ? "Proceed to Billing" : "View Subscription Details"}
|
||||||
|
</Button>
|
||||||
</BlockStack>
|
</BlockStack>
|
||||||
</BlockStack>
|
</BlockStack>
|
||||||
</Card>
|
</Card>
|
||||||
</Layout.Section>
|
</Layout.Section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
{/* Modal for Subscription Info */}
|
||||||
|
<Modal
|
||||||
|
open={activeModal}
|
||||||
|
onClose={closeModal}
|
||||||
|
title="Subscription Details"
|
||||||
|
// primaryAction={{
|
||||||
|
// content: "Proceed to Billing",
|
||||||
|
// onAction: () => {
|
||||||
|
// submit(null, { method: "post", form: document.getElementById("billing-form") });
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
primaryAction={{
|
||||||
|
content: "Proceed to Billing",
|
||||||
|
onAction: () => {
|
||||||
|
submit(null, { method: "post", form: document.getElementById("billing-form") });
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
secondaryActions={[{ content: "Close", onAction: closeModal }]}
|
||||||
|
>
|
||||||
|
<Form id="billing-form" method="post">
|
||||||
|
<Modal.Section>
|
||||||
|
|
||||||
|
<BlockStack gap="100">
|
||||||
|
<TextField
|
||||||
|
label="Subscription Status"
|
||||||
|
value={subscription ? subscription.status : "No active subscription"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Trial Days Left"
|
||||||
|
value={subscription?.trialDays ? `${subscription.trialDays} days left` : "No trial available"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Subscription Plan"
|
||||||
|
value={subscription?.id ? "Pro Plan" : "Not subscribed"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Trial Expiration Date"
|
||||||
|
value={subscription?.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "N/A"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</BlockStack>
|
||||||
|
</Modal.Section>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { useFetcher, useNavigate } from "@remix-run/react";
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Layout,
|
|
||||||
Card,
|
|
||||||
Tabs,
|
|
||||||
LegacyCard,
|
|
||||||
} from "@shopify/polaris";
|
|
||||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
|
||||||
import { authenticate } from "../shopify.server";
|
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
|
||||||
await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const action = async ({ request }) => {
|
|
||||||
const { admin } = await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
const shopify = useAppBridge();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const productId = fetcher.data?.product?.id?.replace(
|
|
||||||
"gid://shopify/Product/",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// You can add toast messages here if needed
|
|
||||||
}, [productId, shopify]);
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
id: "settings-tab",
|
|
||||||
content: "⚙️ Settings",
|
|
||||||
panelID: "settings-content",
|
|
||||||
to: "/app/settings",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "brands-tab",
|
|
||||||
content: "🏷️ Brands",
|
|
||||||
panelID: "brands-content",
|
|
||||||
to: "/app/brands",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "manage-tab",
|
|
||||||
content: "📦 Manage Brands",
|
|
||||||
panelID: "manage-content",
|
|
||||||
to: "/app/managebrand",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "help-tab",
|
|
||||||
content: "🆘 Help",
|
|
||||||
panelID: "help-content",
|
|
||||||
to: "/app/help",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "login-tab",
|
|
||||||
content: "🔐 Login",
|
|
||||||
panelID: "login-content",
|
|
||||||
to: "/app/login",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
|
||||||
(selectedTabIndex) => {
|
|
||||||
setSelectedTab(selectedTabIndex);
|
|
||||||
navigate(tabs[selectedTabIndex].to);
|
|
||||||
},
|
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TitleBar title="Data4Autos Turn 14 Integration" />
|
|
||||||
<Layout>
|
|
||||||
<Layout.Section>
|
|
||||||
<Card>
|
|
||||||
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
|
||||||
<LegacyCard.Section title={tabs[selectedTab].content}>
|
|
||||||
<p>Redirecting to {tabs[selectedTab].content}...</p>
|
|
||||||
</LegacyCard.Section>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { useFetcher } from "@remix-run/react";
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Layout,
|
|
||||||
Card,
|
|
||||||
Tabs,
|
|
||||||
Button,
|
|
||||||
BlockStack,
|
|
||||||
InlineStack,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Link,
|
|
||||||
LegacyCard,
|
|
||||||
} from "@shopify/polaris";
|
|
||||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
|
||||||
import { authenticate } from "../shopify.server";
|
|
||||||
import SettingsPage from "./app.settings"; // Adjust the path if needed
|
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
|
||||||
await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const action = async ({ request }) => {
|
|
||||||
const { admin } = await authenticate.admin(request);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
const shopify = useAppBridge();
|
|
||||||
const isLoading =
|
|
||||||
["loading", "submitting"].includes(fetcher.state) &&
|
|
||||||
fetcher.formMethod === "POST";
|
|
||||||
const productId = fetcher.data?.product?.id?.replace(
|
|
||||||
"gid://shopify/Product/",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Toast placeholder (disabled)
|
|
||||||
}, [productId, shopify]);
|
|
||||||
|
|
||||||
const generateProduct = () => fetcher.submit({}, { method: "POST" });
|
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
const handleTabChange = useCallback(
|
|
||||||
(selectedTabIndex) => setSelectedTab(selectedTabIndex),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
id: "settings-tab",
|
|
||||||
content: "⚙️ Settings",
|
|
||||||
panelID: "settings-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "brands-tab",
|
|
||||||
content: "🏷️ Brands",
|
|
||||||
panelID: "brands-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "manage-tab",
|
|
||||||
content: "📦 Manage Brands",
|
|
||||||
panelID: "manage-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "help-tab",
|
|
||||||
content: "🆘 Help",
|
|
||||||
panelID: "help-content",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "login-tab",
|
|
||||||
content: "🔐 Login",
|
|
||||||
panelID: "login-content",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TitleBar title="Data4Autos Turn 14 Integration" />
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Layout.Section>
|
|
||||||
<Card>
|
|
||||||
<Tabs tabs={tabs} selected={selectedTab} onSelect={handleTabChange}>
|
|
||||||
<LegacyCard.Section title={tabs[selectedTab].content}>
|
|
||||||
{selectedTab === 0 && (
|
|
||||||
<SettingsPage standalone={false} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTab === 1 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>View available brands from Turn14.</Text>
|
|
||||||
<Link url="/app/brands">
|
|
||||||
<Button>Go to Brands Page</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTab === 2 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Manage your synced brand collections.</Text>
|
|
||||||
<Link url="/app/managebrand">
|
|
||||||
<Button>Go to Manage Brands</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTab === 3 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Help and documentation links.</Text>
|
|
||||||
<Button url="https://data4autos.com/help" external>
|
|
||||||
Open Help Docs
|
|
||||||
</Button>
|
|
||||||
<Button url="mailto:support@data4autos.com" tone="critical">
|
|
||||||
Contact Support
|
|
||||||
</Button>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTab === 4 && (
|
|
||||||
<BlockStack gap="200">
|
|
||||||
<Text>Login to manage Turn14 credentials.</Text>
|
|
||||||
<Link url="/app/login">
|
|
||||||
<Button>Go to Login</Button>
|
|
||||||
</Link>
|
|
||||||
</BlockStack>
|
|
||||||
)}
|
|
||||||
</LegacyCard.Section>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
295
app/routes/app.brands copy 2.jsx
Normal file
295
app/routes/app.brands copy 2.jsx
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
TextField,
|
||||||
|
Checkbox,
|
||||||
|
Button,
|
||||||
|
Thumbnail,
|
||||||
|
Spinner,
|
||||||
|
Toast,
|
||||||
|
Frame,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
|
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
|
||||||
|
// Get brands
|
||||||
|
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const brandJson = await brandRes.json();
|
||||||
|
if (!brandRes.ok) {
|
||||||
|
return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get collections
|
||||||
|
const gqlRaw = await admin.graphql(`
|
||||||
|
{
|
||||||
|
collections(first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const gql = await gqlRaw.json();
|
||||||
|
const collections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
|
||||||
|
|
||||||
|
return json({
|
||||||
|
brands: brandJson.data,
|
||||||
|
collections,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
|
||||||
|
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
|
||||||
|
// Get current collections
|
||||||
|
const gqlRaw = await admin.graphql(`
|
||||||
|
{
|
||||||
|
collections(first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const gql = await gqlRaw.json();
|
||||||
|
const existingCollections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
|
||||||
|
|
||||||
|
const selectedTitles = selectedBrands.map((b) => b.name.toLowerCase());
|
||||||
|
const logoMap = Object.fromEntries(selectedBrands.map(b => [b.name.toLowerCase(), b.logo]));
|
||||||
|
|
||||||
|
// Delete unselected
|
||||||
|
for (const col of existingCollections) {
|
||||||
|
if (!selectedTitles.includes(col.title.toLowerCase())) {
|
||||||
|
await admin.graphql(`
|
||||||
|
mutation {
|
||||||
|
collectionDelete(input: { id: "${col.id}" }) {
|
||||||
|
deletedCollectionId
|
||||||
|
userErrors { message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new
|
||||||
|
for (const brand of selectedBrands) {
|
||||||
|
const exists = existingCollections.find(
|
||||||
|
(c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
const escapedName = brand.name.replace(/"/g, '\\"');
|
||||||
|
const logo = brand.logo || "";
|
||||||
|
|
||||||
|
await admin.graphql(`
|
||||||
|
mutation {
|
||||||
|
collectionCreate(input: {
|
||||||
|
title: "${escapedName}",
|
||||||
|
descriptionHtml: "Products from brand ${escapedName}",
|
||||||
|
image: {
|
||||||
|
altText: "${escapedName} Logo",
|
||||||
|
src: "${logo}"
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
collection { id }
|
||||||
|
userErrors { message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shopDataRaw = await admin.graphql(`
|
||||||
|
{
|
||||||
|
shop {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const shopRes = await admin.graphql(`{ shop { id } }`);
|
||||||
|
const shopJson = await shopRes.json();
|
||||||
|
const shopId = shopJson?.data?.shop?.id;
|
||||||
|
|
||||||
|
await admin.graphql(`
|
||||||
|
mutation {
|
||||||
|
metafieldsSet(metafields: [{
|
||||||
|
namespace: "turn14",
|
||||||
|
key: "selected_brands",
|
||||||
|
type: "json",
|
||||||
|
ownerId: "${shopId}",
|
||||||
|
value: ${JSON.stringify(JSON.stringify(selectedBrands))}
|
||||||
|
}]) {
|
||||||
|
metafields {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
userErrors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BrandsPage() {
|
||||||
|
const { brands, collections } = useLoaderData();
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const isSubmitting = fetcher.state === "submitting";
|
||||||
|
const [toastActive, setToastActive] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const collectionTitles = new Set(collections.map((c) => c.title.toLowerCase()));
|
||||||
|
const defaultSelected = brands
|
||||||
|
.filter((b) => collectionTitles.has(b.name.toLowerCase()))
|
||||||
|
.map((b) => b.id);
|
||||||
|
|
||||||
|
const [selectedIds, setSelectedIds] = useState(defaultSelected);
|
||||||
|
const [filteredBrands, setFilteredBrands] = useState(brands);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const term = search.toLowerCase();
|
||||||
|
setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term)));
|
||||||
|
}, [search, brands]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetcher.data?.success) {
|
||||||
|
setToastActive(true);
|
||||||
|
}
|
||||||
|
}, [fetcher.data]);
|
||||||
|
|
||||||
|
const toggleSelect = (id) => {
|
||||||
|
setSelectedIds((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
const filteredBrandIds = filteredBrands.map(b => b.id);
|
||||||
|
const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id));
|
||||||
|
|
||||||
|
if (allFilteredSelected) {
|
||||||
|
// Deselect all filtered brands
|
||||||
|
setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id)));
|
||||||
|
} else {
|
||||||
|
// Select all filtered brands
|
||||||
|
setSelectedIds(prev => {
|
||||||
|
const combined = new Set([...prev, ...filteredBrandIds]);
|
||||||
|
return Array.from(combined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toastMarkup = toastActive ? (
|
||||||
|
<Toast
|
||||||
|
content="Collections updated successfully!"
|
||||||
|
onDismiss={() => setToastActive(false)}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
|
||||||
|
const allFilteredSelected = filteredBrands.length > 0 &&
|
||||||
|
filteredBrands.every(brand => selectedIds.includes(brand.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Frame>
|
||||||
|
<Page title="Data4Autos Turn14 Brands List">
|
||||||
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<TextField
|
||||||
|
label="Search brands"
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Type brand name..."
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<Checkbox
|
||||||
|
label="Select All"
|
||||||
|
checked={allFilteredSelected}
|
||||||
|
onChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout.Section>
|
||||||
|
|
||||||
|
<Layout.Section>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
|
||||||
|
gap: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredBrands.map((brand) => (
|
||||||
|
<Card key={brand.id} sectioned>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<Checkbox
|
||||||
|
label=""
|
||||||
|
checked={selectedIds.includes(brand.id)}
|
||||||
|
onChange={() => toggleSelect(brand.id)}
|
||||||
|
/>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
brand.logo ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={brand.name}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<strong>{brand.name}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Layout.Section>
|
||||||
|
|
||||||
|
<Layout.Section>
|
||||||
|
<fetcher.Form method="post">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="selectedBrands"
|
||||||
|
value={JSON.stringify(selectedBrands)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
submit
|
||||||
|
disabled={selectedIds.length === 0 || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
||||||
|
</Button>
|
||||||
|
</fetcher.Form>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
{toastMarkup}
|
||||||
|
</Page>
|
||||||
|
</Frame>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -96,32 +96,94 @@ export const action = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new
|
// Create new
|
||||||
|
// for (const brand of selectedBrands) {
|
||||||
|
// const exists = existingCollections.find(
|
||||||
|
// (c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||||
|
// );
|
||||||
|
// if (!exists) {
|
||||||
|
// const escapedName = brand.name.replace(/"/g, '\\"');
|
||||||
|
// const logo = brand.logo || "";
|
||||||
|
|
||||||
|
// await admin.graphql(`
|
||||||
|
// mutation {
|
||||||
|
// collectionCreate(input: {
|
||||||
|
// title: "${escapedName}",
|
||||||
|
// descriptionHtml: "Products from brand ${escapedName}",
|
||||||
|
// image: {
|
||||||
|
// altText: "${escapedName} Logo",
|
||||||
|
// src: "${logo}"
|
||||||
|
// }
|
||||||
|
// }) {
|
||||||
|
// collection { id }
|
||||||
|
// userErrors { message }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// for (const brand of selectedBrands) {
|
||||||
|
// const exists = existingCollections.find(
|
||||||
|
// (c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||||
|
// );
|
||||||
|
// if (!exists) {
|
||||||
|
// const escapedName = brand.name.replace(/"/g, '\\"');
|
||||||
|
// // Only build the image block if there's a logo URL:
|
||||||
|
// const imageBlock = brand.logo
|
||||||
|
// ? `
|
||||||
|
// image: {
|
||||||
|
// altText: "${escapedName} Logo",
|
||||||
|
// src: "${brand.logo}"
|
||||||
|
// }
|
||||||
|
// `
|
||||||
|
// : "";
|
||||||
|
|
||||||
|
// await admin.graphql(`
|
||||||
|
// mutation {
|
||||||
|
// collectionCreate(input: {
|
||||||
|
// title: "${escapedName}",
|
||||||
|
// descriptionHtml: "Products from brand ${escapedName}"
|
||||||
|
// ${imageBlock}
|
||||||
|
// }) {
|
||||||
|
// collection { id }
|
||||||
|
// userErrors { message }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const fallbackLogo =
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png";
|
||||||
|
|
||||||
for (const brand of selectedBrands) {
|
for (const brand of selectedBrands) {
|
||||||
const exists = existingCollections.find(
|
const exists = existingCollections.find(
|
||||||
(c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
(c) => c.title.toLowerCase() === brand.name.toLowerCase()
|
||||||
);
|
);
|
||||||
if (!exists) {
|
if (exists) continue;
|
||||||
const escapedName = brand.name.replace(/"/g, '\\"');
|
|
||||||
const logo = brand.logo || "";
|
|
||||||
|
|
||||||
await admin.graphql(`
|
const escapedName = brand.name.replace(/"/g, '\\"');
|
||||||
mutation {
|
const logoSrc = brand.logo || fallbackLogo;
|
||||||
collectionCreate(input: {
|
|
||||||
title: "${escapedName}",
|
await admin.graphql(`
|
||||||
descriptionHtml: "Products from brand ${escapedName}",
|
mutation {
|
||||||
image: {
|
collectionCreate(input: {
|
||||||
altText: "${escapedName} Logo",
|
title: "${escapedName}",
|
||||||
src: "${logo}"
|
descriptionHtml: "Products from brand ${escapedName}",
|
||||||
}
|
image: {
|
||||||
}) {
|
altText: "${escapedName} Logo",
|
||||||
collection { id }
|
src: "${logoSrc}"
|
||||||
userErrors { message }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`);
|
}) {
|
||||||
|
collection { id }
|
||||||
|
userErrors { message }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const shopDataRaw = await admin.graphql(`
|
const shopDataRaw = await admin.graphql(`
|
||||||
{
|
{
|
||||||
shop {
|
shop {
|
||||||
@ -190,7 +252,7 @@ export default function BrandsPage() {
|
|||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
const filteredBrandIds = filteredBrands.map(b => b.id);
|
const filteredBrandIds = filteredBrands.map(b => b.id);
|
||||||
const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id));
|
const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id));
|
||||||
|
|
||||||
if (allFilteredSelected) {
|
if (allFilteredSelected) {
|
||||||
// Deselect all filtered brands
|
// Deselect all filtered brands
|
||||||
setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id)));
|
setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id)));
|
||||||
@ -211,7 +273,7 @@ export default function BrandsPage() {
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
|
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
|
||||||
const allFilteredSelected = filteredBrands.length > 0 &&
|
const allFilteredSelected = filteredBrands.length > 0 &&
|
||||||
filteredBrands.every(brand => selectedIds.includes(brand.id));
|
filteredBrands.every(brand => selectedIds.includes(brand.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -219,6 +281,22 @@ export default function BrandsPage() {
|
|||||||
<Page title="Data4Autos Turn14 Brands List">
|
<Page title="Data4Autos Turn14 Brands List">
|
||||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<Layout.Section>
|
||||||
|
<fetcher.Form method="post">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="selectedBrands"
|
||||||
|
value={JSON.stringify(selectedBrands)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
submit
|
||||||
|
disabled={selectedIds.length === 0 || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
||||||
|
</Button>
|
||||||
|
</fetcher.Form>
|
||||||
|
</Layout.Section>
|
||||||
<Layout.Section>
|
<Layout.Section>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
<TextField
|
<TextField
|
||||||
@ -271,22 +349,7 @@ export default function BrandsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Layout.Section>
|
</Layout.Section>
|
||||||
|
|
||||||
<Layout.Section>
|
|
||||||
<fetcher.Form method="post">
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="selectedBrands"
|
|
||||||
value={JSON.stringify(selectedBrands)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
submit
|
|
||||||
disabled={selectedIds.length === 0 || isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? <Spinner size="small" /> : "Save Collections"}
|
|
||||||
</Button>
|
|
||||||
</fetcher.Form>
|
|
||||||
</Layout.Section>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
{toastMarkup}
|
{toastMarkup}
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
231
app/routes/app.managebrand copy.jsx
Normal file
231
app/routes/app.managebrand copy.jsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
IndexTable,
|
||||||
|
Card,
|
||||||
|
Thumbnail,
|
||||||
|
TextContainer,
|
||||||
|
Spinner,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Banner,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||||
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||||
|
|
||||||
|
const res = await admin.graphql(`{
|
||||||
|
shop {
|
||||||
|
metafield(namespace: "turn14", key: "selected_brands") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const rawValue = data?.data?.shop?.metafield?.value;
|
||||||
|
|
||||||
|
let brands = [];
|
||||||
|
try {
|
||||||
|
brands = JSON.parse(rawValue);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Failed to parse metafield value:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ brands, accessToken });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ManageBrandProducts() {
|
||||||
|
const actionData = useActionData();
|
||||||
|
const { brands, accessToken } = useLoaderData();
|
||||||
|
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||||
|
const [itemsMap, setItemsMap] = useState({});
|
||||||
|
const [loadingMap, setLoadingMap] = useState({});
|
||||||
|
const [productCount, setProductCount] = useState("0");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const [initialLoad, setInitialLoad] = useState(true);
|
||||||
|
|
||||||
|
// Function to toggle all brands
|
||||||
|
const toggleAllBrands = async () => {
|
||||||
|
for (const brand of brands) {
|
||||||
|
await toggleBrandItems(brand.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run on initial load
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialLoad && brands.length > 0) {
|
||||||
|
toggleAllBrands();
|
||||||
|
setInitialLoad(false);
|
||||||
|
}
|
||||||
|
}, [brands, initialLoad]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const toggleBrandItems = async (brandId) => {
|
||||||
|
const isExpanded = expandedBrand === brandId;
|
||||||
|
if (isExpanded) {
|
||||||
|
setExpandedBrand(null);
|
||||||
|
} else {
|
||||||
|
setExpandedBrand(brandId);
|
||||||
|
|
||||||
|
if (!itemsMap[brandId]) {
|
||||||
|
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||||
|
try {
|
||||||
|
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
|
||||||
|
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
setProductCount(data.length)
|
||||||
|
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching items:", err);
|
||||||
|
}
|
||||||
|
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||||
|
} else {
|
||||||
|
setProductCount(itemsMap[brandId].length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||||
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
{brands.length === 0 ? (
|
||||||
|
<Layout.Section>
|
||||||
|
<Card sectioned>
|
||||||
|
<p>No brands selected yet.</p>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
) : (
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<IndexTable
|
||||||
|
resourceName={{ singular: "brand", plural: "brands" }}
|
||||||
|
itemCount={brands.length}
|
||||||
|
headings={[
|
||||||
|
{ title: "Brand ID" },
|
||||||
|
{ title: "Logo" },
|
||||||
|
{ title: "Action" },
|
||||||
|
{ title: "Products Count" },
|
||||||
|
]}
|
||||||
|
selectable={false}
|
||||||
|
>
|
||||||
|
{brands.map((brand, index) => (
|
||||||
|
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
|
||||||
|
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
brand.logo ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={brand.name}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>
|
||||||
|
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||||
|
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||||
|
</Button>
|
||||||
|
</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
|
||||||
|
</IndexTable.Row>
|
||||||
|
))}
|
||||||
|
</IndexTable>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{brands.map(
|
||||||
|
(brand) =>
|
||||||
|
expandedBrand === brand.id && (
|
||||||
|
<Layout.Section fullWidth key={brand.id + "-expanded"}>
|
||||||
|
<Card sectioned>
|
||||||
|
{actionData?.success && (
|
||||||
|
<Banner title="✅ Product created!" status="success">
|
||||||
|
<p>
|
||||||
|
{actionData.results.map((r) => (
|
||||||
|
<span key={r.variant.id}>
|
||||||
|
Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Items from ${brand.name}`} sectioned>
|
||||||
|
{loadingMap[brand.id] ? (
|
||||||
|
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||||
|
) : (
|
||||||
|
|
||||||
|
|
||||||
|
<div style={{ paddingTop: "1rem" }}>
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="brandId" value={brand.id} />
|
||||||
|
<TextField
|
||||||
|
label="Number of products to add"
|
||||||
|
type="number"
|
||||||
|
name="productCount"
|
||||||
|
value={productCount}
|
||||||
|
onChange={setProductCount}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button submit primary style={{ marginTop: "1rem" }}>
|
||||||
|
Add First {productCount} Products to Store
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
Total Products Count : {(itemsMap[brand.id] || []).length}
|
||||||
|
{(
|
||||||
|
itemsMap[brand.id] && itemsMap[brand.id].length > 0
|
||||||
|
? itemsMap[brand.id]
|
||||||
|
: []
|
||||||
|
).map((item) => (
|
||||||
|
<Card key={item?.id} title={item?.attributes.product_name} sectioned>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section oneThird>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
item?.attributes.thumbnail ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={item?.attributes.product_name}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Layout.Section>
|
||||||
|
<Layout.Section>
|
||||||
|
<TextContainer spacing="tight">
|
||||||
|
<p><strong>Part Number:</strong> {item?.attributes.part_number}</p>
|
||||||
|
<p><strong>Category:</strong> {item?.attributes.category} > {item?.attributes.subcategory}</p>
|
||||||
|
</TextContainer>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
515
app/routes/app.managebrand_040725.jsx
Normal file
515
app/routes/app.managebrand_040725.jsx
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
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,
|
||||||
|
} from "@shopify/polaris";
|
||||||
|
import { authenticate } from "../shopify.server";
|
||||||
|
import { TitleBar } from "@shopify/app-bridge-react";
|
||||||
|
|
||||||
|
export const loader = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||||
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||||
|
|
||||||
|
const res = await admin.graphql(`{
|
||||||
|
shop {
|
||||||
|
metafield(namespace: "turn14", key: "selected_brands") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const rawValue = data?.data?.shop?.metafield?.value;
|
||||||
|
|
||||||
|
let brands = [];
|
||||||
|
try {
|
||||||
|
brands = JSON.parse(rawValue);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Failed to parse metafield value:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ brands, accessToken });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }) => {
|
||||||
|
const { admin } = await authenticate.admin(request);
|
||||||
|
const formData = await request.formData();
|
||||||
|
const brandId = formData.get("brandId");
|
||||||
|
const rawCount = formData.get("productCount");
|
||||||
|
const productCount = parseInt(rawCount, 10) || 10;
|
||||||
|
|
||||||
|
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||||
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||||
|
|
||||||
|
// Fetch items from Turn14 API
|
||||||
|
const itemsRes = await fetch(
|
||||||
|
`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const itemsData = await itemsRes.json();
|
||||||
|
|
||||||
|
function slugify(str) {
|
||||||
|
return str
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const attrs = item.attributes;
|
||||||
|
|
||||||
|
// Build and normalize collection titles
|
||||||
|
const category = attrs.category;
|
||||||
|
const subcategory = attrs.subcategory || "";
|
||||||
|
const brand = attrs.brand;
|
||||||
|
const subcats = subcategory
|
||||||
|
.split(/[,\/]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const collectionTitles = Array.from(
|
||||||
|
new Set([category, ...subcats, brand].filter(Boolean))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find or create collections, collect their IDs
|
||||||
|
const collectionIds = [];
|
||||||
|
for (const title of collectionTitles) {
|
||||||
|
const lookupRes = await admin.graphql(`
|
||||||
|
{
|
||||||
|
collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
|
||||||
|
nodes { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const lookupJson = await lookupRes.json();
|
||||||
|
const existing = lookupJson.data.collections.nodes;
|
||||||
|
if (existing.length) {
|
||||||
|
collectionIds.push(existing[0].id);
|
||||||
|
} else {
|
||||||
|
const createColRes = await admin.graphql(`
|
||||||
|
mutation($input: CollectionInput!) {
|
||||||
|
collectionCreate(input: $input) {
|
||||||
|
collection { id }
|
||||||
|
userErrors { field message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, { variables: { input: { title } } });
|
||||||
|
const createColJson = await createColRes.json();
|
||||||
|
const errs = createColJson.data.collectionCreate.userErrors;
|
||||||
|
if (errs.length) {
|
||||||
|
throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
|
||||||
|
}
|
||||||
|
collectionIds.push(createColJson.data.collectionCreate.collection.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tags
|
||||||
|
const tags = [
|
||||||
|
attrs.category,
|
||||||
|
...subcats,
|
||||||
|
attrs.brand,
|
||||||
|
attrs.part_number,
|
||||||
|
attrs.mfr_part_number,
|
||||||
|
attrs.price_group,
|
||||||
|
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
|
||||||
|
attrs.barcode
|
||||||
|
].filter(Boolean).map((t) => t.trim());
|
||||||
|
|
||||||
|
// Prepare media inputs
|
||||||
|
const mediaInputs = (attrs.files || [])
|
||||||
|
.filter((f) => f.type === "Image" && f.url)
|
||||||
|
.map((file) => ({
|
||||||
|
originalSource: file.url,
|
||||||
|
mediaContentType: "IMAGE",
|
||||||
|
alt: `${attrs.product_name} — ${file.media_content}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Pick the longest "Market Description" or fallback to part_description
|
||||||
|
const marketDescs = (attrs.descriptions || [])
|
||||||
|
.filter((d) => d.type === "Market Description")
|
||||||
|
.map((d) => d.description);
|
||||||
|
const descriptionHtml = marketDescs.length
|
||||||
|
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
|
||||||
|
: attrs.part_description;
|
||||||
|
|
||||||
|
// Create product + attach to collections + add media
|
||||||
|
const createProdRes = await admin.graphql(`
|
||||||
|
mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
|
||||||
|
productCreate(product: $prod, media: $media) {
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
variants(first: 1) {
|
||||||
|
nodes { id inventoryItem { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userErrors { field message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {
|
||||||
|
variables: {
|
||||||
|
prod: {
|
||||||
|
title: attrs.product_name,
|
||||||
|
descriptionHtml: descriptionHtml,
|
||||||
|
vendor: attrs.brand,
|
||||||
|
productType: attrs.category,
|
||||||
|
handle: slugify(attrs.part_number || attrs.product_name),
|
||||||
|
tags,
|
||||||
|
collectionsToJoin: collectionIds,
|
||||||
|
status: "ACTIVE",
|
||||||
|
},
|
||||||
|
media: mediaInputs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const createProdJson = await createProdRes.json();
|
||||||
|
const prodErrs = createProdJson.data.productCreate.userErrors;
|
||||||
|
if (prodErrs.length) {
|
||||||
|
const taken = prodErrs.some((e) => /already in use/i.test(e.message));
|
||||||
|
if (taken) {
|
||||||
|
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = createProdJson.data.productCreate.product;
|
||||||
|
const variantNode = product.variants.nodes[0];
|
||||||
|
const variantId = variantNode.id;
|
||||||
|
const inventoryItemId = variantNode.inventoryItem.id;
|
||||||
|
|
||||||
|
// Bulk-update variant (price, compare-at, barcode)
|
||||||
|
const price = parseFloat(attrs.price) || 1000;
|
||||||
|
const comparePrice = parseFloat(attrs.compare_price) || null;
|
||||||
|
const barcode = attrs.barcode || "";
|
||||||
|
|
||||||
|
const bulkRes = await admin.graphql(`
|
||||||
|
mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
|
||||||
|
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
|
||||||
|
productVariants { id price compareAtPrice barcode }
|
||||||
|
userErrors { field message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {
|
||||||
|
variables: {
|
||||||
|
productId: product.id,
|
||||||
|
variants: [{
|
||||||
|
id: variantId,
|
||||||
|
price,
|
||||||
|
...(comparePrice !== null && { compareAtPrice: comparePrice }),
|
||||||
|
...(barcode && { barcode }),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const bulkJson = await bulkRes.json();
|
||||||
|
const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
|
||||||
|
if (bulkErrs.length) {
|
||||||
|
throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
|
||||||
|
}
|
||||||
|
const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0];
|
||||||
|
|
||||||
|
// Update inventory item (SKU, cost & weight)
|
||||||
|
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
|
||||||
|
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
|
||||||
|
|
||||||
|
const invRes = await admin.graphql(`
|
||||||
|
mutation($id: ID!, $input: InventoryItemInput!) {
|
||||||
|
inventoryItemUpdate(id: $id, input: $input) {
|
||||||
|
inventoryItem {
|
||||||
|
id
|
||||||
|
sku
|
||||||
|
measurement {
|
||||||
|
weight { value unit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userErrors { field message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {
|
||||||
|
variables: {
|
||||||
|
id: inventoryItemId,
|
||||||
|
input: {
|
||||||
|
sku: attrs.part_number,
|
||||||
|
cost: costPerItem,
|
||||||
|
measurement: {
|
||||||
|
weight: { value: weightValue, unit: "POUNDS" }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const invJson = await invRes.json();
|
||||||
|
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
|
||||||
|
if (invErrs.length) {
|
||||||
|
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
|
||||||
|
}
|
||||||
|
const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
results.push({
|
||||||
|
productId: product.id,
|
||||||
|
variant: {
|
||||||
|
id: updatedVariant.id,
|
||||||
|
price: updatedVariant.price,
|
||||||
|
compareAtPrice: updatedVariant.compareAtPrice,
|
||||||
|
sku: inventoryItem.sku,
|
||||||
|
barcode: updatedVariant.barcode,
|
||||||
|
weight: inventoryItem.measurement.weight.value,
|
||||||
|
weightUnit: inventoryItem.measurement.weight.unit,
|
||||||
|
},
|
||||||
|
collections: collectionTitles,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ success: true, results });
|
||||||
|
};
|
||||||
|
|
||||||
|
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 toggleAllBrands = async () => {
|
||||||
|
for (const brand of brands) {
|
||||||
|
await toggleBrandItems(brand.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialLoad && brands.length > 0) {
|
||||||
|
toggleAllBrands();
|
||||||
|
setInitialLoad(false);
|
||||||
|
}
|
||||||
|
}, [brands, initialLoad]);
|
||||||
|
|
||||||
|
// const toggleBrandItems = async (brandId) => {
|
||||||
|
// const isExpanded = expandedBrand === brandId;
|
||||||
|
// if (isExpanded) {
|
||||||
|
// setExpandedBrand(null);
|
||||||
|
// } else {
|
||||||
|
// setExpandedBrand(brandId);
|
||||||
|
// if (!itemsMap[brandId]) {
|
||||||
|
// setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||||
|
// try {
|
||||||
|
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${accessToken}`,
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// const data = await res.json();
|
||||||
|
// setItemsMap((prev) => ({ ...prev, [brandId]: data }));
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error("Error fetching items:", err);
|
||||||
|
// }
|
||||||
|
// setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const toggleBrandItems = async (brandId) => {
|
||||||
|
const isExpanded = expandedBrand === brandId;
|
||||||
|
if (isExpanded) {
|
||||||
|
setExpandedBrand(null);
|
||||||
|
} else {
|
||||||
|
setExpandedBrand(brandId);
|
||||||
|
if (!itemsMap[brandId]) {
|
||||||
|
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
// Ensure we have an array of valid items
|
||||||
|
const validItems = Array.isArray(data)
|
||||||
|
? data.filter(item => item && item.id && item.attributes)
|
||||||
|
: [];
|
||||||
|
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching items:", err);
|
||||||
|
setItemsMap((prev) => ({ ...prev, [brandId]: [] })); // Set empty array on error
|
||||||
|
}
|
||||||
|
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||||
|
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||||
|
<Layout>
|
||||||
|
{brands.length === 0 ? (
|
||||||
|
<Layout.Section>
|
||||||
|
<Card sectioned>
|
||||||
|
<p>No brands selected yet.</p>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
) : (
|
||||||
|
<Layout.Section>
|
||||||
|
<Card>
|
||||||
|
<IndexTable
|
||||||
|
resourceName={{ singular: "brand", plural: "brands" }}
|
||||||
|
itemCount={brands.length}
|
||||||
|
headings={[
|
||||||
|
{ title: "Brand ID" },
|
||||||
|
{ title: "Logo" },
|
||||||
|
{ title: "Action" },
|
||||||
|
{ title: "Products Count" },
|
||||||
|
]}
|
||||||
|
selectable={false}
|
||||||
|
>
|
||||||
|
{brands.map((brand, index) => (
|
||||||
|
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
|
||||||
|
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
brand.logo ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={brand.name}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>
|
||||||
|
<Button onClick={() => toggleBrandItems(brand.id)}>
|
||||||
|
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||||
|
</Button>
|
||||||
|
</IndexTable.Cell>
|
||||||
|
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
|
||||||
|
</IndexTable.Row>
|
||||||
|
))}
|
||||||
|
</IndexTable>
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{brands.map(
|
||||||
|
(brand) =>
|
||||||
|
expandedBrand === brand.id && (
|
||||||
|
<Layout.Section fullWidth key={brand.id + "-expanded"}>
|
||||||
|
<Card sectioned>
|
||||||
|
{actionData?.success && (
|
||||||
|
<Banner title="✅ Product created!" status="success">
|
||||||
|
<p>
|
||||||
|
{actionData.results.map((r) => (
|
||||||
|
<span key={r.variant.id}>
|
||||||
|
Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Items from ${brand.name}`} sectioned>
|
||||||
|
{loadingMap[brand.id] ? (
|
||||||
|
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||||
|
) : (
|
||||||
|
<div style={{ paddingTop: "1rem" }}>
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="brandId" value={brand.id} />
|
||||||
|
<TextField
|
||||||
|
label="Number of products to add"
|
||||||
|
type="number"
|
||||||
|
name="productCount"
|
||||||
|
value={productCount}
|
||||||
|
onChange={setProductCount}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button submit primary style={{ marginTop: "1rem" }}>
|
||||||
|
Add First {productCount} Products to Store
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
{/* <p>Total Products Available: {(itemsMap[brand.id] || []).length}</p>
|
||||||
|
{(
|
||||||
|
itemsMap[brand.id] && itemsMap[brand.id].length > 0
|
||||||
|
? itemsMap[brand.id]
|
||||||
|
: []
|
||||||
|
).map((item) => (
|
||||||
|
<Card key={item?.id} title={item?.attributes.product_name} sectioned>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section oneThird>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
item?.attributes.thumbnail ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={item?.attributes.product_name}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Layout.Section>
|
||||||
|
<Layout.Section>
|
||||||
|
<TextContainer spacing="tight">
|
||||||
|
<p><strong>Part Number:</strong> {item?.attributes.part_number}</p>
|
||||||
|
<p><strong>Category:</strong> {item?.attributes.category} > {item?.attributes.subcategory}</p>
|
||||||
|
<p><strong>Price:</strong> ${item?.attributes.price}</p>
|
||||||
|
<p><strong>Description:</strong> {item?.attributes.part_description}</p>
|
||||||
|
</TextContainer>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Card>
|
||||||
|
))} */}
|
||||||
|
|
||||||
|
{(
|
||||||
|
itemsMap[brand.id] && itemsMap[brand.id].length > 0
|
||||||
|
? itemsMap[brand.id].filter(item => item && item.id) // Filter out null/undefined items
|
||||||
|
: []
|
||||||
|
).map((item) => (
|
||||||
|
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Section oneThird>
|
||||||
|
<Thumbnail
|
||||||
|
source={
|
||||||
|
item?.attributes?.thumbnail ||
|
||||||
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||||
|
}
|
||||||
|
alt={item?.attributes?.product_name || 'Product image'}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Layout.Section>
|
||||||
|
<Layout.Section>
|
||||||
|
<TextContainer spacing="tight">
|
||||||
|
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
|
||||||
|
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}</p>
|
||||||
|
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
|
||||||
|
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
|
||||||
|
</TextContainer>
|
||||||
|
</Layout.Section>
|
||||||
|
</Layout>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Layout.Section>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ api_version = "2025-04"
|
|||||||
uri = "/webhooks/app/uninstalled"
|
uri = "/webhooks/app/uninstalled"
|
||||||
|
|
||||||
[access_scopes]
|
[access_scopes]
|
||||||
scopes = "read_inventory,read_products,write_inventory,write_products"
|
scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
redirect_urls = ["https://shopify.data4autos.com/auth/callback", "https://shopify.data4autos.com/auth/shopify"] # Update this line as well
|
redirect_urls = ["https://backend.dine360.ca/auth/callback"] # Update this line as well
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user