updated the front end desing
This commit is contained in:
parent
6cb1d01b0c
commit
922890b3b3
BIN
app/assets/data4autos_logo.png
Normal file
BIN
app/assets/data4autos_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@ -1,16 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
/* import { useEffect, useState, useCallback } from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Text,
|
||||
Card,
|
||||
Tabs,
|
||||
Button,
|
||||
BlockStack,
|
||||
Box,
|
||||
List,
|
||||
Link,
|
||||
InlineStack,
|
||||
Text,
|
||||
Badge,
|
||||
Link,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
@ -23,67 +23,8 @@ export const loader = async ({ request }) => {
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const color = ["Red", "Orange", "Yellow", "Green"][
|
||||
Math.floor(Math.random() * 4)
|
||||
];
|
||||
const response = await admin.graphql(
|
||||
`#graphql
|
||||
mutation populateProduct($product: ProductCreateInput!) {
|
||||
productCreate(product: $product) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
handle
|
||||
status
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
price
|
||||
barcode
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: {
|
||||
product: {
|
||||
title: `${color} Snowboard`,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const responseJson = await response.json();
|
||||
const product = responseJson.data.productCreate.product;
|
||||
const variantId = product.variants.edges[0].node.id;
|
||||
const variantResponse = await admin.graphql(
|
||||
`#graphql
|
||||
mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
|
||||
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
|
||||
productVariants {
|
||||
id
|
||||
price
|
||||
barcode
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
variables: {
|
||||
productId: product.id,
|
||||
variants: [{ id: variantId, price: "100.00" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
const variantResponseJson = await variantResponse.json();
|
||||
|
||||
return {
|
||||
product: responseJson.data.productCreate.product,
|
||||
variant: variantResponseJson.data.productVariantsBulkUpdate.productVariants,
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
@ -97,6 +38,33 @@ export default function Index() {
|
||||
"",
|
||||
);
|
||||
|
||||
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");
|
||||
@ -106,12 +74,9 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="Remix app template">
|
||||
<button variant="primary" onClick={generateProduct}>
|
||||
Generate a product
|
||||
</button>
|
||||
</TitleBar>
|
||||
|
||||
<TitleBar
|
||||
title="Data4Autos Turn 14 Integration"
|
||||
/>
|
||||
|
||||
<BlockStack gap="500">
|
||||
<Layout>
|
||||
@ -119,216 +84,525 @@ export default function Index() {
|
||||
<Card>
|
||||
<BlockStack gap="500">
|
||||
<BlockStack gap="200">
|
||||
<Link url="/app/app/settings" removeUnderline>
|
||||
Go to Settings Page
|
||||
</Link>
|
||||
|
||||
<Text as="h2" variant="headingMd">
|
||||
Congrats on creating a new Shopify app 🎉
|
||||
</Text>
|
||||
<Text variant="bodyMd" as="p">
|
||||
This embedded app template uses{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/tools/app-bridge"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
App Bridge
|
||||
</Link>{" "}
|
||||
interface examples like an{" "}
|
||||
<Link url="/app/additional" removeUnderline>
|
||||
additional page in the app nav
|
||||
</Link>
|
||||
, as well as an{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/api/admin-graphql"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
Admin GraphQL
|
||||
</Link>{" "}
|
||||
mutation demo, to provide a starting point for app
|
||||
development.
|
||||
</Text>
|
||||
</BlockStack>
|
||||
<BlockStack gap="200">
|
||||
<Text as="h3" variant="headingMd">
|
||||
Get started with products
|
||||
</Text>
|
||||
<Text as="p" variant="bodyMd">
|
||||
Generate a product with GraphQL and get the JSON output for
|
||||
that product. Learn more about the{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/api/admin-graphql/latest/mutations/productCreate"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
productCreate
|
||||
</Link>{" "}
|
||||
mutation in our API references.
|
||||
</Text>
|
||||
</BlockStack>
|
||||
<InlineStack gap="300">
|
||||
<Button loading={isLoading} onClick={generateProduct}>
|
||||
Generate a product
|
||||
</Button>
|
||||
{fetcher.data?.product && (
|
||||
<Button
|
||||
url={`shopify:admin/products/${productId}`}
|
||||
target="_blank"
|
||||
variant="plain"
|
||||
>
|
||||
View product
|
||||
</Button>
|
||||
)}
|
||||
</InlineStack>
|
||||
{fetcher.data?.product && (
|
||||
<>
|
||||
<Text as="h3" variant="headingMd">
|
||||
{" "}
|
||||
productCreate mutation
|
||||
</Text>
|
||||
<Box
|
||||
padding="400"
|
||||
background="bg-surface-active"
|
||||
borderWidth="025"
|
||||
borderRadius="200"
|
||||
borderColor="border"
|
||||
overflowX="scroll"
|
||||
>
|
||||
<pre style={{ margin: 0 }}>
|
||||
<code>
|
||||
{JSON.stringify(fetcher.data.product, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</Box>
|
||||
<Text as="h3" variant="headingMd">
|
||||
{" "}
|
||||
productVariantsBulkUpdate mutation
|
||||
</Text>
|
||||
<Box
|
||||
padding="400"
|
||||
background="bg-surface-active"
|
||||
borderWidth="025"
|
||||
borderRadius="200"
|
||||
borderColor="border"
|
||||
overflowX="scroll"
|
||||
>
|
||||
<pre style={{ margin: 0 }}>
|
||||
<code>
|
||||
{JSON.stringify(fetcher.data.variant, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
<Layout.Section variant="oneThird">
|
||||
<BlockStack gap="500">
|
||||
<Card>
|
||||
<BlockStack gap="200">
|
||||
<Text as="h2" variant="headingMd">
|
||||
App template specs
|
||||
</Text>
|
||||
<BlockStack gap="200">
|
||||
<InlineStack align="space-between">
|
||||
<Text as="span" variant="bodyMd">
|
||||
Framework
|
||||
</Text>
|
||||
<Link
|
||||
url="https://remix.run"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
Remix
|
||||
<Link url="/app/settings" removeUnderline>
|
||||
<Button>Go to Settings Page</Button>
|
||||
</Link>
|
||||
</InlineStack>
|
||||
<InlineStack align="space-between">
|
||||
<Text as="span" variant="bodyMd">
|
||||
Database
|
||||
</Text>
|
||||
<Link
|
||||
url="https://www.prisma.io/"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
Prisma
|
||||
<InlineStack gap="300">
|
||||
<Link url="/app/brands" removeUnderline>
|
||||
<Button>Go to Brands Page</Button>
|
||||
</Link>
|
||||
</InlineStack>
|
||||
<InlineStack align="space-between">
|
||||
<Text as="span" variant="bodyMd">
|
||||
Interface
|
||||
</Text>
|
||||
<span>
|
||||
<Link
|
||||
url="https://polaris.shopify.com"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
Polaris
|
||||
</Link>
|
||||
{", "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/tools/app-bridge"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
App Bridge
|
||||
</Link>
|
||||
</span>
|
||||
</InlineStack>
|
||||
<InlineStack align="space-between">
|
||||
<Text as="span" variant="bodyMd">
|
||||
API
|
||||
</Text>
|
||||
<Link
|
||||
url="https://shopify.dev/docs/api/admin-graphql"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
GraphQL API
|
||||
<InlineStack gap="300">
|
||||
<Link url="/app/managebrand" removeUnderline>
|
||||
<Button>Go to Brands Manage Page</Button>
|
||||
</Link>
|
||||
</InlineStack>
|
||||
</BlockStack>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
<Card>
|
||||
<BlockStack gap="200">
|
||||
<Text as="h2" variant="headingMd">
|
||||
Next steps
|
||||
</Text>
|
||||
<List>
|
||||
<List.Item>
|
||||
Build an{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/getting-started/build-app-example"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
{" "}
|
||||
example app
|
||||
</Link>{" "}
|
||||
to get started
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Explore Shopify’s API with{" "}
|
||||
<Link
|
||||
url="https://shopify.dev/docs/apps/tools/graphiql-admin-api"
|
||||
target="_blank"
|
||||
removeUnderline
|
||||
>
|
||||
GraphiQL
|
||||
</Link>
|
||||
</List.Item>
|
||||
</List>
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</BlockStack>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
*/
|
||||
@ -13,6 +13,7 @@ import {
|
||||
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";
|
||||
|
||||
@ -201,9 +202,11 @@ export default function BrandsPage() {
|
||||
|
||||
return (
|
||||
<Frame>
|
||||
<Page title="Brands List">
|
||||
<Page title="Data4Autos Turn14 Brands List">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem", flexWrap: "wrap" }}></div>
|
||||
<TextField
|
||||
label="Search brands"
|
||||
value={search}
|
||||
|
||||
95
app/routes/app.help.jsx
Normal file
95
app/routes/app.help.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
Text,
|
||||
BlockStack,
|
||||
Link,
|
||||
Button,
|
||||
Collapsible,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function HelpPage() {
|
||||
const [openIndex, setOpenIndex] = useState(null);
|
||||
|
||||
const toggle = useCallback((index) => {
|
||||
setOpenIndex((prev) => (prev === index ? null : index));
|
||||
}, []);
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
title: "📌 How do I connect my Turn14 account?",
|
||||
content:
|
||||
"Go to the Settings page, enter your Turn14 Client ID and Secret, then click 'Save & Connect'. A green badge will confirm successful connection.",
|
||||
},
|
||||
{
|
||||
title: "📦 Where can I import brands from?",
|
||||
content:
|
||||
"Use the 'Brands' tab in the left menu to view and import available brands from Turn14 into your Shopify store.",
|
||||
},
|
||||
{
|
||||
title: "🔄 How do I sync brand collections?",
|
||||
content:
|
||||
"In the 'Manage Brands' section, select the brands and hit 'Sync to Shopify'. A manual collection will be created or updated.",
|
||||
},
|
||||
{
|
||||
title: "🔐 Is my Turn14 API key secure?",
|
||||
content:
|
||||
"Yes. The credentials are stored using Shopify’s encrypted storage (metafields), ensuring they are safe and secure.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<TitleBar title="Help & Documentation" />
|
||||
<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,4 +1,4 @@
|
||||
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
|
||||
/* import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
|
||||
import { boundary } from "@shopify/shopify-app-remix/server";
|
||||
import { AppProvider } from "@shopify/shopify-app-remix/react";
|
||||
import { NavMenu } from "@shopify/app-bridge-react";
|
||||
@ -34,6 +34,45 @@ export function ErrorBoundary() {
|
||||
return boundary.error(useRouteError());
|
||||
}
|
||||
|
||||
export const headers = (headersArgs) => {
|
||||
return boundary.headers(headersArgs);
|
||||
}; */
|
||||
|
||||
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
|
||||
import { boundary } from "@shopify/shopify-app-remix/server";
|
||||
import { AppProvider } from "@shopify/shopify-app-remix/react";
|
||||
import { NavMenu } from "@shopify/app-bridge-react";
|
||||
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
await authenticate.admin(request);
|
||||
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const { apiKey } = useLoaderData();
|
||||
|
||||
return (
|
||||
<AppProvider isEmbeddedApp apiKey={apiKey}>
|
||||
<NavMenu>
|
||||
<Link to="/app" rel="home">🏠 Home</Link>
|
||||
<Link to="/app/settings">⚙️ Settings</Link>
|
||||
<Link to="/app/brands">🏷️ Brands</Link>
|
||||
<Link to="/app/managebrand">📦 Manage Brands</Link>
|
||||
<Link to="/app/help">🆘 Help</Link>
|
||||
</NavMenu>
|
||||
<Outlet />
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return boundary.error(useRouteError());
|
||||
}
|
||||
|
||||
export const headers = (headersArgs) => {
|
||||
return boundary.headers(headersArgs);
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* import React, { useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
@ -8,15 +9,18 @@ import {
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
Text,
|
||||
TextField,
|
||||
Banner,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { useState } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
// Load selected brands and access token from Shopify metafield
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
||||
const { getTurn14AccessTokenFromMetafield } = await import(
|
||||
"../utils/turn14Token.server"
|
||||
);
|
||||
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||||
|
||||
const res = await admin.graphql(`
|
||||
@ -28,7 +32,6 @@ export const loader = async ({ request }) => {
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const data = await res.json();
|
||||
const rawValue = data?.data?.shop?.metafield?.value;
|
||||
|
||||
@ -42,13 +45,262 @@ export const loader = async ({ request }) => {
|
||||
return json({ brands, accessToken });
|
||||
};
|
||||
|
||||
// Handle adding products for a specific brand
|
||||
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 items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
|
||||
const results = [];
|
||||
for (const item of items1) {
|
||||
const attrs = item.attributes;
|
||||
|
||||
// 0️⃣ 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))
|
||||
);
|
||||
|
||||
// 1️⃣ Find or create collections, collect their IDs
|
||||
const collectionIds = [];
|
||||
for (const title of collectionTitles) {
|
||||
// lookup
|
||||
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 {
|
||||
// create
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ 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());
|
||||
|
||||
// 3️⃣ 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}`,
|
||||
}));
|
||||
|
||||
|
||||
// 2️⃣ 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;
|
||||
|
||||
// 4️⃣ 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;
|
||||
|
||||
// 5️⃣ 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];
|
||||
|
||||
// 6️⃣ 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;
|
||||
|
||||
// 7️⃣ 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 });
|
||||
};
|
||||
|
||||
// Main React component for managing brand products
|
||||
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 [adding, setAdding] = useState(false);
|
||||
|
||||
const toggleBrandItems = async (brandId) => {
|
||||
const isExpanded = expandedBrand === brandId;
|
||||
@ -75,134 +327,9 @@ export default function ManageBrandProducts() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProducts = async (brandId) => {
|
||||
const count = parseInt(productCount || "10");
|
||||
const items = (itemsMap[brandId] || []).slice(0, count);
|
||||
if (!items.length) return alert("No products to add.");
|
||||
setAdding(true);
|
||||
|
||||
for (const item of items) {
|
||||
const attr = item.attributes;
|
||||
|
||||
// Step 1: Create Product (only allowed fields)
|
||||
const productInput = {
|
||||
title: attr.product_name,
|
||||
descriptionHtml: `<p>${attr.part_description}</p>`,
|
||||
vendor: attr.brand,
|
||||
productType: attr.category,
|
||||
tags: [attr.subcategory, attr.brand].filter(Boolean).join(", "),
|
||||
};
|
||||
|
||||
const createProductRes = await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productCreate($input: ProductInput!) {
|
||||
productCreate(input: $input) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: { input: productInput },
|
||||
}),
|
||||
});
|
||||
|
||||
const createProductResult = await createProductRes.json();
|
||||
const product = createProductResult?.data?.productCreate?.product;
|
||||
const productErrors = createProductResult?.data?.productCreate?.userErrors;
|
||||
|
||||
if (productErrors?.length || !product?.id) {
|
||||
console.error("❌ Product create error:", productErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
const productId = product.id;
|
||||
|
||||
// Step 2: Create Variant
|
||||
const variantInput = {
|
||||
productId,
|
||||
sku: attr.part_number,
|
||||
barcode: attr.barcode || undefined,
|
||||
price: "0.00",
|
||||
weight: attr.dimensions?.[0]?.weight || 0,
|
||||
weightUnit: "KILOGRAMS",
|
||||
inventoryManagement: "SHOPIFY",
|
||||
};
|
||||
|
||||
await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productVariantCreate($input: ProductVariantInput!) {
|
||||
productVariantCreate(input: $input) {
|
||||
productVariant {
|
||||
id
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: { input: variantInput },
|
||||
}),
|
||||
});
|
||||
|
||||
// Step 3: Add Image
|
||||
if (attr.thumbnail) {
|
||||
await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productImageCreate($productId: ID!, $image: ImageInput!) {
|
||||
productImageCreate(productId: $productId, image: $image) {
|
||||
image {
|
||||
id
|
||||
src
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
productId,
|
||||
image: {
|
||||
src: attr.thumbnail,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Added:", attr.product_name);
|
||||
}
|
||||
|
||||
setAdding(false);
|
||||
alert(`${items.length} products added.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Manage Brand Products">
|
||||
<Page title="Data4Autos Turn14 Manage Brand Products">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
{brands.length === 0 && (
|
||||
<Layout.Section>
|
||||
@ -213,46 +340,54 @@ export default function ManageBrandProducts() {
|
||||
)}
|
||||
|
||||
{brands.map((brand) => (
|
||||
<div key={brand.id}>
|
||||
<React.Fragment key={brand.id}>
|
||||
<Layout.Section oneThird>
|
||||
<Card title={brand.name} sectioned>
|
||||
<Thumbnail
|
||||
source={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="large"
|
||||
/>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Brand:</strong> {brand.name}</p>
|
||||
<p><strong>ID:</strong> {brand.id}</p>
|
||||
</TextContainer>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)} fullWidth>
|
||||
<Button fullWidth onClick={() => toggleBrandItems(brand.id)}>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
|
||||
{expandedBrand === brand.id && (
|
||||
<Layout.Section fullWidth>
|
||||
<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>
|
||||
)}
|
||||
<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"
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button
|
||||
onClick={() => handleAddProducts(brand.id)}
|
||||
loading={adding}
|
||||
disabled={adding}
|
||||
primary
|
||||
>
|
||||
<Button submit primary style={{ marginTop: '1rem' }}>
|
||||
Add First {productCount} Products to Store
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
@ -260,7 +395,7 @@ export default function ManageBrandProducts() {
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
{(itemsMap[brand.id] || []).map(item => (
|
||||
{(itemsMap[brand.id] || []).map((item) => (
|
||||
<Card key={item.id} title={item.attributes.product_name} sectioned>
|
||||
<Layout>
|
||||
<Layout.Section oneThird>
|
||||
@ -276,9 +411,7 @@ export default function ManageBrandProducts() {
|
||||
<Layout.Section>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Part Number:</strong> {item.attributes.part_number}</p>
|
||||
<p><strong>Brand:</strong> {item.attributes.brand}</p>
|
||||
<p><strong>Category:</strong> {item.attributes.category} > {item.attributes.subcategory}</p>
|
||||
<p><strong>Dimensions:</strong> {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in</p>
|
||||
<p><strong>Category:</strong> {item.attributes.category} > {item.attributes.subcategory}</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
@ -289,9 +422,203 @@ export default function ManageBrandProducts() {
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
*/
|
||||
import React, { 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("10");
|
||||
|
||||
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`, {
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
]}
|
||||
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.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>
|
||||
)}
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
{(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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,429 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
Thumbnail,
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
TextField,
|
||||
Banner,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
// Load selected brands and access token from Shopify metafield
|
||||
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 });
|
||||
};
|
||||
|
||||
// Handle adding products for a specific brand
|
||||
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 items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
|
||||
const results = [];
|
||||
for (const item of items1) {
|
||||
const attrs = item.attributes;
|
||||
|
||||
// 0️⃣ 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))
|
||||
);
|
||||
|
||||
// 1️⃣ Find or create collections, collect their IDs
|
||||
const collectionIds = [];
|
||||
for (const title of collectionTitles) {
|
||||
// lookup
|
||||
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 {
|
||||
// create
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ 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());
|
||||
|
||||
// 3️⃣ 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}`,
|
||||
}));
|
||||
|
||||
|
||||
// 2️⃣ 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;
|
||||
|
||||
// 4️⃣ 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;
|
||||
|
||||
// 5️⃣ 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];
|
||||
|
||||
// 6️⃣ 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;
|
||||
|
||||
// 7️⃣ 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 });
|
||||
};
|
||||
|
||||
// Main React component for managing brand products
|
||||
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 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`, {
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Manage Brand Products">
|
||||
<Layout>
|
||||
{brands.length === 0 && (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<p>No brands selected yet.</p>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
|
||||
{brands.map((brand) => (
|
||||
<React.Fragment key={brand.id}>
|
||||
<Layout.Section oneThird>
|
||||
<Card title={brand.name} sectioned>
|
||||
<Thumbnail
|
||||
source={
|
||||
brand.logo ||
|
||||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
||||
}
|
||||
alt={brand.name}
|
||||
size="large"
|
||||
/>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>ID:</strong> {brand.id}</p>
|
||||
</TextContainer>
|
||||
<Button fullWidth onClick={() => toggleBrandItems(brand.id)}>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
|
||||
{expandedBrand === brand.id && (
|
||||
<Layout.Section fullWidth>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
{(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>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
297
app/routes/app.managebrand_bak.jsx
Normal file
297
app/routes/app.managebrand_bak.jsx
Normal file
@ -0,0 +1,297 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
Thumbnail,
|
||||
TextContainer,
|
||||
Spinner,
|
||||
Button,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@shopify/polaris";
|
||||
import { useState } from "react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
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 { brands, accessToken } = useLoaderData();
|
||||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||||
const [itemsMap, setItemsMap] = useState({});
|
||||
const [loadingMap, setLoadingMap] = useState({});
|
||||
const [productCount, setProductCount] = useState("10");
|
||||
const [adding, setAdding] = useState(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/brand/${brandId}?page=1`, {
|
||||
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 handleAddProducts = async (brandId) => {
|
||||
const count = parseInt(productCount || "10");
|
||||
const items = (itemsMap[brandId] || []).slice(0, count);
|
||||
if (!items.length) return alert("No products to add.");
|
||||
setAdding(true);
|
||||
|
||||
for (const item of items) {
|
||||
const attr = item.attributes;
|
||||
|
||||
// Step 1: Create Product (only allowed fields)
|
||||
const productInput = {
|
||||
title: attr.product_name,
|
||||
descriptionHtml: `<p>${attr.part_description}</p>`,
|
||||
vendor: attr.brand,
|
||||
productType: attr.category,
|
||||
tags: [attr.subcategory, attr.brand].filter(Boolean).join(", "),
|
||||
};
|
||||
|
||||
const createProductRes = await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productCreate($input: ProductInput!) {
|
||||
productCreate(input: $input) {
|
||||
product {
|
||||
id
|
||||
title
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: { input: productInput },
|
||||
}),
|
||||
});
|
||||
|
||||
const createProductResult = await createProductRes.json();
|
||||
const product = createProductResult?.data?.productCreate?.product;
|
||||
const productErrors = createProductResult?.data?.productCreate?.userErrors;
|
||||
|
||||
if (productErrors?.length || !product?.id) {
|
||||
console.error("❌ Product create error:", productErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
const productId = product.id;
|
||||
|
||||
// Step 2: Create Variant
|
||||
const variantInput = {
|
||||
productId,
|
||||
sku: attr.part_number,
|
||||
barcode: attr.barcode || undefined,
|
||||
price: "0.00",
|
||||
weight: attr.dimensions?.[0]?.weight || 0,
|
||||
weightUnit: "KILOGRAMS",
|
||||
inventoryManagement: "SHOPIFY",
|
||||
};
|
||||
|
||||
await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productVariantCreate($input: ProductVariantInput!) {
|
||||
productVariantCreate(input: $input) {
|
||||
productVariant {
|
||||
id
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: { input: variantInput },
|
||||
}),
|
||||
});
|
||||
|
||||
// Step 3: Add Image
|
||||
if (attr.thumbnail) {
|
||||
await fetch("/admin/api/2023-04/graphql.json", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Shopify-Access-Token": accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation productImageCreate($productId: ID!, $image: ImageInput!) {
|
||||
productImageCreate(productId: $productId, image: $image) {
|
||||
image {
|
||||
id
|
||||
src
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
productId,
|
||||
image: {
|
||||
src: attr.thumbnail,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Added:", attr.product_name);
|
||||
}
|
||||
|
||||
setAdding(false);
|
||||
alert(`${items.length} products added.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Manage Brand Products">
|
||||
<Layout>
|
||||
{brands.length === 0 && (
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<p>No brands selected yet.</p>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
|
||||
{brands.map((brand) => (
|
||||
<div key={brand.id}>
|
||||
<Layout.Section oneThird>
|
||||
<Card title={brand.name} sectioned>
|
||||
<Thumbnail
|
||||
source={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
|
||||
alt={brand.name}
|
||||
size="large"
|
||||
/>
|
||||
<TextContainer spacing="tight">
|
||||
<p><strong>Brand:</strong> {brand.name}</p>
|
||||
<p><strong>ID:</strong> {brand.id}</p>
|
||||
</TextContainer>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button onClick={() => toggleBrandItems(brand.id)} fullWidth>
|
||||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
|
||||
{expandedBrand === brand.id && (
|
||||
<Layout.Section fullWidth>
|
||||
<Card sectioned>
|
||||
<TextField
|
||||
label="Number of products to add"
|
||||
type="number"
|
||||
value={productCount}
|
||||
onChange={setProductCount}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<Button
|
||||
onClick={() => handleAddProducts(brand.id)}
|
||||
loading={adding}
|
||||
disabled={adding}
|
||||
primary
|
||||
>
|
||||
Add First {productCount} Products to Store
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title={`Items from ${brand.name}`} sectioned>
|
||||
{loadingMap[brand.id] ? (
|
||||
<Spinner accessibilityLabel="Loading items" size="small" />
|
||||
) : (
|
||||
<div style={{ paddingTop: "1rem" }}>
|
||||
{(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>Brand:</strong> {item.attributes.brand}</p>
|
||||
<p><strong>Category:</strong> {item.attributes.category} > {item.attributes.subcategory}</p>
|
||||
<p><strong>Dimensions:</strong> {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in</p>
|
||||
</TextContainer>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Layout>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@ -10,6 +10,7 @@ import {
|
||||
TextContainer,
|
||||
InlineError,
|
||||
} from "@shopify/polaris";
|
||||
import { TitleBar } from "@shopify/app-bridge-react";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
@ -136,7 +137,7 @@ export const action = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage() {
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
@ -148,7 +149,8 @@ export default function SettingsPage() {
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
|
||||
return (
|
||||
<Page title="Turn14 API Settings">
|
||||
<Page title="Data4Autos Turn14 API Settings">
|
||||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
@ -206,3 +208,220 @@ export default function SettingsPage() {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
import { json } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, Form } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Card,
|
||||
TextField,
|
||||
Button,
|
||||
InlineError,
|
||||
BlockStack,
|
||||
Text,
|
||||
} from "@shopify/polaris";
|
||||
import { authenticate } from "../shopify.server";
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const gqlResponse = await admin.graphql(`
|
||||
{
|
||||
shop {
|
||||
id
|
||||
name
|
||||
metafield(namespace: "turn14", key: "credentials") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const shopData = await gqlResponse.json();
|
||||
const shopName = shopData?.data?.shop?.name || "Unknown Shop";
|
||||
const metafieldRaw = shopData?.data?.shop?.metafield?.value;
|
||||
|
||||
let creds = {};
|
||||
if (metafieldRaw) {
|
||||
try {
|
||||
creds = JSON.parse(metafieldRaw);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored credentials:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ shopName, creds });
|
||||
};
|
||||
|
||||
export const action = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const clientId = formData.get("client_id") || "";
|
||||
const clientSecret = formData.get("client_secret") || "";
|
||||
|
||||
const { admin } = await authenticate.admin(request);
|
||||
|
||||
const shopInfo = await admin.graphql(`{ shop { id } }`);
|
||||
const shopId = (await shopInfo.json())?.data?.shop?.id;
|
||||
|
||||
try {
|
||||
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return json({
|
||||
success: false,
|
||||
error: tokenData.error || "Failed to fetch access token",
|
||||
});
|
||||
}
|
||||
|
||||
const accessToken = tokenData.access_token;
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||
|
||||
const credentials = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
const mutation = `
|
||||
mutation {
|
||||
metafieldsSet(metafields: [
|
||||
{
|
||||
ownerId: "${shopId}"
|
||||
namespace: "turn14"
|
||||
key: "credentials"
|
||||
type: "json"
|
||||
value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
|
||||
}
|
||||
]) {
|
||||
metafields {
|
||||
key
|
||||
value
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const saveRes = await admin.graphql(mutation);
|
||||
const result = await saveRes.json();
|
||||
|
||||
if (result?.data?.metafieldsSet?.userErrors?.length) {
|
||||
return json({
|
||||
success: false,
|
||||
error: result.data.metafieldsSet.userErrors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Turn14 token fetch failed:", err);
|
||||
return json({
|
||||
success: false,
|
||||
error: "Network or unexpected error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingsPage({ standalone = true }) {
|
||||
const loaderData = useLoaderData();
|
||||
const actionData = useActionData();
|
||||
|
||||
const savedCreds = loaderData?.creds || {};
|
||||
const shopName = loaderData?.shopName || "Shop";
|
||||
|
||||
const [clientId, setClientId] = useState(
|
||||
actionData?.clientId || savedCreds.clientId || ""
|
||||
);
|
||||
const [clientSecret, setClientSecret] = useState(
|
||||
actionData?.clientSecret || savedCreds.clientSecret || ""
|
||||
);
|
||||
const displayToken = actionData?.accessToken || savedCreds.accessToken;
|
||||
|
||||
const content = (
|
||||
<Layout>
|
||||
<Layout.Section>
|
||||
<Card sectioned>
|
||||
<BlockStack gap="300">
|
||||
<Text variant="bodyMd"><strong>Connected Shop:</strong> {shopName}</Text>
|
||||
|
||||
<Form method="post">
|
||||
<BlockStack gap="200">
|
||||
<TextField
|
||||
label="Turn14 Client ID"
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onChange={setClientId}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<TextField
|
||||
label="Turn14 Client Secret"
|
||||
name="client_secret"
|
||||
value={clientSecret}
|
||||
onChange={setClientSecret}
|
||||
autoComplete="off"
|
||||
requiredIndicator
|
||||
/>
|
||||
<Button submit primary>
|
||||
Generate Access Token
|
||||
</Button>
|
||||
</BlockStack>
|
||||
</Form>
|
||||
|
||||
{actionData?.error && (
|
||||
<InlineError
|
||||
message={`❌ ${actionData.error}`}
|
||||
fieldID="client_id"
|
||||
/>
|
||||
)}
|
||||
|
||||
{displayToken && (
|
||||
<BlockStack gap="100">
|
||||
<Text variant="bodySm" fontWeight="semibold" tone="success">
|
||||
✅ Access token:
|
||||
</Text>
|
||||
<code
|
||||
style={{
|
||||
background: "#f4f4f4",
|
||||
padding: "10px",
|
||||
display: "block",
|
||||
wordWrap: "break-word",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{displayToken}
|
||||
</code>
|
||||
</BlockStack>
|
||||
)}
|
||||
</BlockStack>
|
||||
</Card>
|
||||
</Layout.Section>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
return standalone ? <Page title="Turn14 API Settings">{content}</Page> : content;
|
||||
}
|
||||
*/
|
||||
@ -7,6 +7,9 @@ import {
|
||||
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
|
||||
import prisma from "./db.server";
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const shopify = shopifyApp({
|
||||
apiKey: process.env.SHOPIFY_API_KEY,
|
||||
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"@shopify/polaris": "^12.27.0",
|
||||
"@shopify/shopify-app-remix": "^3.7.0",
|
||||
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
|
||||
"dotenv": "^17.0.0",
|
||||
"isbot": "^5.1.0",
|
||||
"prisma": "^6.2.1",
|
||||
"react": "^18.2.0",
|
||||
@ -2034,6 +2035,19 @@
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-tools/prisma-loader/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-tools/relay-operation-optimizer": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.19.tgz",
|
||||
@ -2958,6 +2972,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/dev/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/dev/node_modules/prettier": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
@ -6462,9 +6488,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz",
|
||||
"integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"@shopify/polaris": "^12.27.0",
|
||||
"@shopify/shopify-app-remix": "^3.7.0",
|
||||
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
|
||||
"dotenv": "^17.0.0",
|
||||
"isbot": "^5.1.0",
|
||||
"prisma": "^6.2.1",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
|
||||
// enough when changing adapters.
|
||||
// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:dev.sqlite"
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
|
||||
|
||||
client_id = "b7534c980967bad619cfdb9d3f837cfa"
|
||||
name = "turn14-test"
|
||||
handle = "turn14-test-1"
|
||||
application_url = "https://manhattan-fifty-pays-detector.trycloudflare.com"
|
||||
application_url = "https://shopify.data4autos.com" # Update this line
|
||||
embedded = true
|
||||
|
||||
[build]
|
||||
@ -22,11 +20,7 @@ api_version = "2025-04"
|
||||
uri = "/webhooks/app/uninstalled"
|
||||
|
||||
[access_scopes]
|
||||
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
|
||||
scopes = "read_inventory,read_products,write_inventory,write_products"
|
||||
|
||||
[auth]
|
||||
redirect_urls = ["https://manhattan-fifty-pays-detector.trycloudflare.com/auth/callback", "https://manhattan-fifty-pays-detector.trycloudflare.com/auth/shopify/callback", "https://manhattan-fifty-pays-detector.trycloudflare.com/api/auth/callback"]
|
||||
|
||||
[pos]
|
||||
embedded = false
|
||||
redirect_urls = ["https://shopify.data4autos.com/auth/callback", "https://shopify.data4autos.com/auth/shopify"] # Update this line as well
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user