metatroncubeswdev 6cb1d01b0c Inital commit
2025-06-27 18:48:53 -04:00

314 lines
8.8 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.

// app/routes/add-full-product.jsx
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
import {
Page,
Layout,
Card,
TextField,
Select,
Button,
Banner,
InlineError,
Text,
} from "@shopify/polaris";
import { useState, useEffect } from "react";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const resp = await admin.graphql(`
{
collections(first: 10, query: "collection_type:manual") {
nodes { id title }
}
}
`);
const { data } = await resp.json();
return json({ collections: data.collections.nodes });
};
export const action = async ({ request }) => {
const form = await request.formData();
const {
title,
descriptionHtml,
vendor,
productType,
handle,
tags,
price,
sku,
costPerItem, // new
collectionId,
imageUrl,
} = Object.fromEntries(form);
const { admin } = await authenticate.admin(request);
// 1⃣ Create product + attach image + 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: tags.split(",").map((t) => t.trim()),
collectionsToJoin: [collectionId],
status: "ACTIVE",
},
media: [
{
originalSource: imageUrl,
mediaContentType: "IMAGE",
alt: `${title} image`,
},
],
},
}
);
const createJson = await createRes.json();
if (createJson.data.productCreate.userErrors.length) {
return json(
{ errors: createJson.data.productCreate.userErrors.map((e) => e.message) },
{ status: 400 }
);
}
const newProduct = createJson.data.productCreate.product;
const variantNode = newProduct.variants.nodes[0];
const variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem.id;
// 2⃣ Set price via bulk update
const priceRes = await admin.graphql(
`#graphql
mutation UpdatePrice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id price }
userErrors { field message }
}
}`,
{
variables: {
productId: newProduct.id,
variants: [{ id: variantId, price: parseFloat(price) }],
},
}
);
const priceJson = await priceRes.json();
if (priceJson.data.productVariantsBulkUpdate.userErrors.length) {
return json(
{ errors: priceJson.data.productVariantsBulkUpdate.userErrors.map((e) => e.message) },
{ status: 400 }
);
}
const updatedVariant = priceJson.data.productVariantsBulkUpdate.productVariants[0];
// 3⃣ Update inventory item: set SKU + cost per item
const skuCostRes = 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), // unit cost in shops default currency :contentReference[oaicite:1]{index=1}
},
},
}
);
const skuCostJson = await skuCostRes.json();
if (skuCostJson.data.inventoryItemUpdate.userErrors.length) {
return json(
{ errors: skuCostJson.data.inventoryItemUpdate.userErrors.map((e) => e.message) },
{ status: 400 }
);
}
const finalSKU = skuCostJson.data.inventoryItemUpdate.inventoryItem.sku;
return json({
success: true,
product: newProduct,
variant: {
id: updatedVariant.id,
price: updatedVariant.price,
sku: finalSKU,
},
});
};
export default function AddFullProductPage() {
const { collections } = useLoaderData();
const actionData = useActionData();
// Controlled inputs
const [title, setTitle] = useState("Dummy Test Product");
const [descriptionHtml, setDescriptionHtml] = useState(
"<p>This is a dummy product for testing.</p>"
);
const [vendor, setVendor] = useState("Test Vendor");
const [productType, setProductType] = useState("Test Type");
const [handle, setHandle] = useState("dummy-test-product");
const [tags, setTags] = useState("test, dummy, sample");
const [price, setPrice] = useState("9.99");
const [costPerItem, setCostPerItem] = useState("5.00"); // new default
const [sku, setSku] = useState("DUMMY-9");
const [imageUrl, setImageUrl] = useState(
"https://via.placeholder.com/300x300.png?text=Dummy+Image"
);
const [collectionId, setCollectionId] = useState(
collections[0]?.id || ""
);
useEffect(() => {
if (!collectionId && collections.length) {
setCollectionId(collections[0].id);
}
}, [collections, collectionId]);
return (
<Page title="Add Full Product (Price + Cost)">
<Layout>
<Layout.Section>
<Card sectioned>
{actionData?.success && (
<Banner title="✅ Product created!" status="success">
<p>
{actionData.product.id} Variant {actionData.variant.id} @ $
{actionData.variant.price} (SKU: {actionData.variant.sku})
</p>
</Banner>
)}
{actionData?.errors && (
<Banner title="🚨 Errors" status="critical">
<ul>
{actionData.errors.map((msg, i) => (
<li key={i}>
<InlineError message={msg} />
</li>
))}
</ul>
</Banner>
)}
<Form method="post">
<TextField
label="Title"
name="title"
value={title}
onChange={setTitle}
required
/>
<TextField
label="Description (HTML)"
name="descriptionHtml"
multiline={4}
value={descriptionHtml}
onChange={setDescriptionHtml}
required
/>
<TextField
label="Vendor"
name="vendor"
value={vendor}
onChange={setVendor}
required
/>
<TextField
label="Product Type"
name="productType"
value={productType}
onChange={setProductType}
required
/>
<TextField
label="Handle (URL suffix)"
name="handle"
value={handle}
onChange={setHandle}
required
/>
<TextField
label="Tags (comma-separated)"
name="tags"
value={tags}
onChange={setTags}
/>
<TextField
type="number"
step="0.01"
label="Price (USD)"
name="price"
value={price}
onChange={setPrice}
required
/>
<TextField
type="number"
step="0.01"
label="Cost per Item"
name="costPerItem"
value={costPerItem}
onChange={setCostPerItem}
required
/>
<TextField
label="SKU"
name="sku"
value={sku}
onChange={setSku}
required
/>
<TextField
label="Image URL"
name="imageUrl"
value={imageUrl}
onChange={setImageUrl}
/>
<Select
label="Add to Collection"
name="collectionId"
options={collections.map((c) => ({
label: c.title,
value: c.id,
}))}
value={collectionId}
onChange={setCollectionId}
required
/>
<Button submit primary style={{ marginTop: "1rem" }}>
Create Product
</Button>
</Form>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}