Some refactoring
This commit is contained in:
+20
-66
@@ -3,20 +3,11 @@ import { Elysia } from "elysia";
|
|||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main";
|
import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main";
|
||||||
import type { ActivityPerformance, Player } from "$lib/services/calculator/util";
|
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" }
|
import rawStandards from "$lib/data/standards.json" assert { type: "json" }
|
||||||
|
import { Printful } from "./services/commerce/util/types";
|
||||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
import WebflowService from "./services/commerce/webflow";
|
||||||
|
import PrintfulService from "./services/commerce/printful";
|
||||||
const syncProduct = async (printfulProduct: Printful.Products.Product) => {
|
import SyncService from "./services/commerce/sync";
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const app = new Elysia({ prefix: "/api" })
|
export const app = new Elysia({ prefix: "/api" })
|
||||||
.use(cors({
|
.use(cors({
|
||||||
@@ -27,7 +18,7 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
|
|
||||||
// CALCULATOR
|
// CALCULATOR
|
||||||
|
|
||||||
.post("/calculate", async ({ body, store }) => {
|
.post("/calculate", async ({ body }) => {
|
||||||
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
|
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
|
||||||
interface CalcRequest {
|
interface CalcRequest {
|
||||||
player: Player;
|
player: Player;
|
||||||
@@ -49,55 +40,19 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
// COMMERCE
|
// COMMERCE
|
||||||
.group("/products", app => app
|
.group("/products", app => app
|
||||||
.group("/sync", app => app
|
.group("/sync", app => app
|
||||||
.state("job", {
|
.get("/", async ({ }) => SyncService.state)
|
||||||
isSyncing: false,
|
.post("/:printfulProductId?", async ({ params }) => {
|
||||||
printfulProductId: undefined as undefined | number
|
SyncService.state.isSyncing = true;
|
||||||
})
|
SyncService.state.printfulProductId = params.printfulProductId ?
|
||||||
.get("/", async ({ store }) => {
|
+params.printfulProductId :
|
||||||
return store.job;
|
undefined;
|
||||||
})
|
|
||||||
.post("/:printfulProductId?", async ({ params, store }) => {
|
|
||||||
if (store.job.isSyncing) return "";
|
|
||||||
store.job.isSyncing = true;
|
|
||||||
try {
|
|
||||||
if (params.printfulProductId) {
|
|
||||||
const printfulProductId = +params.printfulProductId;
|
|
||||||
|
|
||||||
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 SyncService.sync(printfulProducts);
|
||||||
await syncProduct(printfulProduct);
|
return "";
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
error({ error }) {
|
error({ error }) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -111,17 +66,16 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
)
|
)
|
||||||
|
|
||||||
// WEBHOOKS
|
// WEBHOOKS
|
||||||
|
.post("/webhook/printful", async ({ body }) => {
|
||||||
.post("/webhook/printful", async ({ body, set }) => {
|
|
||||||
const payload = body as Printful.Webhook.EventPayload;
|
const payload = body as Printful.Webhook.EventPayload;
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case Printful.Webhook.Event.ProductUpdated: {
|
case Printful.Webhook.Event.ProductUpdated: {
|
||||||
const printfulProduct = await Printful.Products.get(payload.data.sync_product.id);
|
const printfulProduct = payload.data.sync_product;
|
||||||
await syncProduct(printfulProduct);
|
await SyncService.sync([printfulProduct]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Printful.Webhook.Event.ProductDeleted: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import avgWeightDataRaw from "$lib/data/avg-weights.json"
|
|||||||
// DATA MODELS
|
// DATA MODELS
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface Player {
|
export type Player = {
|
||||||
metrics: Metrics;
|
metrics: Metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,137 +1,10 @@
|
|||||||
import { formatSlug, type DeepPartial } from "./util";
|
import type { Printful } from "./util/types";
|
||||||
import type { Webflow } from "./webflow";
|
import { type DeepPartial } from "./util/misc";
|
||||||
|
|
||||||
export namespace Printful {
|
export default class PrintfulService {
|
||||||
export namespace Webhook {
|
static Products = class {
|
||||||
// meta
|
static async getAll(offset: number = 0): Promise<Printful.Products.SyncProduct[]> {
|
||||||
interface MetaData {
|
const allSyncProducts: Printful.Products.SyncProduct[] = [];
|
||||||
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[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@@ -145,7 +18,7 @@ export namespace Printful {
|
|||||||
throw new Error("Failed to get all Printful products",);
|
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);
|
allSyncProducts.push(...payload.result);
|
||||||
offset = allSyncProducts.length;
|
offset = allSyncProducts.length;
|
||||||
@@ -161,7 +34,7 @@ export namespace Printful {
|
|||||||
return allSyncProducts;
|
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}`, {
|
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { ...env().AUTH_HEADERS }
|
headers: { ...env().AUTH_HEADERS }
|
||||||
@@ -172,11 +45,11 @@ export namespace Printful {
|
|||||||
throw new Error("Failed to update Printful product");
|
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;
|
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}`, {
|
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -191,53 +64,25 @@ export namespace Printful {
|
|||||||
throw new Error("Failed to update Printful product");
|
throw new Error("Failed to update Printful product");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function convertVariantsToWebflowSkus(printfulVariants: SyncVariant[]) {
|
export const env = () => {
|
||||||
const getVariantMainImage = (syncVariant: SyncVariant): string => {
|
if (typeof Bun === "undefined") {
|
||||||
const previewFile = syncVariant.files.find(f => f.type === "preview");
|
throw new Error("Must be in a server context. Make sure to run using --bun.");
|
||||||
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 = () => {
|
const vars = {
|
||||||
if (typeof Bun === "undefined") {
|
API_URL: "https://api.printful.com",
|
||||||
throw new Error("Must be in a server context. Make sure to run using --bun.");
|
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
|
||||||
}
|
STORE_ID: Bun.env.PRINTFUL_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}`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
...vars,
|
||||||
|
AUTH_HEADERS: {
|
||||||
|
"Authorization": `Bearer ${vars.AUTH_TOKEN}`,
|
||||||
|
"X-PF-Store-Id": `${vars.STORE_ID}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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(/--+/g, '-') // collapse multiple dashes
|
||||||
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
|
.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";
|
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,10 @@
|
|||||||
import { Printful } from "$lib/services/commerce/printful"
|
import type { Webflow } from "./util/types";
|
||||||
import { formatSlug, getProductImageUrls, type DeepPartial } from "./util";
|
import { type DeepPartial } from "./util/misc";
|
||||||
|
|
||||||
export namespace Webflow {
|
export default class WebflowService {
|
||||||
export namespace Products {
|
static Products = class {
|
||||||
export namespace Skus {
|
static Skus = class {
|
||||||
export type Sku = {
|
static async create(webflowProductId: string, skus: DeepPartial<Webflow.Products.Skus.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>[]) {
|
|
||||||
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus`, {
|
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
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}`, {
|
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -61,38 +40,7 @@ export namespace Webflow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = {
|
static async create(webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>) {
|
||||||
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(`${env().API_SITES_URL}/products`, {
|
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -112,69 +60,7 @@ export namespace Webflow {
|
|||||||
return createdWebflowProduct.product.id
|
return createdWebflowProduct.product.id
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUsingPrintful(printfulProduct: Printful.Products.Product): Promise<Webflow.Products.ProductAndSkus> {
|
static async getAll(): 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[]> {
|
|
||||||
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -188,10 +74,10 @@ export namespace Webflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = await res.json();
|
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}`, {
|
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -209,10 +95,10 @@ export namespace Webflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = await res.json();
|
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}`, {
|
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -228,60 +114,7 @@ export namespace Webflow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUsingPrintful(printfulProduct: Printful.Products.Product) {
|
static async remove(webflowProductId: string) {
|
||||||
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(`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`, {
|
const res = await fetch(`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -296,35 +129,25 @@ export namespace Webflow {
|
|||||||
throw new Error("Failed to remove Webflow product");
|
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}` }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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 { Printful } from "$lib/services/commerce/printful";
|
||||||
import { Webflow } from "$lib/services/commerce/webflow";
|
import { Webflow } from "$lib/services/commerce/webflow";
|
||||||
|
|
||||||
const player: Player = {
|
const player: Player = {
|
||||||
@@ -61,6 +62,7 @@ const products = await Webflow.Products.getAll();
|
|||||||
|
|
||||||
const skuId = "68d080a5d90e8263e52242e9";
|
const skuId = "68d080a5d90e8263e52242e9";
|
||||||
|
|
||||||
|
const colors = ["Black", "White", "Red"];
|
||||||
|
|
||||||
// const sku = product.skus[0];
|
// const sku = product.skus[0];
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Printful } from '$lib/services/commerce/printful';
|
import PrintfulService from '$lib/services/commerce/printful';
|
||||||
import { Webflow } from '$lib/services/commerce/webflow';
|
import WebflowService from '$lib/services/commerce/webflow';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
|
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
|
||||||
return {
|
return {
|
||||||
products: {
|
products: {
|
||||||
printful: Printful.Products.getAll(),
|
printful: PrintfulService.Products.getAll(),
|
||||||
webflow: Webflow.Products.getAll(),
|
webflow: WebflowService.Products.getAll(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from "$lib/api.js";
|
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";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|||||||
Reference in New Issue
Block a user