This commit is contained in:
Dominic Ferrando
2025-09-21 21:01:38 -04:00
parent b3253425e3
commit 4f395e9a7c
9 changed files with 530 additions and 377 deletions
+55 -33
View File
@@ -7,6 +7,8 @@ import { Webflow } from "$lib/services/commerce/webflow";
import { Printful } from "$lib/services/commerce/printful"; import { Printful } from "$lib/services/commerce/printful";
import rawStandards from "$lib/data/standards.json" assert { type: "json" } import rawStandards from "$lib/data/standards.json" assert { type: "json" }
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const app = new Elysia({ prefix: "/api" }) export const app = new Elysia({ prefix: "/api" })
.use(cors({ .use(cors({
origin: '*' origin: '*'
@@ -25,58 +27,78 @@ export const app = new Elysia({ prefix: "/api" })
const { player, activityPerformances } = body as CalcRequest; const { player, activityPerformances } = body as CalcRequest;
const levelCalculator = new LevelCalculator(MAIN_STANDARDS); const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
return { levels: levelCalculator.calculate(player, activityPerformances) }; return { levels: levelCalculator.calculate(player, activityPerformances) };
}, {
error({ error }) {
console.error(error);
return {
message: "Failed to calculate",
error
}
}
}) })
// COMMERCE // COMMERCE
.post("/products/sync", async ({ body, set }) => { .post("/products/sync", async ({ body, set }) => {
console.log("Syncing..."); console.log("Syncing...");
try { const printfulProducts = await Printful.Products.getAll();
const printfulProducts = await Printful.Products.getAll(); const webflowProducts = await Webflow.Products.getAll();
const webflowProducts = await Webflow.Products.getAll(); for (const printfulProduct of printfulProducts) {
for (const p of printfulProducts) { await sleep(2000);
const exists = webflowProducts.some(wp => wp.id === p.external_id); console.log(printfulProduct.name, printfulProduct.id, printfulProduct.external_id);
const fullPrintfulProduct = await Printful.Products.get(printfulProduct.id);
const fullPrintfulProduct = await Printful.Products.get(p.id); let webflowProduct = webflowProducts
if (exists) .find(webflowProduct => webflowProduct.product.id === printfulProduct.external_id);
await Webflow.Products.updateUsingPrintful(fullPrintfulProduct); if (webflowProduct)
else await Webflow.Products.updateUsingPrintful(fullPrintfulProduct);
await Webflow.Products.createUsingPrintful(fullPrintfulProduct); else
} webflowProduct = await Webflow.Products.createUsingPrintful(fullPrintfulProduct);
} await Webflow.Products.syncImages(webflowProduct);
catch (error) {
console.error(error);
set.status = 500;
return { error: "internal_error" };
} }
console.log("Done"); console.log("Done");
return ""; return "";
}, {
error({ error }) {
console.error(error);
return {
message: "Failed to sync products",
error
}
}
}) })
// WEBHOOKS // WEBHOOKS
.post("/webhook/printful", async ({ body, set }) => { .post("/webhook/printful", async ({ body, set }) => {
const payload = body as Printful.Webhook.EventPayload; const payload = body as Printful.Webhook.EventPayload;
try { switch (payload.type) {
switch (payload.type) { case Printful.Webhook.Event.ProductUpdated: {
case Printful.Webhook.Event.ProductUpdated: { const printfulProduct = await Printful.Products.get(payload.data.sync_product.id);
const prod = await Printful.Products.get(payload.data.sync_product.id);
const exists = await Webflow.Products.exists(prod.sync_product.external_id); let webflowProduct = await Webflow.Products.get(printfulProduct.sync_product.external_id);
if (exists) await Webflow.Products.updateUsingPrintful(prod); if (webflowProduct)
else await Webflow.Products.createUsingPrintful(prod); await Webflow.Products.updateUsingPrintful(printfulProduct);
break; else
} webflowProduct = await Webflow.Products.createUsingPrintful(printfulProduct);
case Printful.Webhook.Event.ProductDeleted: { await Webflow.Products.syncImages(webflowProduct);
await Webflow.Products.remove(payload.data.sync_product.external_id);
break; break;
} }
case Printful.Webhook.Event.ProductDeleted: {
await Webflow.Products.remove(payload.data.sync_product.external_id);
break;
} }
} catch (error) {
console.error(error);
set.status = 500;
return { error: "internal_error" };
} }
return ""; return "";
}, {
error({ error }) {
console.error(error);
return {
message: "Failed to execute webhook",
error
}
}
}); });
export type App = typeof app; export type App = typeof app;
+1
View File
@@ -102,6 +102,7 @@ export const range = (length: number) => Array.from({ length: length }, (_, i) =
// return interpolatedAvg; // return interpolatedAvg;
// } // }
export const getAvgWeight = (gender: Gender, age: number) => { export const getAvgWeight = (gender: Gender, age: number) => {
type AvgWeightData = { type AvgWeightData = {
metadata: { metadata: {
+121 -66
View File
@@ -1,5 +1,15 @@
import { formatSlug, type DeepPartial } from "./util";
import type { Webflow } from "./webflow";
export namespace Printful { export namespace Printful {
export const API_URL = "https://api.printful.com"; export const API_URL = "https://api.printful.com";
const AUTH_TOKEN = typeof Bun === "undefined" ? "" : Bun.env.PRINTFUL_AUTH;
const STORE_ID = typeof Bun === "undefined" ? "" : Bun.env.PRINTFUL_STORE_ID;
const AUTH_HEADERS = {
"Authorization": `Bearer ${AUTH_TOKEN}`,
"X-PF-Store-Id": `${STORE_ID}`
};
export namespace Webhook { export namespace Webhook {
// meta // meta
@@ -61,12 +71,12 @@ export namespace Printful {
} }
} }
interface Option { type Option = {
id: string; id: string;
value: string; value: string;
} }
interface File { type File = {
type: string; type: string;
id: number; id: number;
url: string; url: string;
@@ -87,72 +97,63 @@ export namespace Printful {
stitch_count_tier: string; stitch_count_tier: string;
} }
export interface SyncProduct { export type Product = {
sync_product: { sync_product: SyncProduct,
id: number; sync_variants: SyncVariant[]
external_id: string; }
name: string;
variants: number; export type SyncProduct = {
synced: number; id: number;
thumbnail_url: string; external_id: string;
is_ignored: boolean; name: string;
}, variants: number;
sync_variants: { synced: number;
id: number; thumbnail_url: string;
external_id: string; is_ignored: boolean;
sync_product_id: number; }
name: string;
synced: boolean; export type SyncVariant = {
id: number;
external_id: string;
sync_product_id: number;
name: string;
synced: boolean;
variant_id: number;
retail_price: string;
currency: string;
is_ignored: boolean;
sku: string;
product: {
variant_id: number; variant_id: number;
retail_price: string; product_id: number;
currency: string; image: string;
is_ignored: boolean; name: string;
sku: string; };
product: { files: File[];
variant_id: number; options: Option[];
product_id: number; main_category_id: number;
image: string; warehouse_product_id: number;
name: string; warehouse_product_variant_id: number;
}; size: string;
files: File[]; color: string;
options: Option[]; availability_status: string;
main_category_id: number;
warehouse_product_id: number;
warehouse_product_variant_id: number;
size: string;
color: string;
availability_status: string;
}[]
} }
export async function get(syncProductId: number): Promise<SyncProduct> { export async function getAll(offset: number = 0): Promise<SyncProduct[]> {
const payload: MetaDataSingle<SyncProduct> = await ( const allSyncProducts: SyncProduct[] = [];
await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).json();
return payload.result;
}
export async function getAll(offset: number = 0): Promise<SyncProduct["sync_product"][]> {
const allSyncProducts: SyncProduct["sync_product"][] = [];
while (true) { while (true) {
try { try {
const payload: MetaDataMulti<SyncProduct["sync_product"]> = await ( const res = await fetch(`${Printful.API_URL}/store/products?offset=${offset}`, {
await fetch(`${Printful.API_URL}/store/products?offset=${offset}`, { method: "GET",
method: "GET", headers: { ...AUTH_HEADERS }
headers: { })
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` if (!res.ok) {
} console.log(res.statusText);
}) }
).json();
const payload: MetaDataMulti<SyncProduct> = await res.json();
allSyncProducts.push(...payload.result); allSyncProducts.push(...payload.result);
offset = allSyncProducts.length; offset = allSyncProducts.length;
@@ -160,17 +161,71 @@ export namespace Printful {
if (allSyncProducts.length >= payload.paging.total) if (allSyncProducts.length >= payload.paging.total)
break; break;
} catch (error) { } catch (error) {
break; console.error("Printful product get all failed:", error);
throw new Error("Failed to get all Printful products",);
} }
} }
return allSyncProducts; return allSyncProducts;
} }
export async function get(printfulProductId: number): Promise<Product> {
const res = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, {
method: "GET",
headers: { ...AUTH_HEADERS }
});
export function getVariantPreviewUrl(syncVariant: Printful.Products.SyncProduct["sync_variants"][number]): string { if (!res.ok) {
const previewFile = syncVariant.files.find(f => f.type === "preview"); console.error("Printful product update failed:", res.statusText);
return previewFile?.preview_url ?? syncVariant.product.image; throw new Error("Failed to update Printful product");
}
const payload: MetaDataSingle<Product> = await res.json();
return payload.result;
}
export async function update(printfulProductId: number, printfulProduct: DeepPartial<Product>) {
const res = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
...AUTH_HEADERS
},
body: JSON.stringify(printfulProduct)
});
if (!res.ok) {
console.error("Printful product update failed:", res.statusText);
throw new Error("Failed to update Printful product");
}
}
export function convertVariantsToWebflowSkus(printfulVariants: SyncVariant[]) {
const getVariantMainImage = (syncVariant: SyncVariant): string => {
const previewFile = syncVariant.files.find(f => f.type === "preview");
return previewFile?.preview_url ?? syncVariant.product.image;
}
const webflowVariants: DeepPartial<Webflow.Products.Skus.Sku>[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": getVariantMainImage(printfulVariant)
}
});
}
return webflowVariants;
} }
} }
} }
+26
View File
@@ -0,0 +1,26 @@
export type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
export const formatSlug = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
};
export const getProductImageUrls = async (slug: string) => {
const res = await (fetch(`${R2_WORKER_URL}/product-images/${slug}`));
if (!res.ok) {
console.error("Failed to get product image keys:", res.statusText);
throw new Error("Failed to get product image keys");
}
const productImageKeys: string[] = await res.json();
return productImageKeys.map(key => `${WEBSITE_MEDIA_URL}/${key}`);
}
export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
+269 -248
View File
@@ -1,301 +1,322 @@
import { Printful } from "$lib/services/commerce/printful" import { Printful } from "$lib/services/commerce/printful"
import { formatSlug, getProductImageUrls, type DeepPartial } from "./util";
export namespace Webflow { export namespace Webflow {
export const API_SITES_URL = `https://api.webflow.com/v2/sites/${Bun.env.WEBFLOW_SITE_ID}`; const SITE_ID = typeof Bun === "undefined" ? "" : Bun.env.WEBFLOW_SITE_ID;
export const API_COLLECTIONS_URL = `https://api.webflow.com/v2/collections/${Bun.env.WEBFLOW_COLLECTION_ID}`; const COLLECTIONS_ID = typeof Bun === "undefined" ? "" : Bun.env.WEBFLOW_COLLECTION_ID;
const AUTH_TOKEN = typeof Bun === "undefined" ? "" : Bun.env.WEBFLOW_AUTH;
const authHeader = { "Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}` }; export const API_SITES_URL = `https://api.webflow.com/v2/sites/${SITE_ID}`;
export const API_COLLECTIONS_URL = `https://api.webflow.com/v2/collections/${COLLECTIONS_ID}`;
const formatSlug = (name: string): string => { const AUTH_HEADER = { "Authorization": `bearer ${AUTH_TOKEN}` };
return name
.toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
};
export namespace Products { export namespace Products {
export interface Product { export namespace Skus {
id: string, export type Sku = {
fieldData: { id: string,
name: string,
slug: string,
description: string
},
skus: Sku[]
}
export interface Sku {
id?: string,
fieldData: {
name: string;
slug: string;
"sku-values"?: Record<string, string>,
price: {
value: number;
unit: string;
currency: string;
};
"main-image"?: string;
}
}
export interface UpsertProduct {
product: {
fieldData: { fieldData: {
name: string; name: string;
slug: string; slug: string;
description: string; "sku-values"?: Record<string, string>,
shippable?: boolean; price: {
"tax-category"?: string; value: number;
"sku-properties": { unit: string;
id: string; currency: string;
name: string; };
enum: { "main-image"?: string;
id: string; "more-images"?:
name: string; {
slug: string; fileId?: string,
}[]; url: string,
}[]; alt?: string,
}[]
} }
};
sku: Sku;
publishStatus?: "staging" | "live";
}
export async function getAll(): Promise<Product[]> {
let res = await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "GET",
headers: {
...authHeader,
}
});
const resObj = await res.json();
return resObj.items as Product[];
}
export async function remove(webflowProductId: string) {
await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({})
});
}
export async function exists(webflowProductId: string): Promise<boolean> {
let findProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
}
});
return findProductResponse.status === 200;
}
export async function updateUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = printfulProduct.sync_product.external_id;
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
} }
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color))); export async function create(webflowProductId: string, skus: DeepPartial<Sku>[]) {
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size))); const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
method: "POST",
let updateProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, { headers: {
method: "PATCH", "Content-Type": "application/json",
headers: { ...AUTH_HEADER
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
}, },
"sku": webflowVariants[0], body: JSON.stringify({
} as Products.UpsertProduct) "skus": skus
}); })
updateProductResponse = await updateProductResponse.json(); });
const getProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, { if (!res.ok) {
method: "GET", console.error("Webflow product SKU create failed:", res.statusText);
headers: { throw new Error("Failed to create Webflow product SKU");
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
} }
}); }
const webflowProduct = await getProductResponse.json();
for (let i = 0; i < webflowProduct["skus"].length; ++i) {
const webflowSkuId = printfulVariants[i].external_id;
export async function update(webflowProductId: string, webflowSkuId: string, webflowSku: DeepPartial<Sku>) {
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, { const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH", method: "PATCH",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}` ...AUTH_HEADER
}, },
body: JSON.stringify({ body: JSON.stringify({
"sku": webflowVariants[i] "sku": webflowSku
}) })
}); });
if (!res.ok) {
console.error("Webflow product SKU update failed:", await res.json());
throw new Error("Failed to update Webflow product SKU");
}
} }
} }
export async function createUsingPrintful(printfulProduct: Printful.Products.SyncProduct) { export type Product = {
const printfulVariants = printfulProduct.sync_variants; id: string,
fieldData: {
const webflowVariants: Products.Sku[] = []; name: string;
for (const printfulVariant of printfulVariants) { slug: string;
webflowVariants.push({ description?: string;
"fieldData": { shippable?: boolean;
"name": printfulVariant.name, "tax-category"?: string;
"slug": formatSlug(printfulVariant.name), "sku-properties": {
"sku-values": { id: string;
"color": printfulVariant.color, name: string;
"size": printfulVariant.size, enum: {
}, id: string;
"price": { name: string;
"value": +printfulVariant.retail_price * 100, slug: string;
"unit": printfulVariant.currency, }[];
"currency": printfulVariant.currency }[];
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
} }
}
export interface ProductAndSkus {
product: Product
skus: Skus.Sku[]
}
export interface ProductAndSku {
product: Product;
sku: Skus.Sku;
publishStatus?: "staging" | "live";
}
export async function create(webflowProductAndSku: DeepPartial<ProductAndSku>) {
const res = await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...AUTH_HEADER
},
body: JSON.stringify(webflowProductAndSku)
});
if (!res.ok) {
console.error("Webflow product create failed:", res.statusText);
console.log(await res.json());
throw new Error("Failed to create Webflow product");
}
const createdWebflowProduct = await res.json();
return createdWebflowProduct.product.id
}
export async function createUsingPrintful(printfulProduct: Printful.Products.Product): Promise<Webflow.Products.ProductAndSkus> {
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants = Printful.Products.convertVariantsToWebflowSkus(printfulVariants);
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color))); const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size))); const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// CREATE WEBFLOW PRODUCT const webflowProductId = await Webflow.Products.create({
let addProductResponse = await (await fetch(`${Webflow.API_SITES_URL}/products`, { "product": {
method: "POST", "fieldData": {
headers: { "name": printfulProduct.sync_product.name,
"Content-Type": "application/json", "slug": formatSlug(printfulProduct.sync_product.name),
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`, "shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
}, },
body: JSON.stringify({ "sku": webflowVariants[0],
"product": { });
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
},
"sku": webflowVariants[0],
} as Products.UpsertProduct)
}))?.json();
if (!addProductResponse?.product?.id) { // CREATE WEBFLOW PRODUCT SKUs
console.error("Webflow product creation failed:", addProductResponse); await Webflow.Products.Skus.create(webflowProductId, webflowVariants.slice(1));
const webflowProduct = await Webflow.Products.get(webflowProductId);
if (!webflowProduct) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product"); throw new Error("Failed to create Webflow product");
} }
const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.sync_product.id; const printfulProductId = printfulProduct.sync_product.id;
await Printful.Products.update(printfulProductId, {
"sync_product": {
"external_id": String(webflowProductId)
},
"sync_variants": webflowProduct.skus.map((sku, i) => ({
"id": Number(printfulVariants[i].id), // printful variant id
"external_id": String(sku.id), // webflow variant id
}))
});
// CREATE WEBFLOW PRODUCT SKUs return webflowProduct;
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, { }
method: "POST",
export async function getAll(): Promise<ProductAndSkus[]> {
const res = await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "GET",
headers: {
...AUTH_HEADER,
}
});
if (!res.ok) {
console.error("Webflow product get all failed:", res.statusText);
throw new Error("Failed to get all Webflow products");
}
const payload = await res.json();
return payload.items as ProductAndSkus[];
}
export async function get(webflowProductId: string): Promise<ProductAndSkus | undefined> {
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
...AUTH_HEADER,
}
});
if (res.status === 404 || res.status === 400) {
return;
}
if (!res.ok) {
console.error("Webflow product get failed:", res.statusText);
throw new Error("Failed to get Webflow product");
}
const payload = await res.json();
return payload as ProductAndSkus;
}
export async function update(webflowProductId: string, webflowProduct: DeepPartial<ProductAndSku>) {
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "PATCH",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}` ...AUTH_HEADER
}, },
body: JSON.stringify({ body: JSON.stringify(webflowProduct)
"skus": webflowVariants.slice(1)
})
}); });
const createSkuObj = await createSkuResponse.json();
const responseSkus = [addProductResponse["sku"], ...createSkuObj["skus"]]; if (!res.ok) {
console.error("Webflow product update failed:", res.statusText);
throw new Error("Failed to update Webflow product");
}
}
// SYNC WEBFLOW/PRINTFUL PRODUCT AND VARIANT IDs export async function updateUsingPrintful(printfulProduct: Printful.Products.Product) {
let modifyPrintfulProductResponse = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, { const webflowProductId = printfulProduct.sync_product.external_id;
method: "PUT",
const printfulVariants = printfulProduct.sync_variants;
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// Update product
Webflow.Products.update(webflowProductId, {
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
}
});
// Update SKUs
const webflowProduct = await Webflow.Products.get(webflowProductId);
if (!webflowProduct) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product");
}
for (let i = 0; i < webflowProduct.skus.length; ++i) {
const webflowSkuId = printfulVariants[i].external_id;
await Webflow.Products.Skus.update(webflowProductId, webflowSkuId, webflowProduct.skus[i]);
}
}
export async function remove(webflowProductId: string) {
const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, ...AUTH_HEADER
"X-PF-Store-Id": Bun.env.PRINTFUL_STORE_ID ?? ""
}, },
body: JSON.stringify({ body: JSON.stringify({})
"sync_product": {
"external_id": String(webflowProductId)
},
"sync_variants": responseSkus.map((sku, i) => ({
"id": Number(printfulVariants[i].id), // printful variant id
"external_id": String(sku.id), // webflow variant id
}))
})
}); });
modifyPrintfulProductResponse = await modifyPrintfulProductResponse.json();
if (!res.ok) {
console.error("Webflow product remove failed:", res.statusText);
throw new Error("Failed to remove Webflow product");
}
}
export async function syncImages(webflowProductAndSkus: ProductAndSkus) {
const productSlug = webflowProductAndSkus.product.fieldData.slug;
console.log(productSlug);
const productImageUrls = await getProductImageUrls(productSlug);
console.log(productImageUrls);
for (const sku of webflowProductAndSkus.skus) {
sku.fieldData["main-image"] = productImageUrls[0] ?? sku.fieldData["main-image"];
sku.fieldData["more-images"] = productImageUrls.slice(1).map(url => ({ url }));
console.log(sku.fieldData["more-images"]);
await Webflow.Products.Skus.update(webflowProductAndSkus.product.id, sku.id, sku);
}
} }
} }
} }
+50 -3
View File
@@ -1,6 +1,5 @@
import { Standards, type ActivityStandards } from "$lib/services/calculator/main";
import { Activity, ftToCm, Gender, getAvgWeight, inToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util"; import { Activity, ftToCm, Gender, getAvgWeight, inToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util";
import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" }; import { Webflow } from "$lib/services/commerce/webflow";
const player: Player = { const player: Player = {
metrics: { metrics: {
@@ -41,7 +40,55 @@ const computedPerformances: ActivityPerformance[] = [
}, },
]; ];
const standards = new Standards(allStandardsRaw as ActivityStandards, { maxLevel: 10 }); // const res = await fetch(`${Webflow.API_SITES_URL}/products/${}/skus/${"68d0154f97e90776a5680740"}`, {
// method: "PATCH",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
// },
// body: JSON.stringify({
// "sku": webflowVariants[i]
// })
// });
const productId = "68d080a129f538cd565bf56d";
const product = await Webflow.Products.get("68d080a129f538cd565bf56d");
const products = await Webflow.Products.getAll();
const skuId = "68d080a5d90e8263e52242e9";
// const sku = product.skus[0];
//
// sku.fieldData["more-images"] = [
// {
// "url": "https://website-media.bladeandbrawn.com/product-images/henry-iv-flag-black-10.png",
// },
// {
// "url": "https://website-media.bladeandbrawn.com/product-images/henry-iv-flag-black-20.png",
// },
// ];
// console.log("Fetching...");
// const res = await fetch(`${Webflow.API_SITES_URL}/products/${productId}/skus/${skuId}`, {
// method: "PATCH",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
// },
// body: JSON.stringify({
// "sku": sku
// })
// });
// console.log(res);
//
// const productAfter = (await Webflow.Products.getAll())[0];
//
// console.log(productAfter.skus[0].fieldData["more-images"]);
// const newStandards: Standards = { // const newStandards: Standards = {
// "Back Squat": [], // "Back Squat": [],
+3 -3
View File
@@ -19,9 +19,9 @@
<input id="sidebar" type="checkbox" class="drawer-toggle" /> <input id="sidebar" type="checkbox" class="drawer-toggle" />
<div class="drawer-content p-10 flex flex-col items-center"> <div class="drawer-content p-10 flex flex-col items-center">
{@render children?.()} {@render children?.()}
<label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> <!-- <label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> -->
Open drawer <!-- Open drawer -->
</label> <!-- </label> -->
</div> </div>
<div class="drawer-side"> <div class="drawer-side">
<label for="sidebar" aria-label="close sidebar" class="drawer-overlay" <label for="sidebar" aria-label="close sidebar" class="drawer-overlay"
+2 -21
View File
@@ -2,30 +2,11 @@ import { Printful } from '$lib/services/commerce/printful';
import { Webflow } from '$lib/services/commerce/webflow'; import { Webflow } from '$lib/services/commerce/webflow';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
const productsStore = {
cache: {
printful: undefined as Printful.Products.SyncProduct["sync_product"][] | undefined,
webflow: undefined as Webflow.Products.Product[] | undefined,
},
getPrintfulProducts: async () => {
if (!productsStore.cache.printful) {
productsStore.cache.printful = await Printful.Products.getAll()
}
return productsStore.cache.printful;
},
getWebflowProducts: async () => {
if (!productsStore.cache.webflow) {
productsStore.cache.webflow = await Webflow.Products.getAll()
}
return productsStore.cache.webflow;
}
};
export const load = async ({ }: Parameters<PageServerLoad>[0]) => { export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
return { return {
products: { products: {
printful: productsStore.getPrintfulProducts(), printful: Printful.Products.getAll(),
webflow: productsStore.getWebflowProducts(), webflow: Webflow.Products.getAll(),
} }
}; };
}; };
+3 -3
View File
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Printful } from "$lib/services/commerce/printful.js"; import { Printful } from "$lib/services/commerce/printful";
const { data } = $props(); const { data } = $props();
let isSyncing = $state(false); let isSyncing = $state(false);
const isPrintfulProductSynced = async ( const isPrintfulProductSynced = async (
printfulProduct: Printful.Products.SyncProduct["sync_product"], printfulProduct: Printful.Products.SyncProduct,
) => { ) => {
const webflowProducts = await data.products.webflow; const webflowProducts = await data.products.webflow;
return webflowProducts.some( return webflowProducts.some(
(p) => p.id === printfulProduct.external_id, (p) => p.product.id === printfulProduct.external_id,
); );
}; };