backend_shopify_data4autos/app.managebrand1.jsx
metatroncubeswdev 6cb1d01b0c Inital commit
2025-06-27 18:48:53 -04:00

460 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
const title = attrs.product_name;
const descriptionHtml = attrs.part_description;
const vendor = attrs.brand;
const productType = attrs.category;
const handle = slugify(attrs.part_number || title);
const tags = [attrs.category, attrs.subcategory, attrs.brand]
.filter(Boolean)
.map((t) => t.trim());
const price = attrs.price?.toString() || "1000";
const compare_price = attrs.compare_price?.toString() || "2000";
const costPerItem = attrs.purchase_cost?.toString() || "100";
const sku = attrs.part_number || '';
const collectionId = "gid://shopify/Collection/447659409624";
// 🔥 Build a CreateMediaInput[] from every file in attrs.files
const mediaInputs = (attrs.files || []).map((file) => ({
originalSource: file.url,
mediaContentType: "IMAGE",
alt: `${title} ${file.media_content}`,
}));
// 1⃣ Create product + images + join collection
const createRes = await admin.graphql(
`#graphql
mutation CreateFullProduct(
$product: ProductCreateInput!
$media: [CreateMediaInput!]
) {
productCreate(product: $product, media: $media) {
product {
id
variants(first: 1) {
nodes {
id
inventoryItem {
id
}
}
}
}
userErrors {
field
message
}
}
}`,
{
variables: {
product: {
title,
descriptionHtml,
vendor,
productType,
handle,
tags,
collectionsToJoin: [collectionId],
status: "ACTIVE",
published: true, // Ensures product is published to the Online Store
},
media: mediaInputs, // ← now includes all your images
},
}
);
const createJson = await createRes.json();
const createErrs = createJson.data.productCreate.userErrors;
if (createErrs.length) {
const handleTaken = createErrs.some((e) =>
/already in use/i.test(e.message)
);
if (handleTaken) {
results.push({ skippedHandle: handle, reason: "handle already in use" });
continue;
}
throw new Error(
`Create errors: ${createErrs.map((e) => e.message).join(", ")}`
);
}
const newProduct = createJson.data.productCreate.product;
const {
id: variantId,
inventoryItem: { id: inventoryItemId },
} = newProduct.variants.nodes[0];
// 2⃣ Bulk-update variant price + compareAtPrice
const variantInputs = [
{
id: variantId,
price: parseFloat(price),
...(compare_price && { compareAtPrice: parseFloat(compare_price) }),
},
];
const priceRes = await admin.graphql(
`#graphql
mutation UpdatePrice(
$productId: ID!
$variants: [ProductVariantsBulkInput!]!
) {
productVariantsBulkUpdate(
productId: $productId
variants: $variants
) {
productVariants {
id
price
compareAtPrice
}
userErrors {
field
message
}
}
}`,
{
variables: {
productId: newProduct.id,
variants: variantInputs,
},
}
);
const priceJson = await priceRes.json();
const priceErrs = priceJson.data.productVariantsBulkUpdate.userErrors;
if (priceErrs.length) {
throw new Error(
`Price update errors: ${priceErrs.map((e) => e.message).join(", ")}`
);
}
const updatedVariant =
priceJson.data.productVariantsBulkUpdate.productVariants[0];
// 3⃣ Update inventory item to set SKU & cost
const skuRes = await admin.graphql(
`#graphql
mutation SetSKUAndCost(
$id: ID!
$input: InventoryItemInput!
) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
sku
}
userErrors {
field
message
}
}
}`,
{
variables: {
id: inventoryItemId,
input: {
sku,
cost: parseFloat(costPerItem),
},
},
}
);
const skuJson = await skuRes.json();
const skuErrs = skuJson.data.inventoryItemUpdate.userErrors;
if (skuErrs.length) {
throw new Error(
`SKU update errors: ${skuErrs.map((e) => e.message).join(", ")}`
);
}
const updatedItem = skuJson.data.inventoryItemUpdate.inventoryItem;
const publicationsRes = await admin.graphql(
`#graphql
query {
publications(first: 5) {
nodes {
id
name
}
}
}`
);
const publications = await publicationsRes.json();
const onlineStorePublication = publications?.data?.publications?.nodes.find(
(pub) => pub.name === "Online Store"
);
const publicationId = onlineStorePublication?.id;
console.log("1234567", publicationId)
if (publicationId && newProduct.id) {
const publishRes = await admin.graphql(
`#graphql
mutation PublishProduct($productId: ID!, $publicationId: ID!) {
publishablePublish(
id: $productId,
input: [{ publicationId: $publicationId }]
) {
publishable {
publishedOnPublication(publicationId: $publicationId)
}
userErrors {
field
message
}
}
}`,
{
variables: {
productId: newProduct.id, // Product ID from the productCreate mutation
publicationId: publicationId, // Online Store Publication ID
},
}
);
const publishJson = await publishRes.json();
const publishErrs = publishJson.data.publishablePublish.userErrors;
if (publishErrs.length) {
throw new Error(
`Publish errors: ${publishErrs.map((e) => e.message).join(", ")}`
);
}
}
results.push({
productId: newProduct.id,
variant: {
id: updatedVariant.id,
price: updatedVariant.price,
compareAtPrice: updatedVariant.compareAtPrice,
sku: updatedItem.sku,
},
});
}
return json({ success: true, results });
};
// Main React component for managing brand products
export default function ManageBrandProducts() {
const actionData = useActionData();
const { brands, accessToken } = useLoaderData();
const [expandedBrand, setExpandedBrand] = useState(null);
const [itemsMap, setItemsMap] = useState({});
const [loadingMap, setLoadingMap] = useState({});
const [productCount, setProductCount] = useState("10");
const toggleBrandItems = async (brandId) => {
const isExpanded = expandedBrand === brandId;
if (isExpanded) {
setExpandedBrand(null);
} else {
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
} catch (err) {
console.error("Error fetching items:", err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
}
};
return (
<Page title="Manage Brand Products">
<Layout>
{brands.length === 0 && (
<Layout.Section>
<Card sectioned>
<p>No brands selected yet.</p>
</Card>
</Layout.Section>
)}
{brands.map((brand) => (
<React.Fragment key={brand.id}>
<Layout.Section oneThird>
<Card title={brand.name} sectioned>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="large"
/>
<TextContainer spacing="tight">
<p><strong>ID:</strong> {brand.id}</p>
</TextContainer>
<Button fullWidth onClick={() => toggleBrandItems(brand.id)}>
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</Card>
</Layout.Section>
{expandedBrand === brand.id && (
<Layout.Section fullWidth>
<Card sectioned>
{actionData?.success && (
<Banner title="✅ Product created!" status="success">
<p>
{actionData.results.map((r) => (
<span key={r.variant.id}>
Product {r.productId} Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
</span>
))}
</p>
</Banner>
)}
<Form method="post">
<input type="hidden" name="brandId" value={brand.id} />
<TextField
label="Number of products to add"
type="number"
name="productCount"
value={productCount}
onChange={setProductCount}
autoComplete="off"
/>
<Button submit primary style={{ marginTop: '1rem' }}>
Add First {productCount} Products to Store
</Button>
</Form>
</Card>
<Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" />
) : (
<div style={{ paddingTop: "1rem" }}>
{(itemsMap[brand.id] || []).map((item) => (
<Card key={item.id} title={item.attributes.product_name} sectioned>
<Layout>
<Layout.Section oneThird>
<Thumbnail
source={
item.attributes.thumbnail ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={item.attributes.product_name}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<p><strong>Part Number:</strong> {item.attributes.part_number}</p>
<p><strong>Category:</strong> {item.attributes.category} &gt; {item.attributes.subcategory}</p>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))}
</div>
)}
</Card>
</Layout.Section>
)}
</React.Fragment>
))}
</Layout>
</Page>
);
}