Some refactoring

This commit is contained in:
Dominic Ferrando
2025-09-24 21:12:38 -04:00
parent 03625732fd
commit 7f0a51d34c
10 changed files with 506 additions and 478 deletions
+20 -66
View File
@@ -3,20 +3,11 @@ import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main";
import type { ActivityPerformance, Player } from "$lib/services/calculator/util";
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));
const syncProduct = async (printfulProduct: Printful.Products.Product) => {
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);
};
import { Printful } from "./services/commerce/util/types";
import WebflowService from "./services/commerce/webflow";
import PrintfulService from "./services/commerce/printful";
import SyncService from "./services/commerce/sync";
export const app = new Elysia({ prefix: "/api" })
.use(cors({
@@ -27,7 +18,7 @@ export const app = new Elysia({ prefix: "/api" })
// CALCULATOR
.post("/calculate", async ({ body, store }) => {
.post("/calculate", async ({ body }) => {
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
interface CalcRequest {
player: Player;
@@ -49,55 +40,19 @@ export const app = new Elysia({ prefix: "/api" })
// COMMERCE
.group("/products", app => app
.group("/sync", app => app
.state("job", {
isSyncing: false,
printfulProductId: undefined as undefined | number
})
.get("/", async ({ store }) => {
return store.job;
})
.post("/:printfulProductId?", async ({ params, store }) => {
if (store.job.isSyncing) return "";
store.job.isSyncing = true;
try {
if (params.printfulProductId) {
const printfulProductId = +params.printfulProductId;
.get("/", async ({ }) => SyncService.state)
.post("/:printfulProductId?", async ({ params }) => {
SyncService.state.isSyncing = true;
SyncService.state.printfulProductId = params.printfulProductId ?
+params.printfulProductId :
undefined;
store.job.printfulProductId = printfulProductId;
const printfulProducts = params.printfulProductId ?
[(await PrintfulService.Products.get(+params.printfulProductId)).sync_product] :
await PrintfulService.Products.getAll();
const printfulProduct = await Printful.Products.get(printfulProductId);
await syncProduct(printfulProduct);
}
else {
console.log("Syncing all products...");
const printfulProducts = await Printful.Products.getAll();
const webflowProducts = await Webflow.Products.getAll();
for (const printfulProduct of printfulProducts) {
store.job.printfulProductId = printfulProduct.id;
await sleep(2000);
const fullPrintfulProduct = await Printful.Products.get(printfulProduct.id);
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 "";
}
catch (error) {
throw error;
}
finally {
store.job.isSyncing = false;
store.job.printfulProductId = undefined;
}
await SyncService.sync(printfulProducts);
return "";
}, {
error({ error }) {
console.error(error);
@@ -111,17 +66,16 @@ export const app = new Elysia({ prefix: "/api" })
)
// WEBHOOKS
.post("/webhook/printful", async ({ body, set }) => {
.post("/webhook/printful", async ({ body }) => {
const payload = body as Printful.Webhook.EventPayload;
switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated: {
const printfulProduct = await Printful.Products.get(payload.data.sync_product.id);
await syncProduct(printfulProduct);
const printfulProduct = payload.data.sync_product;
await SyncService.sync([printfulProduct]);
break;
}
case Printful.Webhook.Event.ProductDeleted: {
await Webflow.Products.remove(payload.data.sync_product.external_id);
await WebflowService.Products.remove(payload.data.sync_product.external_id);
break;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import avgWeightDataRaw from "$lib/data/avg-weights.json"
// DATA MODELS
// -------------------------------------------------------------------------------------------------
export interface Player {
export type Player = {
metrics: Metrics;
}
+28 -183
View File
@@ -1,137 +1,10 @@
import { formatSlug, type DeepPartial } from "./util";
import type { Webflow } from "./webflow";
import type { Printful } from "./util/types";
import { type DeepPartial } from "./util/misc";
export namespace Printful {
export namespace Webhook {
// meta
interface MetaData {
created: number;
retries: number;
store: number;
}
export enum Event {
ProductUpdated = "product_updated",
ProductDeleted = "product_deleted"
}
// product updated
export interface ProductUpdated extends MetaData {
type: Event.ProductUpdated,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
variants: number;
synced: number;
thumbnail_url: string;
is_ignored: boolean;
};
}
}
// product deleted
export interface ProductDeleted extends MetaData {
type: Event.ProductDeleted,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
};
}
}
export type EventPayload = ProductUpdated | ProductDeleted
}
export namespace Products {
export interface MetaDataSingle<T = any> {
code: number;
result: T;
}
export interface MetaDataMulti<T = any> {
code: number;
result: T[];
paging: {
total: number,
offset: number,
limit: number
}
}
type Option = {
id: string;
value: string;
}
type File = {
type: string;
id: number;
url: string;
options: Option[];
hash: string;
filename: string;
mime_type: string;
size: number;
width: number;
height: number;
dpi: number;
status: string;
created: number;
thumbnail_url: string;
preview_url: string;
visible: boolean;
is_temporary: boolean;
stitch_count_tier: string;
}
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;
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 getAll(offset: number = 0): Promise<SyncProduct[]> {
const allSyncProducts: SyncProduct[] = [];
export default class PrintfulService {
static Products = class {
static async getAll(offset: number = 0): Promise<Printful.Products.SyncProduct[]> {
const allSyncProducts: Printful.Products.SyncProduct[] = [];
while (true) {
try {
@@ -145,7 +18,7 @@ export namespace Printful {
throw new Error("Failed to get all Printful products",);
}
const payload: MetaDataMulti<SyncProduct> = await res.json();
const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> = await res.json();
allSyncProducts.push(...payload.result);
offset = allSyncProducts.length;
@@ -161,7 +34,7 @@ export namespace Printful {
return allSyncProducts;
}
export async function get(printfulProductId: number): Promise<Product> {
static async get(printfulProductId: number): Promise<Printful.Products.Product> {
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, {
method: "GET",
headers: { ...env().AUTH_HEADERS }
@@ -172,11 +45,11 @@ export namespace Printful {
throw new Error("Failed to update Printful product");
}
const payload: MetaDataSingle<Product> = await res.json();
const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> = await res.json();
return payload.result;
}
export async function update(printfulProductId: number, printfulProduct: DeepPartial<Product>) {
static async update(printfulProductId: number, printfulProduct: DeepPartial<Printful.Products.Product>) {
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, {
method: "PUT",
headers: {
@@ -191,53 +64,25 @@ export namespace Printful {
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;
}
export const env = () => {
if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun.");
}
export const env = () => {
if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun.");
}
const vars = {
API_URL: "https://api.printful.com",
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
STORE_ID: Bun.env.PRINTFUL_STORE_ID
};
return {
...vars,
AUTH_HEADERS: {
"Authorization": `Bearer ${vars.AUTH_TOKEN}`,
"X-PF-Store-Id": `${vars.STORE_ID}`
}
};
const vars = {
API_URL: "https://api.printful.com",
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
STORE_ID: Bun.env.PRINTFUL_STORE_ID
};
}
return {
...vars,
AUTH_HEADERS: {
"Authorization": `Bearer ${vars.AUTH_TOKEN}`,
"X-PF-Store-Id": `${vars.STORE_ID}`
}
};
};
+229
View File
@@ -0,0 +1,229 @@
import type { Printful, Webflow } from "./util/types";
import { formatSlug, type DeepPartial } from "./util/misc";
import WebflowService from "./webflow";
import PrintfulService from "./printful";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
export default class SyncService {
static state = {
isSyncing: false,
printfulProductId: undefined as undefined | number
}
static async sync(printfulProducts: Printful.Products.SyncProduct[]) {
this.state.isSyncing = true;
try {
const webflowProducts = await WebflowService.Products.getAll();
const mainPrintfulProducts: Printful.Products.SyncProduct[] = [];
for (const printfulProduct of printfulProducts) {
if (this.isSpecialPrintfulVariant(printfulProduct)) {
const mainProductName = this.getMainProductName(printfulProduct.name);
const mainProductAlreadyInserted = mainPrintfulProducts
.some(p => p.name.includes(mainProductName));
if (!mainProductAlreadyInserted) {
mainPrintfulProducts.push(printfulProduct);
}
}
else {
mainPrintfulProducts.push(printfulProduct);
}
}
for (const printfulProduct of mainPrintfulProducts) {
this.state.isSyncing = true;
this.state.printfulProductId = printfulProduct.id;
await sleep(2000);
const fullPrintfulProduct = await PrintfulService.Products.get(printfulProduct.id);
const printfulVariants = fullPrintfulProduct.sync_variants;
const webflowVariants = this.convertPrintfulVariantsToWebflowSkus(printfulVariants);
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// SYNC PRODUCT DATA
let webflowProduct = webflowProducts
.find(webflowProduct => webflowProduct.product.id === printfulProduct.external_id);
if (webflowProduct) {
// SYNC PRODUCT UPDATE
const webflowProductId = fullPrintfulProduct.sync_product.external_id;
WebflowService.Products.update(webflowProductId, {
"product": {
"fieldData": {
"name": fullPrintfulProduct.sync_product.name,
"slug": formatSlug(fullPrintfulProduct.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 webflow product SKUs
const webflowProduct = await WebflowService.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 WebflowService.Products.Skus.update(webflowProductId, webflowSkuId, webflowProduct.skus[i]);
}
}
else {
// SYNC PRODUCT CREATE
const webflowProductId = await WebflowService.Products.create({
"product": {
"fieldData": {
"name": fullPrintfulProduct.sync_product.name,
"slug": formatSlug(fullPrintfulProduct.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
await WebflowService.Products.Skus.create(webflowProductId, webflowVariants.slice(1));
webflowProduct = await WebflowService.Products.get(webflowProductId);
if (!webflowProduct) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product");
}
await PrintfulService.Products.update(printfulProduct.id, {
"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
}))
});
}
// SYNC IMAGES
const productSlug = webflowProduct.product.fieldData.slug;
const productImageUrls = await this.getProductImageUrls(productSlug);
for (const sku of webflowProduct.skus) {
sku.fieldData["main-image"] = productImageUrls[0] ?? sku.fieldData["main-image"];
sku.fieldData["more-images"] = productImageUrls.slice(1).map(url => ({ url }));
await WebflowService.Products.Skus.update(webflowProduct.product.id, sku.id, sku);
}
}
}
catch (error) {
throw error;
}
finally {
this.state.isSyncing = false;
this.state.printfulProductId = undefined;
}
}
private static convertPrintfulVariantsToWebflowSkus(printfulVariants: Printful.Products.SyncVariant[]) {
const getVariantMainImage = (syncVariant: Printful.Products.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;
}
private static 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}`);
}
private static findColorInProductName = (productName: string): string | undefined => {
const m = productName.match(/\[([^\]]+)\]$/);
return m ? m[1] : undefined;
};
private static isSpecialPrintfulVariant = (printfulProduct: Printful.Products.SyncProduct) => {
const colorInName = this.findColorInProductName(printfulProduct.name);
return !!colorInName;
}
private static getMainProductName = (productName: string) => {
const colorInName = this.findColorInProductName(productName);
if (colorInName) {
return productName.replace(colorInName, "").trimEnd();
}
else {
return productName;
}
};
}
@@ -11,16 +11,3 @@ export const formatSlug = (name: string): string => {
.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";
+188
View File
@@ -0,0 +1,188 @@
export namespace Printful {
export namespace Products {
export interface MetaDataSingle<T = any> {
code: number;
result: T;
}
export interface MetaDataMulti<T = any> {
code: number;
result: T[];
paging: {
total: number,
offset: number,
limit: number
}
}
type Option = {
id: string;
value: string;
}
type File = {
type: string;
id: number;
url: string;
options: Option[];
hash: string;
filename: string;
mime_type: string;
size: number;
width: number;
height: number;
dpi: number;
status: string;
created: number;
thumbnail_url: string;
preview_url: string;
visible: boolean;
is_temporary: boolean;
stitch_count_tier: string;
}
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;
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 namespace Webhook {
// meta
interface MetaData {
created: number;
retries: number;
store: number;
}
export enum Event {
ProductUpdated = "product_updated",
ProductDeleted = "product_deleted"
}
// product updated
export interface ProductUpdated extends MetaData {
type: Event.ProductUpdated,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
variants: number;
synced: number;
thumbnail_url: string;
is_ignored: boolean;
};
}
}
// product deleted
export interface ProductDeleted extends MetaData {
type: Event.ProductDeleted,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
};
}
}
export type EventPayload = ProductUpdated | ProductDeleted
}
}
export namespace Webflow {
export namespace Products {
export namespace Skus {
export type Sku = {
id: string,
fieldData: {
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,
}[]
}
}
}
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";
}
}
}
+33 -210
View File
@@ -1,31 +1,10 @@
import { Printful } from "$lib/services/commerce/printful"
import { formatSlug, getProductImageUrls, type DeepPartial } from "./util";
import type { Webflow } from "./util/types";
import { type DeepPartial } from "./util/misc";
export namespace Webflow {
export namespace Products {
export namespace Skus {
export type Sku = {
id: string,
fieldData: {
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,
}[]
}
}
export async function create(webflowProductId: string, skus: DeepPartial<Sku>[]) {
export default class WebflowService {
static Products = class {
static Skus = class {
static async create(webflowProductId: string, skus: DeepPartial<Webflow.Products.Skus.Sku>[]) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus`, {
method: "POST",
headers: {
@@ -43,7 +22,7 @@ export namespace Webflow {
}
}
export async function update(webflowProductId: string, webflowSkuId: string, webflowSku: DeepPartial<Sku>) {
static async update(webflowProductId: string, webflowSkuId: string, webflowSku: DeepPartial<Webflow.Products.Skus.Sku>) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH",
headers: {
@@ -61,38 +40,7 @@ export namespace Webflow {
}
}
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>) {
static async create(webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>) {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "POST",
headers: {
@@ -112,69 +60,7 @@ export namespace Webflow {
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
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 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
}))
});
return webflowProduct;
}
export async function getAll(): Promise<ProductAndSkus[]> {
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "GET",
headers: {
@@ -188,10 +74,10 @@ export namespace Webflow {
}
const payload = await res.json();
return payload.items as ProductAndSkus[];
return payload.items as Webflow.Products.ProductAndSkus[];
}
export async function get(webflowProductId: string): Promise<ProductAndSkus | undefined> {
static async get(webflowProductId: string): Promise<Webflow.Products.ProductAndSkus | undefined> {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
@@ -209,10 +95,10 @@ export namespace Webflow {
}
const payload = await res.json();
return payload as ProductAndSkus;
return payload as Webflow.Products.ProductAndSkus;
}
export async function update(webflowProductId: string, webflowProduct: DeepPartial<ProductAndSku>) {
static async update(webflowProductId: string, webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
method: "PATCH",
headers: {
@@ -228,60 +114,7 @@ export namespace Webflow {
}
}
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) {
static async remove(webflowProductId: string) {
const res = await fetch(`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
headers: {
@@ -296,35 +129,25 @@ export namespace Webflow {
throw new Error("Failed to remove Webflow product");
}
}
export async function syncImages(webflowProductAndSkus: ProductAndSkus) {
const productSlug = webflowProductAndSkus.product.fieldData.slug;
const productImageUrls = await getProductImageUrls(productSlug);
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 }));
await Webflow.Products.Skus.update(webflowProductAndSkus.product.id, sku.id, sku);
}
}
}
const env = () => {
if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun.");
}
const vars = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
};
return {
...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`,
API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`,
AUTH_HEADER: { "Authorization": `bearer ${vars.AUTH_TOKEN}` }
};
};
}
const env = () => {
if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun.");
}
const vars = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
};
return {
...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`,
API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`,
AUTH_HEADER: { "Authorization": `bearer ${vars.AUTH_TOKEN}` }
};
};
+2
View File
@@ -1,4 +1,5 @@
import { Activity, ftToCm, Gender, getAvgWeight, inToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util";
import { Printful } from "$lib/services/commerce/printful";
import { Webflow } from "$lib/services/commerce/webflow";
const player: Player = {
@@ -61,6 +62,7 @@ const products = await Webflow.Products.getAll();
const skuId = "68d080a5d90e8263e52242e9";
const colors = ["Black", "White", "Red"];
// const sku = product.skus[0];
//
+4 -4
View File
@@ -1,12 +1,12 @@
import { Printful } from '$lib/services/commerce/printful';
import { Webflow } from '$lib/services/commerce/webflow';
import PrintfulService from '$lib/services/commerce/printful';
import WebflowService from '$lib/services/commerce/webflow';
import type { PageServerLoad } from './$types';
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
return {
products: {
printful: Printful.Products.getAll(),
webflow: Webflow.Products.getAll(),
printful: PrintfulService.Products.getAll(),
webflow: WebflowService.Products.getAll(),
}
};
};
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { api } from "$lib/api.js";
import type { Printful } from "$lib/services/commerce/printful";
import type { Printful } from "$lib/services/commerce/util/types.js";
import { onMount } from "svelte";
const { data } = $props();