From 922890b3b32d727d133b4579bb98d2dfb7182b89 Mon Sep 17 00:00:00 2001 From: Metatron Host Date: Sun, 29 Jun 2025 18:30:52 +0000 Subject: [PATCH] updated the front end desing --- app/assets/data4autos_logo.png | Bin 0 -> 29993 bytes app/routes/app._index.jsx | 828 +++++++++++++++++++---------- app/routes/app.brands.jsx | 5 +- app/routes/app.help.jsx | 95 ++++ app/routes/app.jsx | 41 +- app/routes/app.managebrand.jsx | 651 +++++++++++++++++------ app/routes/app.managebrand1.jsx | 429 --------------- app/routes/app.managebrand_bak.jsx | 297 +++++++++++ app/routes/app.settings.jsx | 225 +++++++- app/shopify.server.js | 3 + package-lock.json | 33 +- package.json | 1 + prisma/schema.prisma | 24 +- shopify.app.toml | 10 +- 14 files changed, 1743 insertions(+), 899 deletions(-) create mode 100644 app/assets/data4autos_logo.png create mode 100644 app/routes/app.help.jsx delete mode 100644 app/routes/app.managebrand1.jsx create mode 100644 app/routes/app.managebrand_bak.jsx diff --git a/app/assets/data4autos_logo.png b/app/assets/data4autos_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d9229e2a6976987a0764c59e20b695edef053e99 GIT binary patch literal 29993 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 bqZ_|T3bjz?TWD|KK|gR+ZIxF_<`4f1Ybp2c literal 0 HcmV?d00001 diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index 7b6d932..e4e1ba1 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -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 ( + + + +

Tab {selected} selected

+
+
+
+ ); +} + useEffect(() => { if (productId) { shopify.toast.show("Product created"); @@ -106,229 +74,535 @@ export default function Index() { return ( - - - + - - + - - Go to Settings Page - - - - Congrats on creating a new Shopify app 🎉 - - - This embedded app template uses{" "} - - App Bridge - {" "} - interface examples like an{" "} - - additional page in the app nav + + + - , as well as an{" "} - - Admin GraphQL - {" "} - mutation demo, to provide a starting point for app - development. - + + + + + + + + + + + - - - Get started with products - - - Generate a product with GraphQL and get the JSON output for - that product. Learn more about the{" "} - - productCreate - {" "} - mutation in our API references. - - - - - {fetcher.data?.product && ( - - )} - - {fetcher.data?.product && ( - <> - - {" "} - productCreate mutation - - -
-                        
-                          {JSON.stringify(fetcher.data.product, null, 2)}
-                        
-                      
-
- - {" "} - productVariantsBulkUpdate mutation - - -
-                        
-                          {JSON.stringify(fetcher.data.variant, null, 2)}
-                        
-                      
-
- - )}
- - - - - - App template specs - - - - - Framework - - - Remix - - - - - Database - - - Prisma - - - - - Interface - - - - Polaris - - {", "} - - App Bridge - - - - - - API - - - GraphQL API - - - - - - - - - Next steps - - - - Build an{" "} - - {" "} - example app - {" "} - to get started - - - Explore Shopify’s API with{" "} - - GraphiQL - - - - - - - -
-
+ +
); } + + */ + +/* //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 ( + + + + + + + + {selectedTab === 0 && ( + + Configure Turn14 integration settings. + + + + + )} + {selectedTab === 1 && ( + + View available brands from Turn14. + + + + + )} + {selectedTab === 2 && ( + + Manage your synced brand collections. + + + + + )} + {selectedTab === 3 && ( + + Help and documentation links. + + + + )} + {selectedTab === 4 && ( + + Login to manage Turn14 credentials. + + + + + )} + + + + + + + ); +} */ + + +/* 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 ( + + + + + + + + Welcome to your Turn14 integration dashboard! + + + Use the navigation in the left sidebar to manage settings, view brands, + sync collections, and more. + + + + + + + ); +} + */ + + +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 ( + + + + + + + + Data4Autos Logo + + Welcome to your Turn14 Dashboard + + + + + + + + 🚀 Data4Autos Turn14 Integration gives you the power to sync + product brands, manage collections, and automate catalog setup directly from + Turn14 to your Shopify store. + + + + 🔧 Use the left sidebar to: + + + ⚙️ Manage API settings + 🏷️ Browse and import available brands + 📦 Sync brand collections to Shopify + 🔐 Handle secure Turn14 login credentials + + + + + + Status: Connected + Shopify x Turn14 + + + + Need help? Contact us at{" "} + support@data4autos.com + + + + + + + + ); +} + + + + +/* 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 ( + + + + + + + +

Redirecting to {tabs[selectedTab].content}...

+
+
+
+
+
+
+ ); +} + + */ + + + + + +/* 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 ( + + + + + + + + + {selectedTab === 0 && ( + + )} + + {selectedTab === 1 && ( + + View available brands from Turn14. + + + + + )} + + {selectedTab === 2 && ( + + Manage your synced brand collections. + + + + + )} + + {selectedTab === 3 && ( + + Help and documentation links. + + + + )} + + {selectedTab === 4 && ( + + Login to manage Turn14 credentials. + + + + + )} + + + + + + + ); +} + */ \ No newline at end of file diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx index 13e4199..85e25cc 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -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 ( - + + +
{ + 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 ( + + + + + + + + Need Help? You’re in the Right Place! + + + This section covers frequently asked questions about the Data4Autos + Turn14 integration app. + + + {faqs.map((faq, index) => ( +
+ + + + {faq.content} + + +
+ ))} + + + Still have questions? Email us at{" "} + + support@data4autos.com + + +
+
+
+
+
+ ); +} diff --git a/app/routes/app.jsx b/app/routes/app.jsx index 4bf8690..76a31bc 100644 --- a/app/routes/app.jsx +++ b/app/routes/app.jsx @@ -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 ( + + + 🏠 Home + ⚙️ Settings + 🏷️ Brands + 📦 Manage Brands + 🆘 Help + + + + ); +} + +export function ErrorBoundary() { + return boundary.error(useRouteError()); +} + export const headers = (headersArgs) => { return boundary.headers(headersArgs); }; diff --git a/app/routes/app.managebrand.jsx b/app/routes/app.managebrand.jsx index 1e6954a..5e29109 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -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: `

${attr.part_description}

`, - 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 ( - + + {brands.length === 0 && ( @@ -213,46 +340,54 @@ export default function ManageBrandProducts() { )} {brands.map((brand) => ( -
+ -

Brand: {brand.name}

ID: {brand.id}

-
- -
+
{expandedBrand === brand.id && ( - -
- -
+
@@ -260,7 +395,7 @@ export default function ManageBrandProducts() { ) : (
- {(itemsMap[brand.id] || []).map(item => ( + {(itemsMap[brand.id] || []).map((item) => ( @@ -276,9 +411,7 @@ export default function ManageBrandProducts() {

Part Number: {item.attributes.part_number}

-

Brand: {item.attributes.brand}

-

Category: {item.attributes.category} > {item.attributes.subcategory}

-

Dimensions: {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in

+

Category: {item.attributes.category} > {item.attributes.subcategory}

@@ -289,9 +422,203 @@ export default function ManageBrandProducts() {
)} -
+
))} ); } + */ +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 ( + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => ( + + {brand.id} + + + + + + + + ))} + + + + )} + + {brands.map( + (brand) => + expandedBrand === brand.id && ( + + + {actionData?.success && ( + +

+ {actionData.results.map((r) => ( + + Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
+
+ ))} +

+
+ )} +
+ + + + +
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+ {(itemsMap[brand.id] || []).map((item) => ( + + + + + + + +

Part Number: {item.attributes.part_number}

+

Category: {item.attributes.category} > {item.attributes.subcategory}

+
+
+
+
+ ))} +
+ )} +
+
+ ) + )} +
+
+ ); +} diff --git a/app/routes/app.managebrand1.jsx b/app/routes/app.managebrand1.jsx deleted file mode 100644 index 9a9c280..0000000 --- a/app/routes/app.managebrand1.jsx +++ /dev/null @@ -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 ( - - - {brands.length === 0 && ( - - -

No brands selected yet.

-
-
- )} - - {brands.map((brand) => ( - - - - - -

ID: {brand.id}

-
- -
-
- - {expandedBrand === brand.id && ( - - - {actionData?.success && ( - -

- {actionData.results.map((r) => ( - - Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
-
- ))} -

-
- )} -
- - - - -
- - - {loadingMap[brand.id] ? ( - - ) : ( -
- {(itemsMap[brand.id] || []).map((item) => ( - - - - - - - -

Part Number: {item.attributes.part_number}

-

Category: {item.attributes.category} > {item.attributes.subcategory}

-
-
-
-
- ))} -
- )} -
-
- )} -
- ))} -
-
- ); -} diff --git a/app/routes/app.managebrand_bak.jsx b/app/routes/app.managebrand_bak.jsx new file mode 100644 index 0000000..1e6954a --- /dev/null +++ b/app/routes/app.managebrand_bak.jsx @@ -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: `

${attr.part_description}

`, + 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 ( + + + {brands.length === 0 && ( + + +

No brands selected yet.

+
+
+ )} + + {brands.map((brand) => ( +
+ + + + +

Brand: {brand.name}

+

ID: {brand.id}

+
+
+ +
+
+
+ + {expandedBrand === brand.id && ( + + + +
+ +
+
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+ {(itemsMap[brand.id] || []).map(item => ( + + + + + + + +

Part Number: {item.attributes.part_number}

+

Brand: {item.attributes.brand}

+

Category: {item.attributes.category} > {item.attributes.subcategory}

+

Dimensions: {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in

+
+
+
+
+ ))} +
+ )} +
+
+ )} +
+ ))} +
+
+ ); +} diff --git a/app/routes/app.settings.jsx b/app/routes/app.settings.jsx index f9948a9..6f8924e 100644 --- a/app/routes/app.settings.jsx +++ b/app/routes/app.settings.jsx @@ -1,4 +1,4 @@ -import { json } from "@remix-run/node"; + import { json } from "@remix-run/node"; import { useLoaderData, useActionData, Form } from "@remix-run/react"; import { useState } from "react"; import { @@ -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 ( - + + @@ -206,3 +208,220 @@ export default function SettingsPage() { ); } + +/* +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 = ( + + + + + Connected Shop: {shopName} + +
+ + + + + +
+ + {actionData?.error && ( + + )} + + {displayToken && ( + + + ✅ Access token: + + + {displayToken} + + + )} +
+
+
+
+ ); + + return standalone ? {content} : content; +} + */ \ No newline at end of file diff --git a/app/shopify.server.js b/app/shopify.server.js index a11bf9d..c73d8f6 100644 --- a/app/shopify.server.js +++ b/app/shopify.server.js @@ -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 || "", diff --git a/package-lock.json b/package-lock.json index eb7d5bd..9fb90fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index adc02f9..a639117 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index af4a01d..37cc464 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,20 +1,14 @@ -// 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" } model Session { - id String @id + id String @id shop String state String isOnline Boolean @default(false) @@ -32,12 +26,12 @@ model Session { } model Turn14Credential { - id String @id @default(cuid()) - shop String @unique - clientId String - clientSecret String - accessToken String - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + shop String @unique + clientId String + clientSecret String + accessToken String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/shopify.app.toml b/shopify.app.toml index 142e22b..5761663 100644 --- a/shopify.app.toml +++ b/shopify.app.toml @@ -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