298 lines
9.7 KiB
JavaScript
298 lines
9.7 KiB
JavaScript
import { json } from "@remix-run/node";
|
|
import { useLoaderData } from "@remix-run/react";
|
|
import {
|
|
Page,
|
|
Layout,
|
|
Card,
|
|
Thumbnail,
|
|
TextContainer,
|
|
Spinner,
|
|
Button,
|
|
Text,
|
|
TextField,
|
|
} from "@shopify/polaris";
|
|
import { useState } from "react";
|
|
import { authenticate } from "../shopify.server";
|
|
|
|
export const loader = async ({ request }) => {
|
|
const { admin } = await authenticate.admin(request);
|
|
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
|
|
const accessToken = await getTurn14AccessTokenFromMetafield(request);
|
|
|
|
const res = await admin.graphql(`
|
|
{
|
|
shop {
|
|
metafield(namespace: "turn14", key: "selected_brands") {
|
|
value
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
const data = await res.json();
|
|
const rawValue = data?.data?.shop?.metafield?.value;
|
|
|
|
let brands = [];
|
|
try {
|
|
brands = JSON.parse(rawValue);
|
|
} catch (err) {
|
|
console.error("❌ Failed to parse metafield value:", err);
|
|
}
|
|
|
|
return json({ brands, accessToken });
|
|
};
|
|
|
|
export default function ManageBrandProducts() {
|
|
const { brands, accessToken } = useLoaderData();
|
|
const [expandedBrand, setExpandedBrand] = useState(null);
|
|
const [itemsMap, setItemsMap] = useState({});
|
|
const [loadingMap, setLoadingMap] = useState({});
|
|
const [productCount, setProductCount] = useState("10");
|
|
const [adding, setAdding] = useState(false);
|
|
|
|
const toggleBrandItems = async (brandId) => {
|
|
const isExpanded = expandedBrand === brandId;
|
|
if (isExpanded) {
|
|
setExpandedBrand(null);
|
|
} else {
|
|
setExpandedBrand(brandId);
|
|
if (!itemsMap[brandId]) {
|
|
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
|
try {
|
|
const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
const data = await res.json();
|
|
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
|
|
} catch (err) {
|
|
console.error("Error fetching items:", err);
|
|
}
|
|
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleAddProducts = async (brandId) => {
|
|
const count = parseInt(productCount || "10");
|
|
const items = (itemsMap[brandId] || []).slice(0, count);
|
|
if (!items.length) return alert("No products to add.");
|
|
setAdding(true);
|
|
|
|
for (const item of items) {
|
|
const attr = item.attributes;
|
|
|
|
// Step 1: Create Product (only allowed fields)
|
|
const productInput = {
|
|
title: attr.product_name,
|
|
descriptionHtml: `<p>${attr.part_description}</p>`,
|
|
vendor: attr.brand,
|
|
productType: attr.category,
|
|
tags: [attr.subcategory, attr.brand].filter(Boolean).join(", "),
|
|
};
|
|
|
|
const createProductRes = await fetch("/admin/api/2023-04/graphql.json", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Shopify-Access-Token": accessToken,
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
mutation productCreate($input: ProductInput!) {
|
|
productCreate(input: $input) {
|
|
product {
|
|
id
|
|
title
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}`,
|
|
variables: { input: productInput },
|
|
}),
|
|
});
|
|
|
|
const createProductResult = await createProductRes.json();
|
|
const product = createProductResult?.data?.productCreate?.product;
|
|
const productErrors = createProductResult?.data?.productCreate?.userErrors;
|
|
|
|
if (productErrors?.length || !product?.id) {
|
|
console.error("❌ Product create error:", productErrors);
|
|
continue;
|
|
}
|
|
|
|
const productId = product.id;
|
|
|
|
// Step 2: Create Variant
|
|
const variantInput = {
|
|
productId,
|
|
sku: attr.part_number,
|
|
barcode: attr.barcode || undefined,
|
|
price: "0.00",
|
|
weight: attr.dimensions?.[0]?.weight || 0,
|
|
weightUnit: "KILOGRAMS",
|
|
inventoryManagement: "SHOPIFY",
|
|
};
|
|
|
|
await fetch("/admin/api/2023-04/graphql.json", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Shopify-Access-Token": accessToken,
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
mutation productVariantCreate($input: ProductVariantInput!) {
|
|
productVariantCreate(input: $input) {
|
|
productVariant {
|
|
id
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}`,
|
|
variables: { input: variantInput },
|
|
}),
|
|
});
|
|
|
|
// Step 3: Add Image
|
|
if (attr.thumbnail) {
|
|
await fetch("/admin/api/2023-04/graphql.json", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Shopify-Access-Token": accessToken,
|
|
},
|
|
body: JSON.stringify({
|
|
query: `
|
|
mutation productImageCreate($productId: ID!, $image: ImageInput!) {
|
|
productImageCreate(productId: $productId, image: $image) {
|
|
image {
|
|
id
|
|
src
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}`,
|
|
variables: {
|
|
productId,
|
|
image: {
|
|
src: attr.thumbnail,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
console.log("✅ Added:", attr.product_name);
|
|
}
|
|
|
|
setAdding(false);
|
|
alert(`${items.length} products added.`);
|
|
};
|
|
|
|
return (
|
|
<Page title="Manage Brand Products">
|
|
<Layout>
|
|
{brands.length === 0 && (
|
|
<Layout.Section>
|
|
<Card sectioned>
|
|
<p>No brands selected yet.</p>
|
|
</Card>
|
|
</Layout.Section>
|
|
)}
|
|
|
|
{brands.map((brand) => (
|
|
<div key={brand.id}>
|
|
<Layout.Section oneThird>
|
|
<Card title={brand.name} sectioned>
|
|
<Thumbnail
|
|
source={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
|
|
alt={brand.name}
|
|
size="large"
|
|
/>
|
|
<TextContainer spacing="tight">
|
|
<p><strong>Brand:</strong> {brand.name}</p>
|
|
<p><strong>ID:</strong> {brand.id}</p>
|
|
</TextContainer>
|
|
<div style={{ marginTop: "1rem" }}>
|
|
<Button onClick={() => toggleBrandItems(brand.id)} fullWidth>
|
|
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</Layout.Section>
|
|
|
|
{expandedBrand === brand.id && (
|
|
<Layout.Section fullWidth>
|
|
<Card sectioned>
|
|
<TextField
|
|
label="Number of products to add"
|
|
type="number"
|
|
value={productCount}
|
|
onChange={setProductCount}
|
|
autoComplete="off"
|
|
/>
|
|
<div style={{ marginTop: "1rem" }}>
|
|
<Button
|
|
onClick={() => handleAddProducts(brand.id)}
|
|
loading={adding}
|
|
disabled={adding}
|
|
primary
|
|
>
|
|
Add First {productCount} Products to Store
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={`Items from ${brand.name}`} sectioned>
|
|
{loadingMap[brand.id] ? (
|
|
<Spinner accessibilityLabel="Loading items" size="small" />
|
|
) : (
|
|
<div style={{ paddingTop: "1rem" }}>
|
|
{(itemsMap[brand.id] || []).map(item => (
|
|
<Card key={item.id} title={item.attributes.product_name} sectioned>
|
|
<Layout>
|
|
<Layout.Section oneThird>
|
|
<Thumbnail
|
|
source={
|
|
item.attributes.thumbnail ||
|
|
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
|
|
}
|
|
alt={item.attributes.product_name}
|
|
size="large"
|
|
/>
|
|
</Layout.Section>
|
|
<Layout.Section>
|
|
<TextContainer spacing="tight">
|
|
<p><strong>Part Number:</strong> {item.attributes.part_number}</p>
|
|
<p><strong>Brand:</strong> {item.attributes.brand}</p>
|
|
<p><strong>Category:</strong> {item.attributes.category} > {item.attributes.subcategory}</p>
|
|
<p><strong>Dimensions:</strong> {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in</p>
|
|
</TextContainer>
|
|
</Layout.Section>
|
|
</Layout>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</Layout.Section>
|
|
)}
|
|
</div>
|
|
))}
|
|
</Layout>
|
|
</Page>
|
|
);
|
|
}
|