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
+44 -22
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,45 +27,62 @@ 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 p of printfulProducts) { for (const printfulProduct of printfulProducts) {
const exists = webflowProducts.some(wp => wp.id === p.external_id); await sleep(2000);
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);
if (webflowProduct)
await Webflow.Products.updateUsingPrintful(fullPrintfulProduct); await Webflow.Products.updateUsingPrintful(fullPrintfulProduct);
else else
await Webflow.Products.createUsingPrintful(fullPrintfulProduct); 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 prod = await Printful.Products.get(payload.data.sync_product.id); const printfulProduct = await Printful.Products.get(payload.data.sync_product.id);
const exists = await Webflow.Products.exists(prod.sync_product.external_id);
if (exists) await Webflow.Products.updateUsingPrintful(prod); let webflowProduct = await Webflow.Products.get(printfulProduct.sync_product.external_id);
else await Webflow.Products.createUsingPrintful(prod); if (webflowProduct)
await Webflow.Products.updateUsingPrintful(printfulProduct);
else
webflowProduct = await Webflow.Products.createUsingPrintful(printfulProduct);
await Webflow.Products.syncImages(webflowProduct);
break; break;
} }
case Printful.Webhook.Event.ProductDeleted: { case Printful.Webhook.Event.ProductDeleted: {
@@ -71,12 +90,15 @@ export const app = new Elysia({ prefix: "/api" })
break; 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: {
+87 -32
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,8 +97,12 @@ export namespace Printful {
stitch_count_tier: string; stitch_count_tier: string;
} }
export interface SyncProduct { export type Product = {
sync_product: { sync_product: SyncProduct,
sync_variants: SyncVariant[]
}
export type SyncProduct = {
id: number; id: number;
external_id: string; external_id: string;
name: string; name: string;
@@ -96,8 +110,9 @@ export namespace Printful {
synced: number; synced: number;
thumbnail_url: string; thumbnail_url: string;
is_ignored: boolean; is_ignored: boolean;
}, }
sync_variants: {
export type SyncVariant = {
id: number; id: number;
external_id: string; external_id: string;
sync_product_id: number; sync_product_id: number;
@@ -122,37 +137,23 @@ export namespace Printful {
size: string; size: string;
color: string; color: string;
availability_status: 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: { headers: { ...AUTH_HEADERS }
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
}) })
).json();
if (!res.ok) {
console.log(res.statusText);
}
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) {
console.error("Printful product update failed:", res.statusText);
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"); const previewFile = syncVariant.files.find(f => f.type === "preview");
return previewFile?.preview_url ?? syncVariant.product.image; 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";
+261 -240
View File
@@ -1,34 +1,20 @@
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 {
export type Sku = {
id: string, id: string,
fieldData: {
name: string,
slug: string,
description: string
},
skus: Sku[]
}
export interface Sku {
id?: string,
fieldData: { fieldData: {
name: string; name: string;
slug: string; slug: string;
@@ -39,15 +25,57 @@ export namespace Webflow {
currency: string; currency: string;
}; };
"main-image"?: string; "main-image"?: string;
"more-images"?:
{
fileId?: string,
url: string,
alt?: string,
}[]
} }
} }
export interface UpsertProduct { export async function create(webflowProductId: string, skus: DeepPartial<Sku>[]) {
product: { const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...AUTH_HEADER
},
body: JSON.stringify({
"skus": skus
})
});
if (!res.ok) {
console.error("Webflow product SKU create failed:", res.statusText);
throw new Error("Failed to create Webflow product SKU");
}
}
export async function update(webflowProductId: string, webflowSkuId: string, webflowSku: DeepPartial<Sku>) {
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...AUTH_HEADER
},
body: JSON.stringify({
"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 type Product = {
id: string,
fieldData: { fieldData: {
name: string; name: string;
slug: string; slug: string;
description: string; description?: string;
shippable?: boolean; shippable?: boolean;
"tax-category"?: string; "tax-category"?: string;
"sku-properties": { "sku-properties": {
@@ -60,242 +88,235 @@ export namespace Webflow {
}[]; }[];
}[]; }[];
} }
}; }
sku: Sku;
export interface ProductAndSkus {
product: Product
skus: Skus.Sku[]
}
export interface ProductAndSku {
product: Product;
sku: Skus.Sku;
publishStatus?: "staging" | "live"; publishStatus?: "staging" | "live";
} }
export async function getAll(): Promise<Product[]> { export async function create(webflowProductAndSku: DeepPartial<ProductAndSku>) {
let res = await fetch(`${Webflow.API_SITES_URL}/products`, { const 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)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
let updateProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "PATCH",
headers: {
"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],
} as Products.UpsertProduct)
});
updateProductResponse = await updateProductResponse.json();
const getProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"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;
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"sku": webflowVariants[i]
})
});
}
}
export async function createUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
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)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// CREATE WEBFLOW PRODUCT
let addProductResponse = await (await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`, ...AUTH_HEADER
}, },
body: JSON.stringify({ body: JSON.stringify(webflowProductAndSku)
"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) { if (!res.ok) {
console.error("Webflow product creation failed:", addProductResponse); console.error("Webflow product create failed:", res.statusText);
console.log(await res.json());
throw new Error("Failed to create Webflow product"); throw new Error("Failed to create Webflow product");
} }
const webflowProductId = addProductResponse["product"]["id"]; const createdWebflowProduct = await res.json();
const printfulProductId = printfulProduct.sync_product.id; 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 foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
const webflowProductId = await Webflow.Products.create({
"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],
});
// CREATE WEBFLOW PRODUCT SKUs // CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, { await Webflow.Products.Skus.create(webflowProductId, webflowVariants.slice(1));
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"skus": webflowVariants.slice(1)
})
});
const createSkuObj = await createSkuResponse.json();
const responseSkus = [addProductResponse["sku"], ...createSkuObj["skus"]]; const webflowProduct = await Webflow.Products.get(webflowProductId);
if (!webflowProduct) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product");
}
// SYNC WEBFLOW/PRINTFUL PRODUCT AND VARIANT IDs const printfulProductId = printfulProduct.sync_product.id;
let modifyPrintfulProductResponse = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, { await Printful.Products.update(printfulProductId, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": Bun.env.PRINTFUL_STORE_ID ?? ""
},
body: JSON.stringify({
"sync_product": { "sync_product": {
"external_id": String(webflowProductId) "external_id": String(webflowProductId)
}, },
"sync_variants": responseSkus.map((sku, i) => ({ "sync_variants": webflowProduct.skus.map((sku, i) => ({
"id": Number(printfulVariants[i].id), // printful variant id "id": Number(printfulVariants[i].id), // printful variant id
"external_id": String(sku.id), // webflow variant id "external_id": String(sku.id), // webflow variant id
})) }))
})
}); });
modifyPrintfulProductResponse = await modifyPrintfulProductResponse.json();
return webflowProduct;
}
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: {
"Content-Type": "application/json",
...AUTH_HEADER
},
body: JSON.stringify(webflowProduct)
});
if (!res.ok) {
console.error("Webflow product update failed:", res.statusText);
throw new Error("Failed to update Webflow product");
}
}
export async function updateUsingPrintful(printfulProduct: Printful.Products.Product) {
const webflowProductId = printfulProduct.sync_product.external_id;
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: {
"Content-Type": "application/json",
...AUTH_HEADER
},
body: JSON.stringify({})
});
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,
); );
}; };