From c58cc7c6160fd263fee7e55b69129217baef66c8 Mon Sep 17 00:00:00 2001
From: root
Date: Mon, 30 Jun 2025 12:51:23 +0000
Subject: [PATCH] Your commit message here
---
app/routes/app.brands copy.jsx | 273 ++++++++++
app/routes/app.brands.jsx | 98 ++--
app/routes/app.managebrand.jsx | 513 +++---------------
app/routes/app.managebrand_bak.jsx | 2 +
app/routes/app.managebrand_bak_300625.jsx | 624 ++++++++++++++++++++++
app/routes/app.settings.jsx | 10 +-
shopify.app.toml | 2 +-
7 files changed, 1026 insertions(+), 496 deletions(-)
create mode 100644 app/routes/app.brands copy.jsx
create mode 100644 app/routes/app.managebrand_bak_300625.jsx
diff --git a/app/routes/app.brands copy.jsx b/app/routes/app.brands copy.jsx
new file mode 100644
index 0000000..85e25cc
--- /dev/null
+++ b/app/routes/app.brands copy.jsx
@@ -0,0 +1,273 @@
+import { json } from "@remix-run/node";
+import { useLoaderData, useFetcher } from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ Card,
+ TextField,
+ Checkbox,
+ Button,
+ Thumbnail,
+ Spinner,
+ Toast,
+ Frame,
+} from "@shopify/polaris";
+import { useEffect, useState } from "react";
+import { TitleBar } from "@shopify/app-bridge-react";
+import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }) => {
+ const accessToken = await getTurn14AccessTokenFromMetafield(request);
+ const { admin } = await authenticate.admin(request);
+
+ // Get brands
+ const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ const brandJson = await brandRes.json();
+ if (!brandRes.ok) {
+ return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 });
+ }
+
+ // Get collections
+ const gqlRaw = await admin.graphql(`
+ {
+ collections(first: 100) {
+ edges {
+ node {
+ id
+ title
+ handle
+ }
+ }
+ }
+ }
+ `);
+ const gql = await gqlRaw.json();
+ const collections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
+
+ return json({
+ brands: brandJson.data,
+ collections,
+ });
+};
+
+export const action = async ({ request }) => {
+ const formData = await request.formData();
+ const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
+
+ const { admin } = await authenticate.admin(request);
+
+ // Get current collections
+ const gqlRaw = await admin.graphql(`
+ {
+ collections(first: 100) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+ `);
+ const gql = await gqlRaw.json();
+ const existingCollections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
+
+ const selectedTitles = selectedBrands.map((b) => b.name.toLowerCase());
+ const logoMap = Object.fromEntries(selectedBrands.map(b => [b.name.toLowerCase(), b.logo]));
+
+ // Delete unselected
+ for (const col of existingCollections) {
+ if (!selectedTitles.includes(col.title.toLowerCase())) {
+ await admin.graphql(`
+ mutation {
+ collectionDelete(input: { id: "${col.id}" }) {
+ deletedCollectionId
+ userErrors { message }
+ }
+ }
+ `);
+ }
+ }
+
+
+ // Create new
+ for (const brand of selectedBrands) {
+ const exists = existingCollections.find(
+ (c) => c.title.toLowerCase() === brand.name.toLowerCase()
+ );
+ if (!exists) {
+ const escapedName = brand.name.replace(/"/g, '\\"');
+ const logo = brand.logo || "";
+
+ await admin.graphql(`
+ mutation {
+ collectionCreate(input: {
+ title: "${escapedName}",
+ descriptionHtml: "Products from brand ${escapedName}",
+ image: {
+ altText: "${escapedName} Logo",
+ src: "${logo}"
+ }
+ }) {
+ collection { id }
+ userErrors { message }
+ }
+ }
+ `);
+ }
+ }
+
+const shopDataRaw = await admin.graphql(`
+ {
+ shop {
+ id
+ }
+ }
+`);
+const shopRes = await admin.graphql(`{ shop { id } }`);
+const shopJson = await shopRes.json();
+const shopId = shopJson?.data?.shop?.id;
+
+await admin.graphql(`
+ mutation {
+ metafieldsSet(metafields: [{
+ namespace: "turn14",
+ key: "selected_brands",
+ type: "json",
+ ownerId: "${shopId}",
+ value: ${JSON.stringify(JSON.stringify(selectedBrands))}
+ }]) {
+ metafields {
+ id
+ }
+ userErrors {
+ message
+ }
+ }
+ }
+`);
+
+
+
+ return json({ success: true });
+
+};
+
+export default function BrandsPage() {
+ const { brands, collections } = useLoaderData();
+ const fetcher = useFetcher();
+ const isSubmitting = fetcher.state === "submitting";
+ const [toastActive, setToastActive] = useState(false);
+ const [search, setSearch] = useState("");
+
+ const collectionTitles = new Set(collections.map((c) => c.title.toLowerCase()));
+ const defaultSelected = brands
+ .filter((b) => collectionTitles.has(b.name.toLowerCase()))
+ .map((b) => b.id);
+
+ const [selectedIds, setSelectedIds] = useState(defaultSelected);
+ const [filteredBrands, setFilteredBrands] = useState(brands);
+
+ useEffect(() => {
+ const term = search.toLowerCase();
+ setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term)));
+ }, [search, brands]);
+
+ useEffect(() => {
+ if (fetcher.data?.success) {
+ setToastActive(true);
+ }
+ }, [fetcher.data]);
+
+ const toggleSelect = (id) => {
+ setSelectedIds((prev) =>
+ prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
+ );
+ };
+
+ const toastMarkup = toastActive ? (
+ setToastActive(false)}
+ />
+ ) : null;
+
+ const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {filteredBrands.map((brand) => (
+
+
+
toggleSelect(brand.id)}
+ />
+
+
+ {brand.name}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {toastMarkup}
+
+
+ );
+}
diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx
index 85e25cc..a057342 100644
--- a/app/routes/app.brands.jsx
+++ b/app/routes/app.brands.jsx
@@ -95,7 +95,6 @@ export const action = async ({ request }) => {
}
}
-
// Create new
for (const brand of selectedBrands) {
const exists = existingCollections.find(
@@ -123,40 +122,37 @@ export const action = async ({ request }) => {
}
}
-const shopDataRaw = await admin.graphql(`
- {
- shop {
- id
- }
- }
-`);
-const shopRes = await admin.graphql(`{ shop { id } }`);
-const shopJson = await shopRes.json();
-const shopId = shopJson?.data?.shop?.id;
-
-await admin.graphql(`
- mutation {
- metafieldsSet(metafields: [{
- namespace: "turn14",
- key: "selected_brands",
- type: "json",
- ownerId: "${shopId}",
- value: ${JSON.stringify(JSON.stringify(selectedBrands))}
- }]) {
- metafields {
+ const shopDataRaw = await admin.graphql(`
+ {
+ shop {
id
}
- userErrors {
- message
+ }
+ `);
+ const shopRes = await admin.graphql(`{ shop { id } }`);
+ const shopJson = await shopRes.json();
+ const shopId = shopJson?.data?.shop?.id;
+
+ await admin.graphql(`
+ mutation {
+ metafieldsSet(metafields: [{
+ namespace: "turn14",
+ key: "selected_brands",
+ type: "json",
+ ownerId: "${shopId}",
+ value: ${JSON.stringify(JSON.stringify(selectedBrands))}
+ }]) {
+ metafields {
+ id
+ }
+ userErrors {
+ message
+ }
}
}
- }
-`);
-
-
+ `);
return json({ success: true });
-
};
export default function BrandsPage() {
@@ -191,6 +187,22 @@ export default function BrandsPage() {
);
};
+ const toggleSelectAll = () => {
+ const filteredBrandIds = filteredBrands.map(b => b.id);
+ const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id));
+
+ if (allFilteredSelected) {
+ // Deselect all filtered brands
+ setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id)));
+ } else {
+ // Select all filtered brands
+ setSelectedIds(prev => {
+ const combined = new Set([...prev, ...filteredBrandIds]);
+ return Array.from(combined);
+ });
+ }
+ };
+
const toastMarkup = toastActive ? (
selectedIds.includes(b.id));
+ const allFilteredSelected = filteredBrands.length > 0 &&
+ filteredBrands.every(brand => selectedIds.includes(brand.id));
return (
@@ -206,14 +220,22 @@ export default function BrandsPage() {
-
-
+
@@ -270,4 +292,4 @@ export default function BrandsPage() {
);
-}
+}
\ No newline at end of file
diff --git a/app/routes/app.managebrand.jsx b/app/routes/app.managebrand.jsx
index 5e29109..f5dc9cb 100644
--- a/app/routes/app.managebrand.jsx
+++ b/app/routes/app.managebrand.jsx
@@ -1,435 +1,5 @@
-/* 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";
-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 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}
-
-
-
-
- ))}
-
- )}
-
-
- )}
-
- ))}
-
-
- );
-}
- */
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
@@ -478,7 +48,29 @@ export default function ManageBrandProducts() {
const [expandedBrand, setExpandedBrand] = useState(null);
const [itemsMap, setItemsMap] = useState({});
const [loadingMap, setLoadingMap] = useState({});
- const [productCount, setProductCount] = useState("10");
+ const [productCount, setProductCount] = useState("0");
+
+
+
+
+ const [initialLoad, setInitialLoad] = useState(true);
+
+// Function to toggle all brands
+const toggleAllBrands = async () => {
+ for (const brand of brands) {
+ await toggleBrandItems(brand.id);
+ }
+};
+
+// Run on initial load
+useEffect(() => {
+ if (initialLoad && brands.length > 0) {
+ toggleAllBrands();
+ setInitialLoad(false);
+ }
+}, [brands, initialLoad]);
+
+
const toggleBrandItems = async (brandId) => {
const isExpanded = expandedBrand === brandId;
@@ -486,21 +78,26 @@ export default function ManageBrandProducts() {
setExpandedBrand(null);
} else {
setExpandedBrand(brandId);
+
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
- const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
+ // const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
+ const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
+ setProductCount(data.length)
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
} catch (err) {
console.error("Error fetching items:", err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
+ } else {
+ setProductCount(itemsMap[brandId].length)
}
}
};
@@ -525,6 +122,7 @@ export default function ManageBrandProducts() {
{ title: "Brand ID" },
{ title: "Logo" },
{ title: "Action" },
+ { title: "Products Count" },
]}
selectable={false}
>
@@ -546,6 +144,7 @@ export default function ManageBrandProducts() {
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
+ {itemsMap[brand.id]?.length || 0}
))}
@@ -569,44 +168,52 @@ export default function ManageBrandProducts() {
)}
-
+
{loadingMap[brand.id] ? (
) : (
+
+
- {(itemsMap[brand.id] || []).map((item) => (
-
+
+ Total Products Count : {(itemsMap[brand.id] || []).length}
+ {(
+ itemsMap[brand.id] && itemsMap[brand.id].length > 0
+ ? itemsMap[brand.id]
+ : []
+ ).map((item) => (
+
- Part Number: {item.attributes.part_number}
- Category: {item.attributes.category} > {item.attributes.subcategory}
+ 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
index 1e6954a..01207a7 100644
--- a/app/routes/app.managebrand_bak.jsx
+++ b/app/routes/app.managebrand_bak.jsx
@@ -260,6 +260,8 @@ export default function ManageBrandProducts() {
) : (
+ Total Products Count : {(itemsMap[brand.id] || []).length}
+
{(itemsMap[brand.id] || []).map(item => (
diff --git a/app/routes/app.managebrand_bak_300625.jsx b/app/routes/app.managebrand_bak_300625.jsx
new file mode 100644
index 0000000..5e29109
--- /dev/null
+++ b/app/routes/app.managebrand_bak_300625.jsx
@@ -0,0 +1,624 @@
+/* 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";
+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 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}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+ );
+}
+ */
+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.settings.jsx b/app/routes/app.settings.jsx
index 6f8924e..5f50384 100644
--- a/app/routes/app.settings.jsx
+++ b/app/routes/app.settings.jsx
@@ -111,6 +111,8 @@ export const action = async ({ request }) => {
}
`;
+
+
const saveRes = await admin.graphql(mutation);
const result = await saveRes.json();
@@ -177,7 +179,7 @@ export default function SettingsPage({ standalone = true }) {
/>
@@ -190,8 +192,8 @@ export default function SettingsPage({ standalone = true }) {
{displayToken && (
- ✅ Access token:
- ✅ Connection Successful
+ {/*
{displayToken}
-
+ */}
)}
diff --git a/shopify.app.toml b/shopify.app.toml
index 5761663..fbb8a86 100644
--- a/shopify.app.toml
+++ b/shopify.app.toml
@@ -1,6 +1,6 @@
client_id = "b7534c980967bad619cfdb9d3f837cfa"
name = "turn14-test"
-handle = "turn14-test-1"
+handle = "d4a-turn14"
application_url = "https://shopify.data4autos.com" # Update this line
embedded = true