latest
This commit is contained in:
+55
-33
@@ -7,6 +7,8 @@ import { Webflow } from "$lib/services/commerce/webflow";
|
||||
import { Printful } from "$lib/services/commerce/printful";
|
||||
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" })
|
||||
.use(cors({
|
||||
origin: '*'
|
||||
@@ -25,58 +27,78 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
const { player, activityPerformances } = body as CalcRequest;
|
||||
const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
|
||||
return { levels: levelCalculator.calculate(player, activityPerformances) };
|
||||
}, {
|
||||
error({ error }) {
|
||||
console.error(error);
|
||||
return {
|
||||
message: "Failed to calculate",
|
||||
error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// COMMERCE
|
||||
|
||||
.post("/products/sync", async ({ body, set }) => {
|
||||
console.log("Syncing...");
|
||||
try {
|
||||
const printfulProducts = await Printful.Products.getAll();
|
||||
const webflowProducts = await Webflow.Products.getAll();
|
||||
for (const p of printfulProducts) {
|
||||
const exists = webflowProducts.some(wp => wp.id === p.external_id);
|
||||
const printfulProducts = await Printful.Products.getAll();
|
||||
const webflowProducts = await Webflow.Products.getAll();
|
||||
for (const printfulProduct of printfulProducts) {
|
||||
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);
|
||||
if (exists)
|
||||
await Webflow.Products.updateUsingPrintful(fullPrintfulProduct);
|
||||
else
|
||||
await Webflow.Products.createUsingPrintful(fullPrintfulProduct);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
set.status = 500;
|
||||
return { error: "internal_error" };
|
||||
let webflowProduct = webflowProducts
|
||||
.find(webflowProduct => webflowProduct.product.id === printfulProduct.external_id);
|
||||
if (webflowProduct)
|
||||
await Webflow.Products.updateUsingPrintful(fullPrintfulProduct);
|
||||
else
|
||||
webflowProduct = await Webflow.Products.createUsingPrintful(fullPrintfulProduct);
|
||||
await Webflow.Products.syncImages(webflowProduct);
|
||||
}
|
||||
console.log("Done");
|
||||
return "";
|
||||
}, {
|
||||
error({ error }) {
|
||||
console.error(error);
|
||||
return {
|
||||
message: "Failed to sync products",
|
||||
error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// WEBHOOKS
|
||||
|
||||
.post("/webhook/printful", async ({ body, set }) => {
|
||||
const payload = body as Printful.Webhook.EventPayload;
|
||||
try {
|
||||
switch (payload.type) {
|
||||
case Printful.Webhook.Event.ProductUpdated: {
|
||||
const prod = 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);
|
||||
else await Webflow.Products.createUsingPrintful(prod);
|
||||
break;
|
||||
}
|
||||
case Printful.Webhook.Event.ProductDeleted: {
|
||||
await Webflow.Products.remove(payload.data.sync_product.external_id);
|
||||
break;
|
||||
}
|
||||
switch (payload.type) {
|
||||
case Printful.Webhook.Event.ProductUpdated: {
|
||||
const printfulProduct = await Printful.Products.get(payload.data.sync_product.id);
|
||||
|
||||
let webflowProduct = await Webflow.Products.get(printfulProduct.sync_product.external_id);
|
||||
if (webflowProduct)
|
||||
await Webflow.Products.updateUsingPrintful(printfulProduct);
|
||||
else
|
||||
webflowProduct = await Webflow.Products.createUsingPrintful(printfulProduct);
|
||||
await Webflow.Products.syncImages(webflowProduct);
|
||||
|
||||
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 "";
|
||||
}, {
|
||||
error({ error }) {
|
||||
console.error(error);
|
||||
return {
|
||||
message: "Failed to execute webhook",
|
||||
error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type App = typeof app;
|
||||
|
||||
@@ -102,6 +102,7 @@ export const range = (length: number) => Array.from({ length: length }, (_, i) =
|
||||
// return interpolatedAvg;
|
||||
// }
|
||||
|
||||
|
||||
export const getAvgWeight = (gender: Gender, age: number) => {
|
||||
type AvgWeightData = {
|
||||
metadata: {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { formatSlug, type DeepPartial } from "./util";
|
||||
import type { Webflow } from "./webflow";
|
||||
|
||||
export namespace Printful {
|
||||
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 {
|
||||
// meta
|
||||
@@ -61,12 +71,12 @@ export namespace Printful {
|
||||
}
|
||||
}
|
||||
|
||||
interface Option {
|
||||
type Option = {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface File {
|
||||
type File = {
|
||||
type: string;
|
||||
id: number;
|
||||
url: string;
|
||||
@@ -87,72 +97,63 @@ export namespace Printful {
|
||||
stitch_count_tier: string;
|
||||
}
|
||||
|
||||
export interface SyncProduct {
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: boolean;
|
||||
},
|
||||
sync_variants: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
sync_product_id: number;
|
||||
name: string;
|
||||
synced: boolean;
|
||||
export type Product = {
|
||||
sync_product: SyncProduct,
|
||||
sync_variants: SyncVariant[]
|
||||
}
|
||||
|
||||
export type SyncProduct = {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: 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;
|
||||
retail_price: string;
|
||||
currency: string;
|
||||
is_ignored: boolean;
|
||||
sku: string;
|
||||
product: {
|
||||
variant_id: number;
|
||||
product_id: number;
|
||||
image: string;
|
||||
name: string;
|
||||
};
|
||||
files: File[];
|
||||
options: Option[];
|
||||
main_category_id: number;
|
||||
warehouse_product_id: number;
|
||||
warehouse_product_variant_id: number;
|
||||
size: string;
|
||||
color: string;
|
||||
availability_status: string;
|
||||
}[]
|
||||
product_id: number;
|
||||
image: string;
|
||||
name: string;
|
||||
};
|
||||
files: File[];
|
||||
options: Option[];
|
||||
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> {
|
||||
const payload: MetaDataSingle<SyncProduct> = await (
|
||||
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"][] = [];
|
||||
export async function getAll(offset: number = 0): Promise<SyncProduct[]> {
|
||||
const allSyncProducts: SyncProduct[] = [];
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const payload: MetaDataMulti<SyncProduct["sync_product"]> = await (
|
||||
await fetch(`${Printful.API_URL}/store/products?offset=${offset}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
|
||||
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
|
||||
}
|
||||
})
|
||||
).json();
|
||||
const res = await fetch(`${Printful.API_URL}/store/products?offset=${offset}`, {
|
||||
method: "GET",
|
||||
headers: { ...AUTH_HEADERS }
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
console.log(res.statusText);
|
||||
}
|
||||
|
||||
const payload: MetaDataMulti<SyncProduct> = await res.json();
|
||||
|
||||
allSyncProducts.push(...payload.result);
|
||||
offset = allSyncProducts.length;
|
||||
@@ -160,17 +161,71 @@ export namespace Printful {
|
||||
if (allSyncProducts.length >= payload.paging.total)
|
||||
break;
|
||||
} catch (error) {
|
||||
break;
|
||||
console.error("Printful product get all failed:", error);
|
||||
throw new Error("Failed to get all Printful products",);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const previewFile = syncVariant.files.find(f => f.type === "preview");
|
||||
return previewFile?.preview_url ?? syncVariant.product.image;
|
||||
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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -1,301 +1,322 @@
|
||||
import { Printful } from "$lib/services/commerce/printful"
|
||||
import { formatSlug, getProductImageUrls, type DeepPartial } from "./util";
|
||||
|
||||
export namespace Webflow {
|
||||
export const API_SITES_URL = `https://api.webflow.com/v2/sites/${Bun.env.WEBFLOW_SITE_ID}`;
|
||||
export const API_COLLECTIONS_URL = `https://api.webflow.com/v2/collections/${Bun.env.WEBFLOW_COLLECTION_ID}`;
|
||||
const SITE_ID = typeof Bun === "undefined" ? "" : Bun.env.WEBFLOW_SITE_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 => {
|
||||
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
|
||||
};
|
||||
const AUTH_HEADER = { "Authorization": `bearer ${AUTH_TOKEN}` };
|
||||
|
||||
export namespace Products {
|
||||
export interface Product {
|
||||
id: string,
|
||||
fieldData: {
|
||||
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: {
|
||||
export namespace Skus {
|
||||
export type Sku = {
|
||||
id: string,
|
||||
fieldData: {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
shippable?: boolean;
|
||||
"tax-category"?: string;
|
||||
"sku-properties": {
|
||||
id: string;
|
||||
name: string;
|
||||
enum: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}[];
|
||||
}[];
|
||||
"sku-values"?: Record<string, string>,
|
||||
price: {
|
||||
value: number;
|
||||
unit: string;
|
||||
currency: string;
|
||||
};
|
||||
"main-image"?: string;
|
||||
"more-images"?:
|
||||
{
|
||||
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)));
|
||||
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
|
||||
}))
|
||||
},
|
||||
]
|
||||
}
|
||||
export async function create(webflowProductId: string, skus: DeepPartial<Sku>[]) {
|
||||
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...AUTH_HEADER
|
||||
},
|
||||
"sku": webflowVariants[0],
|
||||
} as Products.UpsertProduct)
|
||||
});
|
||||
updateProductResponse = await updateProductResponse.json();
|
||||
body: JSON.stringify({
|
||||
"skus": skus
|
||||
})
|
||||
});
|
||||
|
||||
const getProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
|
||||
if (!res.ok) {
|
||||
console.error("Webflow product SKU create failed:", res.statusText);
|
||||
throw new Error("Failed to create Webflow product SKU");
|
||||
}
|
||||
});
|
||||
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}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
|
||||
...AUTH_HEADER
|
||||
},
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
});
|
||||
export type Product = {
|
||||
id: string,
|
||||
fieldData: {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
shippable?: boolean;
|
||||
"tax-category"?: string;
|
||||
"sku-properties": {
|
||||
id: string;
|
||||
name: string;
|
||||
enum: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
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 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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
|
||||
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
|
||||
}))
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
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)
|
||||
}))?.json();
|
||||
"sku": webflowVariants[0],
|
||||
});
|
||||
|
||||
if (!addProductResponse?.product?.id) {
|
||||
console.error("Webflow product creation failed:", addProductResponse);
|
||||
// CREATE WEBFLOW PRODUCT SKUs
|
||||
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");
|
||||
}
|
||||
|
||||
const webflowProductId = addProductResponse["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
|
||||
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
|
||||
method: "POST",
|
||||
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",
|
||||
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
|
||||
...AUTH_HEADER
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"skus": webflowVariants.slice(1)
|
||||
})
|
||||
body: JSON.stringify(webflowProduct)
|
||||
});
|
||||
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
|
||||
let modifyPrintfulProductResponse = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, {
|
||||
method: "PUT",
|
||||
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",
|
||||
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
|
||||
"X-PF-Store-Id": Bun.env.PRINTFUL_STORE_ID ?? ""
|
||||
...AUTH_HEADER
|
||||
},
|
||||
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
|
||||
}))
|
||||
})
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 allStandardsRaw from "$lib/data/standards.json" assert { type: "json" };
|
||||
import { Webflow } from "$lib/services/commerce/webflow";
|
||||
|
||||
const player: Player = {
|
||||
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 = {
|
||||
// "Back Squat": [],
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<input id="sidebar" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content p-10 flex flex-col items-center">
|
||||
{@render children?.()}
|
||||
<label for="sidebar" class="btn btn-primary drawer-button lg:hidden">
|
||||
Open drawer
|
||||
</label>
|
||||
<!-- <label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> -->
|
||||
<!-- Open drawer -->
|
||||
<!-- </label> -->
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label for="sidebar" aria-label="close sidebar" class="drawer-overlay"
|
||||
|
||||
@@ -2,30 +2,11 @@ import { Printful } from '$lib/services/commerce/printful';
|
||||
import { Webflow } from '$lib/services/commerce/webflow';
|
||||
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]) => {
|
||||
return {
|
||||
products: {
|
||||
printful: productsStore.getPrintfulProducts(),
|
||||
webflow: productsStore.getWebflowProducts(),
|
||||
printful: Printful.Products.getAll(),
|
||||
webflow: Webflow.Products.getAll(),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Printful } from "$lib/services/commerce/printful.js";
|
||||
import { Printful } from "$lib/services/commerce/printful";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
let isSyncing = $state(false);
|
||||
|
||||
const isPrintfulProductSynced = async (
|
||||
printfulProduct: Printful.Products.SyncProduct["sync_product"],
|
||||
printfulProduct: Printful.Products.SyncProduct,
|
||||
) => {
|
||||
const webflowProducts = await data.products.webflow;
|
||||
return webflowProducts.some(
|
||||
(p) => p.id === printfulProduct.external_id,
|
||||
(p) => p.product.id === printfulProduct.external_id,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user