Working on redirect old URLs to new URLs framework in Squidex, renamed Multilingual to Localized, NonMultilingual to NonLocalized. Splitting apart massive Amazon integration script into smaller components.

This commit is contained in:
David Ball 2024-08-24 13:42:24 -04:00
parent b0d4fecd8f
commit 3cfa093d01
68 changed files with 901 additions and 603 deletions

View File

@ -6,6 +6,7 @@
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"catalog": "bun run src/apps/catalog/catalog.ts",
"preview": "astro preview",
"astro": "astro",
"test": "vitest",

View File

@ -1,404 +0,0 @@
import { getBrandsByLangSlug, getBrandsUsingJsonQuery, getMarketplacesUsingJsonQuery, getProductCategoriesByIds, getProductCategoriesUsingJsonQuery, getProductListingsUsingJsonQuery, getProductsByIds, getProductsUsingJsonQuery, getSellersUsingJsonQuery, performSyncLocalizedSlugs } from '../data/api-client.ts';
import { program } from 'commander';
import * as core from '../data/core/client.js'
import type { AmazonMarketplaceConnection } from '../data/models/components/AmazonMarketplaceConnection.js';
import type { Product } from '../data/models/multis/Product.js';
import type { Listing } from '../data/models/multis/Listing.js';
import type { Brand } from '../data/models/multis/Brand.js';
import { askAiProductSubCategoryEvalQuestionsSet1, askAiProductSubCategoryEvalQuestionsSet2, askAiTopLevelProductCategoryEvalQuestions, designAiTopLevelProductCategoryEvalQuestions, doesProductAlreadyExist, generateAIProductDescription, generateAITagsForProduct, getAddNewBrandDtoByName, getAddNewProductDtoByProduct, getAddNewProductListingDtoByProduct, getAddNewProductSubCategoryDto, getAddNewSellerDtoByName, getAllTopLevelProductCategories, getAmazonMarketplaceConnectionSchemaDto, getAmazonMarketplaceDto, getBrandDtoByName, getMarketplaceConnectionSchemaDto, getSellerDtoByName, isValidASIN, removeQuotes, uploadDownloadedImageToSquidexAsAsset, translateAmazonDescription_from_en_US_to_es_US, translateAmazonDescription_from_en_US_to_fr_CA, translateProductDescription_from_en_US_to_es_US, translateProductName_from_en_US_to_es_US, translateProductName_from_en_US_to_fr_CA, translateTags_from_en_US_to_es_US, translateTags_from_en_US_to_fr_CA, trimPeriods, upsertAssetFolder, getAllAssetsInFolder, getAddNewOfferDto, lookupAmazonASINs, translateProductDescription_from_en_US_to_fr_CA } from './modules/amazon-catalog-helpers.js';
import slugify from 'slugify';
import type { Multilingual } from '../data/internals/MultilingualT.js';
import type { NonMultilingual } from '../data/internals/NonMultilingualT.js';
import type { MarketplaceConnection } from '../data/models/components/MarketplaceConnection.js';
import type { Offer } from '../data/models/multis/Offer.js';
import { SCHEMAS } from '../data/models/schemas.js';
import { CheerioCrawler, type CheerioCrawlingContext, log } from 'crawlee';
import { extractProductDetails } from '../scraper/amazon.js';
program.command('ls')
.description('List all the products')
.action(async (args: string[]) => {
let productsDto = await getProductsUsingJsonQuery();
productsDto.items.forEach((productDto, index) => {
console.log(`[ls:${index+1}] ID: ${productDto.id}`)
console.log(`[ls:${index+1}] Product (en-US): ${productDto.data?.productName['en-US']}`);
console.log(`[ls:${index+1}] Product (es-US): ${productDto.data?.productName['es-US']}`);
console.log(`[ls:${index+1}] Product (fr-CA): ${productDto.data?.productName['fr-CA']}`);
let asin = productDto.data?.marketplaceConnections.iv.map((connection) => (connection.connection as AmazonMarketplaceConnection).asin).join('');
let siteStripUrl = productDto.data?.marketplaceConnections.iv.map((connection) => (connection.connection as AmazonMarketplaceConnection).siteStripeUrl).join('');
console.log(`[ls:${index+1}] ASIN: ${asin} ${!!(asin||'').match(/[A-Z0-9]{10}/g) ? 'Is a valid ASIN.' : 'Is not a valid ASIN.'}`)
console.log(`[ls:${index+1}] Amazon SiteStripe URL: ${siteStripUrl}`)
console.log();
});
console.log(`[ls] Returned ${productsDto.items.length} products.`)
})
.configureHelp();
program.command('crawlee-products')
.alias('crawlee-product')
.argument('<URLs...>')
.description('Attempts to crawl Amazon product data using crawlee')
.action(async (urls: string[]) => {
/**
* Performs the logic of the crawler. It is called for each URL to crawl.
* - Passed to the crawler using the `requestHandler` option.
*/
const requestHandler = async (context: CheerioCrawlingContext) => {
const { $, request } = context;
const { url } = request;
log.info(`Scraping product page`, { url });
const extractedProduct = extractProductDetails($);
log.info(`Scraped product details for "${extractedProduct.title}", saving...`, { url });
crawler.pushData(extractedProduct);
};
/**
* The crawler instance. Crawlee provides a few different crawlers, but we'll use CheerioCrawler, as it's very fast and simple to use.
* - Alternatively, we could use a full browser crawler like `PlaywrightCrawler` to imitate a real browser.
*/
const crawler = new CheerioCrawler({ requestHandler });
await crawler.run(urls);
})
.configureHelp();
program.command('sync-slugs')
.description('Sync URL slugs for each frontend endpoint.')
.action(async (asin: string, args: string[]) => {
await performSyncLocalizedSlugs(console.log);
})
.configureHelp();
program.command('append-images')
.alias('append-image')
.argument('<ASIN>', 'Amazon Standard Identification Numbers')
.argument('<URLs...>', 'Image URLs to transmit to the database.')
.description('Download images from URL .')
.action(async (asin: string, urls: string[]) => {
if (!isValidASIN(asin)) {
console.error(`[append-images] error: ${asin} is not a valid ASIN. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
let productsDto = await getProductsUsingJsonQuery(JSON.stringify({ filter: {
op: 'eq',
path: 'data.marketplaceConnections.iv.connection.asin',
value: asin,
}}));
if (productsDto.items.length === 0) {
console.error(`[append-images] error: ${asin} was not found in the database. Please procure or enter the product first.`);
return;
}
let marketplacesDto = await getMarketplacesUsingJsonQuery(JSON.stringify({ filter: {
op: 'eq',
path: 'data.marketplaceName.en-US',
value: 'Amazon',
}}));
if (marketplacesDto.items.length === 0) {
console.error(`[append-images] error: Amazon marketplace not found in database. Please set up Amazon marketplace in database.`);
return;
}
console.log(`[append-images] Upserting Asset Folder products`);
let productsAssetFolder = await upsertAssetFolder('products');
console.log(`[append-images] Matching Asset Folder: ${productsAssetFolder.folderName} with Asset Folder ID: ${productsAssetFolder.id}`);
console.log(`[append-images] Upserting Asset Folder ${productsAssetFolder.folderName}/amazon`);
let productsAmazonAssetFolder = await upsertAssetFolder('amazon', productsAssetFolder.id);
console.log(`[append-images] Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName} with Asset Folder ID: ${productsAmazonAssetFolder.id}`);
console.log(`[append-images] Upserting Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${asin}`);
let productsASINFolder = await upsertAssetFolder(asin, productsAmazonAssetFolder.id);
console.log(`[append-images] Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${productsASINFolder.folderName} with Asset Folder ID: ${productsASINFolder.id}`);
let amazonMarketplaceDto = marketplacesDto.items[0];
for (let productDto of productsDto.items) {
let listingsDto = await getProductListingsUsingJsonQuery(JSON.stringify({ filter: {
'and': [
{
op: 'eq',
path: 'data.product.iv',
value: productDto.id,
},
{
op: 'eq',
path: 'data.marketplace.iv',
value: amazonMarketplaceDto.id,
}
]
}}));
if (listingsDto.items.length === 0) {
console.error(`[append-images] error: Product listing for ${asin} on Amazon marketplace was not found in the database. Please procure or enter the product listing first.`);
return;
}
for (let listingDto of listingsDto.items) {
let listing = listingDto.data!;
let didUpdate = false;
let amazonAssetsDto = await getAllAssetsInFolder(productsASINFolder.id);
for (let i = 0; i < urls.length; i++) {
console.log(urls[i]);
let foundUploaded = amazonAssetsDto.filter((asset) => (asset.metadata['amazon-url'] as string||'') === urls[i]);
if (!foundUploaded.length) { // is not found
console.log(`[append-images] Transmitting Product Image ${urls[i]} to Squidex Assets`);
let assetDto = await uploadDownloadedImageToSquidexAsAsset(urls[i]!, productsASINFolder.id);
console.log(`[append-images] Saved Asset Id: ${assetDto.id} to Asset Folder Id: ${assetDto.parentId}`);
if (!listing.marketplaceImages) {
listing.marketplaceImages = { iv: [] };
}
if (!listing.marketplaceImages.iv) {
listing.marketplaceImages.iv = [];
}
listing.marketplaceImages.iv.push(assetDto.id);
didUpdate = true;
}
else { // is found
console.log(`[append-images] Matched Asset Id: ${foundUploaded[0].id}, Amazon Product Image: ${foundUploaded[0].metadata['amazon-url'] as string||''}.`);
}
}
if (didUpdate) {
console.log(`[append-images] Listing did update, updating product listing with appended images.`);
let updatedDto = await core.client.contents.putContent(SCHEMAS.LISTINGS, listingDto.id, {
unpublished: false,
body: listing as any,
}, {
timeoutInSeconds: core.TIMEOUT_IN_SECONDS
});
console.log(`[append-images] Listing version ${updatedDto.version} stored.`);
}
}
}
})
.configureHelp();
// TODO: when I get back I need to build a command that lets me replay the procure-asins previous API request
program.command('procure-asins')
.alias('procure-asin')
.argument('<ASINs...>', 'Amazon Standard Identification Numbers')
.description('Begin automated product procurement from Amazon by inputting one or more ASINs separated by spaces.')
.action(async (asins: string[]) => {
if (!asins.length) {
console.error(`[procure-asin] error: You must specify one or more valid ASINs. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
for (let a = 0; a < asins.length; a++) {
let asin = asins[a].toUpperCase();
if (!isValidASIN(asin)) {
console.error(`[procure-asin] error: ${asin} is not a valid ASIN. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
}
console.log(`[procure-asin] You started product enrollment for ${asins.length} items.`);
if (asins.length > 10) {
console.log(`[procure-asin] PA API calls will be broken up into 10 items per Amazon API request.`);
}
const MAX_ITEMS_PER_API_REQUEST = 10;
for (let a = 0; a < asins.length; a += MAX_ITEMS_PER_API_REQUEST) {
let asinsToRequest = asins.slice(a, a + MAX_ITEMS_PER_API_REQUEST).map(asin => asin = asin.toUpperCase());
console.log(`[procure-asin] Begin product enrollment(s) for ${asins.length} ASINs ${asinsToRequest.join(' ')}.`);
//get Amazon data ; we will use previously obtained data for the testing phase
//TODO:remove these three lines and replace with code that reads the Amazon PA API
// console.log(`[procure-asin] NOTICE: For testing purposes, using data from stored previous API response.`);
console.log(`[procure-asin] NOTICE: Production Amazon PA API request in progress.`);
const responseDto = await lookupAmazonASINs(asinsToRequest);
const apiResponse = responseDto.items[0].data?.apiResponse.iv!;
apiResponse.ItemsResult!.Items!.forEach(async (amazonItem) => {
let asin = amazonItem.ASIN!;
console.log(`[procure-asin] Mock data override: product enrollment is for ASIN ${asin}.`);
// enable this if we're saving data:
// if (await doesProductAlreadyExist(asin)) {
// console.error(`[procure-asin] error: Product with ASIN already exists in the application.`);
// return;
// }
console.log(`[procure-asin] Amazon PA API Response contains ASIN: ${asin}.`);
const amazonProductName_en_US = amazonItem.ItemInfo?.Title?.DisplayValue!;
console.log(`[procure-asin] Amazon PA API Response contains Product Name: ${amazonProductName_en_US}.`);
console.log(`[procure-asin] Amazon PA API Response contains Product Features: \n${amazonItem.ItemInfo?.Features?.DisplayValues?.map((item) => `- ${item}`).join('\n')}`);
const amazonProductImages = [amazonItem.Images?.Primary?.Large?.URL, ...(amazonItem.Images?.Variants?.map(variant => variant.Large?.URL)||[])];
console.log(`[procure-asin] Amazon PA API Response contains Product Images: \n${amazonProductImages.map((url, index) => `${index+1}. ${url}`).join('\n')}`);
console.log(`[procure-asin] Upserting Asset Folder products`);
let productsAssetFolder = await upsertAssetFolder('products');
console.log(`[procure-asin] Matching Asset Folder: ${productsAssetFolder.folderName} with Asset Folder ID: ${productsAssetFolder.id}`);
console.log(`[procure-asin] Upserting Asset Folder ${productsAssetFolder.folderName}/amazon`);
let productsAmazonAssetFolder = await upsertAssetFolder('amazon', productsAssetFolder.id);
console.log(`[procure-asin] Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName} with Asset Folder ID: ${productsAmazonAssetFolder.id}`);
console.log(`[procure-asin] Upserting Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${asin}`);
let productsASINFolder = await upsertAssetFolder(asin, productsAmazonAssetFolder.id);
console.log(`[procure-asin] Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${productsASINFolder.folderName} with Asset Folder ID: ${productsASINFolder.id}`);
let amazonAssetsDto = await getAllAssetsInFolder(productsASINFolder.id);
let amazonAssetsIds = amazonAssetsDto.map(asset => asset.id);
let amazonAssetsUrls: string[] = amazonAssetsDto.map(asset => asset.metadata['amazon-url'] as string||'').filter(url => url);
for (let i = 0; i < amazonProductImages.length; i++) {
let foundUploaded = amazonAssetsDto.filter((asset) => (asset.metadata['amazon-url'] as string||'') === amazonProductImages[i]);
if (!foundUploaded.length) { // is not found
console.log(`[procure-asin] Transmitting Amazon Product Image ${amazonProductImages[i]} to Squidex Assets`);
let assetDto = await uploadDownloadedImageToSquidexAsAsset(amazonProductImages[i]!, productsASINFolder.id);
console.log(`[procure-asin] Saved Asset Id: ${assetDto.id} to Asset Folder Id: ${assetDto.parentId}`);
amazonAssetsDto.push(assetDto);
amazonAssetsIds.push(assetDto.id);
amazonAssetsUrls.push(assetDto.metadata['amazon-url'] as string||'');
}
else { // is found
console.log(`[procure-asin] Matched Asset Id: ${foundUploaded[0].id}, Amazon Product Image: ${foundUploaded[0].metadata['amazon-url'] as string||''}.`);
}
}
let schemaProductMarketplaceConnectionDto = await getMarketplaceConnectionSchemaDto();
console.log(`[procure-asin] Matching Schema ID: ${schemaProductMarketplaceConnectionDto.id}, Schema Name: ${schemaProductMarketplaceConnectionDto.name}.`);
let schemaAmazonMarketplaceConnectionDto = await getAmazonMarketplaceConnectionSchemaDto();
console.log(`[procure-asin] Matching Schema ID: ${schemaAmazonMarketplaceConnectionDto.id}, Schema Name: ${schemaAmazonMarketplaceConnectionDto.name}.`);
let amazonMarketplaceDto = await getAmazonMarketplaceDto()
console.log(`[procure-asin] Matching Marketplace ID: ${amazonMarketplaceDto.items[0].id}, Marketplace Name: ${amazonMarketplaceDto.items[0].data?.marketplaceName['en-US']}.`);
let bylineBrand = amazonItem.ItemInfo!.ByLineInfo!.Brand!.DisplayValue!;
console.log(`[procure-asin] Amazon PA API Response contains Brand ${bylineBrand}. Checking database for brand...`);
let brandsDto = await getBrandDtoByName(bylineBrand);
if (!brandsDto.items.length) {
console.log(`[procure-asin] Brand not found in database. Product requires creation of brand first.`);
brandsDto = await getAddNewBrandDtoByName(bylineBrand);
console.log(`[procure-asin] Brand Id: ${brandsDto.items[0].id}, Brand Name: ${brandsDto.items[0].data?.brandName['en-US']} created in database.`);
} else {
console.log(`[procure-asin] Matching Brand ID: ${brandsDto.items[0].id}, Brand Name: ${brandsDto.items[0].data?.brandName['en-US']}.`);
}
let brandName = brandsDto.items[0].data?.brandName['en-US']!;
let features = amazonItem.ItemInfo?.Features?.DisplayValues!||[];
console.log(`[procure-asin] Amazon Product Name (en-US): ${amazonProductName_en_US}`);
console.log(`[procure-asin] Requesting llama3.1 LLM to translate product name from en-US to es-US.`);
const amazonProductName_es_US = await translateProductName_from_en_US_to_es_US(amazonItem, brandName, amazonProductName_en_US);
console.log(`[procure-asin] AI Product Name (es-US): ${amazonProductName_es_US}`);
console.log(`[procure-asin] Requesting llama3.1 LLM to translate product name from en-US to fr-CA.`);
const amazonProductName_fr_CA = await translateProductName_from_en_US_to_fr_CA(amazonItem, brandName, amazonProductName_en_US);
console.log(`[procure-asin] AI Product Name (fr-CA): ${amazonProductName_fr_CA}`);
const aiProductDescriptionResponse_en_US = await generateAIProductDescription(amazonProductName_en_US, features);
console.log(`[procure-asin] Generated AI Product Description (en-US):\n${aiProductDescriptionResponse_en_US}`);
console.log(`[procure-asin] Requesting llama3.1 LLM to translate product description from en-US to es-US.`);
const aiProductDescriptionResponse_es_US = await translateProductDescription_from_en_US_to_es_US(brandName, aiProductDescriptionResponse_en_US);
console.log(`[procure-asin] Translated AI Product Description (es-US):\n${aiProductDescriptionResponse_es_US}`);
console.log(`[procure-asin] Requesting llama3.1 LLM to translate product description from en-US to fr-CA.`);
const aiProductDescriptionResponse_fr_CA = await translateProductDescription_from_en_US_to_fr_CA(brandName, aiProductDescriptionResponse_en_US);
console.log(`[procure-asin] AI Product Description (fr-CA):\n${aiProductDescriptionResponse_fr_CA}`);
const aiTopLevelCategoryChoice = await askAiTopLevelProductCategoryEvalQuestions(brandName, amazonProductName_en_US);
console.log('[procure-asin]', `AI chooses to put product within top level Product Category ID: ${aiTopLevelCategoryChoice}, Product Category Name: ${(await getAllTopLevelProductCategories()).filter((category) => category.id === aiTopLevelCategoryChoice)[0].categoryName!['en-US']}`);
const aiSubCategoryQuestion1Answer = await askAiProductSubCategoryEvalQuestionsSet1(aiTopLevelCategoryChoice, brandName, amazonProductName_en_US, features);
console.log('[procure-asin]', `AI then chooses within the top-level category to ${aiSubCategoryQuestion1Answer?'make a new sub-category':'use an existing sub-category'}.`);
const aiSubCategoryQuestion2Answer = await askAiProductSubCategoryEvalQuestionsSet2(apiResponse, aiTopLevelCategoryChoice, brandName, amazonProductName_en_US, features, aiSubCategoryQuestion1Answer);;
let aiProductCategoriesDto;
if (!aiSubCategoryQuestion1Answer) {
let aiSubCategoryQuestion2AnswerStr = aiSubCategoryQuestion2Answer as string;
console.log(`[procure-asin] AI suggested existing sub-category ${aiSubCategoryQuestion2Answer}`);
aiProductCategoriesDto = await getProductCategoriesByIds(aiSubCategoryQuestion2AnswerStr);
}
else {
let aiSubCategoryQuestion2AnswerObj = aiSubCategoryQuestion2Answer as {
categoryName: Multilingual<string>,
description: Multilingual<string>,
parentCategory: NonMultilingual<string[]>,
};
console.log(`[procure-asin] AI suggested new sub-category ${aiSubCategoryQuestion2AnswerObj.categoryName['en-US']}`);
aiProductCategoriesDto = await getAddNewProductSubCategoryDto({ iv: [aiTopLevelCategoryChoice] }, aiSubCategoryQuestion2AnswerObj.categoryName, aiSubCategoryQuestion2AnswerObj.description);
}
let aiProductTags_en_US = await generateAITagsForProduct(amazonProductName_en_US, features);
let aiProductTags_es_US = await translateTags_from_en_US_to_es_US(aiProductTags_en_US);
let aiProductTags_fr_CA = await translateTags_from_en_US_to_fr_CA(aiProductTags_en_US);
let product: Product = {
brand: { iv: [brandsDto.items![0].id!] },
categories: { iv: aiProductCategoriesDto.items.map(category => category.id) },
productName: {
"en-US": amazonProductName_en_US,
"es-US": amazonProductName_es_US,
"fr-CA": amazonProductName_fr_CA,
},
slug: {
"en-US": `${brandsDto.items![0].data?.slug['en-US']}/${slugify(trimPeriods(removeQuotes(amazonProductName_en_US)), { lower: true, trim: true })}`,
"es-US": `${brandsDto.items![0].data?.slug['es-US']}/${slugify(trimPeriods(removeQuotes(amazonProductName_es_US)), { lower: true, trim: true })}`,
"fr-CA": `${brandsDto.items![0].data?.slug['fr-CA']}/${slugify(trimPeriods(removeQuotes(amazonProductName_fr_CA)), { lower: true, trim: true })}`,
},
description: {
"en-US": aiProductDescriptionResponse_en_US,
"es-US": aiProductDescriptionResponse_es_US,
"fr-CA": aiProductDescriptionResponse_fr_CA,
},
tags: {
"en-US": aiProductTags_en_US,
"es-US": aiProductTags_es_US,
"fr-CA": aiProductTags_fr_CA,
},
marketplaceConnections: {
iv: [
{
schemaId: schemaProductMarketplaceConnectionDto.id,
marketplace: [amazonMarketplaceDto.items[0].id],
connection: {
schemaId: schemaAmazonMarketplaceConnectionDto.id,
asin,
siteStripeUrl: amazonItem.DetailPageURL,
} as AmazonMarketplaceConnection | MarketplaceConnection
}
]
}
};
console.log(`[procure-asin] New product to store:`, product);
let productsDto = await getAddNewProductDtoByProduct(product);
console.log(`[procure-asin] Product Id: ${productsDto.items[0].id}, Product Name: ${productsDto.items[0].data?.productName['en-US']} created in database.`);
let listing: Listing = {
marketplace: { iv: [ amazonMarketplaceDto.items[0].id ] },
product: { iv: [ productsDto.items[0].id ]},
marketplaceDescription: {
"en-US": `${features.map((feature) => `- ${feature}`).join('\n')}\n`,
"es-US": await translateAmazonDescription_from_en_US_to_es_US(brandName, features),
"fr-CA": await translateAmazonDescription_from_en_US_to_fr_CA(brandName, features),
},
marketplaceImages: { iv: amazonAssetsIds },
};
console.log(`[procure-asin] New product listing to store:`, listing);
let listingsDto = await getAddNewProductListingDtoByProduct(listing);
console.log(`[procure-asin] Product Listing Id: ${listingsDto.items[0].id}, Product Id: ${listingsDto.items[0].data?.product.iv[0]}, Marketplace Id: ${listingsDto.items[0].data?.marketplace.iv[0]} created in database.`);
amazonItem.Offers?.Listings?.forEach(async (amazonListing) => {
console.log(`[procure-asin] Amazon PA API Response contains Seller ${amazonListing.MerchantInfo?.Name} offer for ${amazonListing.Price?.Amount} in ${amazonListing.Condition?.Value} condition. Checking database for seller...`);
let sellersDto = await getSellerDtoByName(amazonListing.MerchantInfo!.Name!);
if (!sellersDto.items.length) {
console.log(`[procure-asin] Seller not found in database. Listing requires creation of seller first.`);
sellersDto = await getAddNewSellerDtoByName(amazonListing.MerchantInfo!.Name!);
console.log(`[procure-asin] Seller Id: ${sellersDto.items[0].id}, Seller Name: ${sellersDto.items[0].data?.sellerName['en-US']} created in database.`);
} else {
console.log(`[procure-asin] Matching Seller ID: ${sellersDto.items[0].id}, Seller Name: ${sellersDto.items[0].data?.sellerName['en-US']}.`);
}
let offer: Offer = {
offerDate: { iv: new Date().toISOString() },
listing: { iv: [ listingsDto.items[0].id ] },
seller: { iv: [ sellersDto.items[0].id ] },
newPrice: { iv: null },
usedPrice: { iv: null },
};
if (amazonListing.Condition?.Value === 'New') {
offer.newPrice = { iv: amazonListing.Price?.Amount!};
offer.usedPrice = { iv: null };
}
else if (amazonListing.Condition?.Value === 'Used') {
offer.newPrice = { iv: null };
offer.usedPrice = { iv: amazonListing.Price?.Amount! }
}
console.log('Generated Offer:\n', offer);
let offersDto = await getAddNewOfferDto(offer);
});
await performSyncLocalizedSlugs(console.log);
});
}
})
.configureHelp();
program.parse();

View File

@ -0,0 +1,111 @@
import type { Command } from "commander";
import * as core from '../../../data/core/client'
import { getAllAssetsInFolder, isValidASIN, uploadDownloadedImageToSquidexAsAsset, upsertAssetFolder } from "../common/catalog-helpers";
import { getMarketplacesUsingJsonQuery, getProductListingsUsingJsonQuery, getProductsUsingJsonQuery } from "../../../data/api-client";
import { SCHEMAS } from "../../../data/models/schemas";
import { logForCommand } from "../common/console";
export const COMMAND_NAME = 'append-images';
const log = logForCommand(COMMAND_NAME);
const error = logForCommand(COMMAND_NAME, console.error);
export const amazonAppendImagesCommand = (amazonCommand: Command) =>
amazonCommand.command(COMMAND_NAME).alias('append-image')
.description('Download images from URL .')
.argument('<ASIN>', 'Amazon Standard Identification Numbers')
.argument('<URLs...>', 'Image URLs to transmit to the database.')
.action(async (asin: string, urls: string[]) => {
if (!isValidASIN(asin)) {
error(`error: ${asin} is not a valid ASIN. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
let productsDto = await getProductsUsingJsonQuery(JSON.stringify({ filter: {
op: 'eq',
path: 'data.marketplaceConnections.iv.connection.asin',
value: asin,
}}));
if (productsDto.items.length === 0) {
error(`error: ${asin} was not found in the database. Please procure or enter the product first.`);
return;
}
let marketplacesDto = await getMarketplacesUsingJsonQuery(JSON.stringify({ filter: {
op: 'eq',
path: 'data.marketplaceName.en-US',
value: 'Amazon',
}}));
if (marketplacesDto.items.length === 0) {
error(`error: Amazon marketplace not found in database. Please set up Amazon marketplace in database.`);
return;
}
log(`Upserting Asset Folder products`);
let productsAssetFolder = await upsertAssetFolder('products');
log(`Matching Asset Folder: ${productsAssetFolder.folderName} with Asset Folder ID: ${productsAssetFolder.id}`);
log(`Upserting Asset Folder ${productsAssetFolder.folderName}/amazon`);
let productsAmazonAssetFolder = await upsertAssetFolder('amazon', productsAssetFolder.id);
log(`Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName} with Asset Folder ID: ${productsAmazonAssetFolder.id}`);
log(`Upserting Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${asin}`);
let productsASINFolder = await upsertAssetFolder(asin, productsAmazonAssetFolder.id);
log(`Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${productsASINFolder.folderName} with Asset Folder ID: ${productsASINFolder.id}`);
let amazonMarketplaceDto = marketplacesDto.items[0];
for (let productDto of productsDto.items) {
let listingsDto = await getProductListingsUsingJsonQuery(JSON.stringify({ filter: {
'and': [
{
op: 'eq',
path: 'data.product.iv',
value: productDto.id,
},
{
op: 'eq',
path: 'data.marketplace.iv',
value: amazonMarketplaceDto.id,
}
]
}}));
if (listingsDto.items.length === 0) {
error(`error: Product listing for ${asin} on Amazon marketplace was not found in the database. Please procure or enter the product listing first.`);
return;
}
for (let listingDto of listingsDto.items) {
let listing = listingDto.data!;
let didUpdate = false;
let amazonAssetsDto = await getAllAssetsInFolder(productsASINFolder.id);
for (let i = 0; i < urls.length; i++) {
log(urls[i]);
let foundUploaded = amazonAssetsDto.filter((asset) => (asset.metadata['amazon-url'] as string||'') === urls[i]);
if (!foundUploaded.length) { // is not found
log(`Transmitting Product Image ${urls[i]} to Squidex Assets`);
let assetDto = await uploadDownloadedImageToSquidexAsAsset(urls[i]!, productsASINFolder.id);
log(`Saved Asset Id: ${assetDto.id} to Asset Folder Id: ${assetDto.parentId}`);
if (!listing.marketplaceImages) {
listing.marketplaceImages = { iv: [] };
}
if (!listing.marketplaceImages.iv) {
listing.marketplaceImages.iv = [];
}
listing.marketplaceImages.iv.push(assetDto.id);
didUpdate = true;
}
else { // is found
log(`Matched Asset Id: ${foundUploaded[0].id}, Amazon Product Image: ${foundUploaded[0].metadata['amazon-url'] as string||''}.`);
}
}
if (didUpdate) {
log(`Listing did update, updating product listing with appended images.`);
let updatedDto = await core.client.contents.putContent(SCHEMAS.LISTINGS, listingDto.id, {
unpublished: false,
body: listing as any,
}, {
timeoutInSeconds: core.TIMEOUT_IN_SECONDS
});
log(`Listing version ${updatedDto.version} stored.`);
}
}
}
})
.configureHelp();

View File

@ -0,0 +1,34 @@
import type { Command } from "commander";
import { CheerioCrawler, log as crawleeLog, type CheerioCrawlingContext } from "crawlee";
import { extractProductDetails } from "../../../scraper/amazon";
import { logForCommand } from "../common/console";
export const COMMAND_NAME = 'crawlee-products';
const log = logForCommand(COMMAND_NAME, crawleeLog.info);
export const amazonCrawleeProductsCommand = (amazonCommand: Command) =>
amazonCommand.command(COMMAND_NAME).alias('crawlee-product')
.description('Attempts to crawl Amazon product data using crawlee')
.argument('<URLs...>')
.action(async (urls: string[]) => {
/**
* Performs the logic of the crawler. It is called for each URL to crawl.
* - Passed to the crawler using the `requestHandler` option.
*/
const requestHandler = async (context: CheerioCrawlingContext) => {
const { $, request } = context;
const { url } = request;
log(`Scraping product page`, { url });
const extractedProduct = extractProductDetails($);
log(`Scraped product details for "${extractedProduct.title}", saving...`, { url });
crawler.pushData(extractedProduct);
};
/**
* The crawler instance. Crawlee provides a few different crawlers, but we'll use CheerioCrawler, as it's very fast and simple to use.
* - Alternatively, we could use a full browser crawler like `PlaywrightCrawler` to imitate a real browser.
*/
const crawler = new CheerioCrawler({ requestHandler });
await crawler.run(urls);
})
.configureHelp();

View File

@ -0,0 +1,251 @@
import type { Command } from "commander";
import { askAiProductSubCategoryEvalQuestionsSet1, askAiProductSubCategoryEvalQuestionsSet2, askAiTopLevelProductCategoryEvalQuestions, generateAIProductDescription, generateAITagsForProduct, getAddNewBrandDtoByName, getAddNewOfferDto, getAddNewProductDtoByProduct, getAddNewProductListingDtoByProduct, getAddNewProductSubCategoryDto, getAddNewSellerDtoByName, getAllAssetsInFolder, getAllTopLevelProductCategories, getAmazonMarketplaceConnectionSchemaDto, getAmazonMarketplaceDto, getBrandDtoByName, getMarketplaceConnectionSchemaDto, getSellerDtoByName, isValidASIN, lookupAmazonASINs, removeQuotes, translateAmazonDescription_from_en_US_to_es_US, translateAmazonDescription_from_en_US_to_fr_CA, translateProductDescription_from_en_US_to_es_US, translateProductDescription_from_en_US_to_fr_CA, translateProductName_from_en_US_to_es_US, translateProductName_from_en_US_to_fr_CA, translateTags_from_en_US_to_es_US, translateTags_from_en_US_to_fr_CA, trimPeriods, uploadDownloadedImageToSquidexAsAsset, upsertAssetFolder } from "../common/catalog-helpers";
import type { Localized } from "../../../data/internals/LocalizedT";
import type { NonLocalized } from "../../../data/internals/NonLocalizedT";
import { getProductCategoriesByIds, performSyncLocalizedSlugs } from "../../../data/api-client";
import type { Product } from "../../../data/models/multis/Product";
import slugify from "slugify";
import type { AmazonMarketplaceConnection } from "../../../data/models/components/AmazonMarketplaceConnection";
import type { MarketplaceConnection } from "../../../data/models/components/MarketplaceConnection";
import type { Listing } from "../../../data/models/multis/Listing";
import type { Offer } from "../../../data/models/multis/Offer";
import { logForCommand } from "../common/console";
export const COMMAND_NAME = 'procure-asins';
const log = logForCommand(COMMAND_NAME);
const error = logForCommand(COMMAND_NAME, console.error);
export const amazonProcureASINsCommand = (amazonCommand: Command) => {
let command = amazonCommand.command(COMMAND_NAME)
.alias('procure-asin')
.argument('<ASINs...>', 'Amazon Standard Identification Numbers')
.description('Begin automated product procurement from Amazon by inputting one or more ASINs separated by spaces.')
.action(async (asins: string[]) => {
if (!asins.length) {
error(`error: You must specify one or more valid ASINs. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
for (let a = 0; a < asins.length; a++) {
let asin = asins[a].toUpperCase();
if (!isValidASIN(asin)) {
error(`error: ${asin} is not a valid ASIN. Amazon Standard Identification Numbers are 10-digits long with letters A-Z and numbers 0-9.`);
return;
}
}
log(`You started product enrollment for ${asins.length} items.`);
if (asins.length > 10) {
log(`PA API calls will be broken up into 10 items per Amazon API request.`);
}
const MAX_ITEMS_PER_API_REQUEST = 10;
for (let a = 0; a < asins.length; a += MAX_ITEMS_PER_API_REQUEST) {
let asinsToRequest = asins.slice(a, a + MAX_ITEMS_PER_API_REQUEST).map(asin => asin = asin.toUpperCase());
log(`Begin product enrollment(s) for ${asins.length} ASINs ${asinsToRequest.join(' ')}.`);
//get Amazon data ; we will use previously obtained data for the testing phase
//TODO:remove these three lines and replace with code that reads the Amazon PA API
// log(`NOTICE: For testing purposes, using data from stored previous API response.`);
log(`NOTICE: Production Amazon PA API request in progress.`);
const responseDto = await lookupAmazonASINs(asinsToRequest);
const apiResponse = responseDto.items[0].data?.apiResponse.iv!;
apiResponse.ItemsResult!.Items!.forEach(async (amazonItem) => {
let asin = amazonItem.ASIN!;
log(`Mock data override: product enrollment is for ASIN ${asin}.`);
// enable this if we're saving data:
// if (await doesProductAlreadyExist(asin)) {
// error(`error: Product with ASIN already exists in the application.`);
// return;
// }
log(`Amazon PA API Response contains ASIN: ${asin}.`);
const amazonProductName_en_US = amazonItem.ItemInfo?.Title?.DisplayValue!;
log(`Amazon PA API Response contains Product Name: ${amazonProductName_en_US}.`);
log(`Amazon PA API Response contains Product Features: \n${amazonItem.ItemInfo?.Features?.DisplayValues?.map((item) => `- ${item}`).join('\n')}`);
const amazonProductImages = [amazonItem.Images?.Primary?.Large?.URL, ...(amazonItem.Images?.Variants?.map(variant => variant.Large?.URL)||[])];
log(`Amazon PA API Response contains Product Images: \n${amazonProductImages.map((url, index) => `${index+1}. ${url}`).join('\n')}`);
log(`Upserting Asset Folder products`);
let productsAssetFolder = await upsertAssetFolder('products');
log(`Matching Asset Folder: ${productsAssetFolder.folderName} with Asset Folder ID: ${productsAssetFolder.id}`);
log(`Upserting Asset Folder ${productsAssetFolder.folderName}/amazon`);
let productsAmazonAssetFolder = await upsertAssetFolder('amazon', productsAssetFolder.id);
log(`Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName} with Asset Folder ID: ${productsAmazonAssetFolder.id}`);
log(`Upserting Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${asin}`);
let productsASINFolder = await upsertAssetFolder(asin, productsAmazonAssetFolder.id);
log(`Matching Asset Folder ${productsAssetFolder.folderName}/${productsAmazonAssetFolder.folderName}/${productsASINFolder.folderName} with Asset Folder ID: ${productsASINFolder.id}`);
let amazonAssetsDto = await getAllAssetsInFolder(productsASINFolder.id);
let amazonAssetsIds = amazonAssetsDto.map(asset => asset.id);
let amazonAssetsUrls: string[] = amazonAssetsDto.map(asset => asset.metadata['amazon-url'] as string||'').filter(url => url);
for (let i = 0; i < amazonProductImages.length; i++) {
let foundUploaded = amazonAssetsDto.filter((asset) => (asset.metadata['amazon-url'] as string||'') === amazonProductImages[i]);
if (!foundUploaded.length) { // is not found
log(`Transmitting Amazon Product Image ${amazonProductImages[i]} to Squidex Assets`);
let assetDto = await uploadDownloadedImageToSquidexAsAsset(amazonProductImages[i]!, productsASINFolder.id);
log(`Saved Asset Id: ${assetDto.id} to Asset Folder Id: ${assetDto.parentId}`);
amazonAssetsDto.push(assetDto);
amazonAssetsIds.push(assetDto.id);
amazonAssetsUrls.push(assetDto.metadata['amazon-url'] as string||'');
}
else { // is found
log(`Matched Asset Id: ${foundUploaded[0].id}, Amazon Product Image: ${foundUploaded[0].metadata['amazon-url'] as string||''}.`);
}
}
let schemaProductMarketplaceConnectionDto = await getMarketplaceConnectionSchemaDto();
log(`Matching Schema ID: ${schemaProductMarketplaceConnectionDto.id}, Schema Name: ${schemaProductMarketplaceConnectionDto.name}.`);
let schemaAmazonMarketplaceConnectionDto = await getAmazonMarketplaceConnectionSchemaDto();
log(`Matching Schema ID: ${schemaAmazonMarketplaceConnectionDto.id}, Schema Name: ${schemaAmazonMarketplaceConnectionDto.name}.`);
let amazonMarketplaceDto = await getAmazonMarketplaceDto()
log(`Matching Marketplace ID: ${amazonMarketplaceDto.items[0].id}, Marketplace Name: ${amazonMarketplaceDto.items[0].data?.marketplaceName['en-US']}.`);
let bylineBrand = amazonItem.ItemInfo!.ByLineInfo!.Brand!.DisplayValue!;
log(`Amazon PA API Response contains Brand ${bylineBrand}. Checking database for brand...`);
let brandsDto = await getBrandDtoByName(bylineBrand);
if (!brandsDto.items.length) {
log(`Brand not found in database. Product requires creation of brand first.`);
brandsDto = await getAddNewBrandDtoByName(bylineBrand);
log(`Brand Id: ${brandsDto.items[0].id}, Brand Name: ${brandsDto.items[0].data?.brandName['en-US']} created in database.`);
} else {
log(`Matching Brand ID: ${brandsDto.items[0].id}, Brand Name: ${brandsDto.items[0].data?.brandName['en-US']}.`);
}
let brandName = brandsDto.items[0].data?.brandName['en-US']!;
let features = amazonItem.ItemInfo?.Features?.DisplayValues!||[];
log(`Amazon Product Name (en-US): ${amazonProductName_en_US}`);
log(`Requesting llama3.1 LLM to translate product name from en-US to es-US.`);
const amazonProductName_es_US = await translateProductName_from_en_US_to_es_US(amazonItem, brandName, amazonProductName_en_US);
log(`AI Product Name (es-US): ${amazonProductName_es_US}`);
log(`Requesting llama3.1 LLM to translate product name from en-US to fr-CA.`);
const amazonProductName_fr_CA = await translateProductName_from_en_US_to_fr_CA(amazonItem, brandName, amazonProductName_en_US);
log(`AI Product Name (fr-CA): ${amazonProductName_fr_CA}`);
const aiProductDescriptionResponse_en_US = await generateAIProductDescription(amazonProductName_en_US, features);
log(`Generated AI Product Description (en-US):\n${aiProductDescriptionResponse_en_US}`);
log(`Requesting llama3.1 LLM to translate product description from en-US to es-US.`);
const aiProductDescriptionResponse_es_US = await translateProductDescription_from_en_US_to_es_US(brandName, aiProductDescriptionResponse_en_US);
log(`Translated AI Product Description (es-US):\n${aiProductDescriptionResponse_es_US}`);
log(`Requesting llama3.1 LLM to translate product description from en-US to fr-CA.`);
const aiProductDescriptionResponse_fr_CA = await translateProductDescription_from_en_US_to_fr_CA(brandName, aiProductDescriptionResponse_en_US);
log(`AI Product Description (fr-CA):\n${aiProductDescriptionResponse_fr_CA}`);
const aiTopLevelCategoryChoice = await askAiTopLevelProductCategoryEvalQuestions(brandName, amazonProductName_en_US);
log('[procure-asin]', `AI chooses to put product within top level Product Category ID: ${aiTopLevelCategoryChoice}, Product Category Name: ${(await getAllTopLevelProductCategories()).filter((category) => category.id === aiTopLevelCategoryChoice)[0].categoryName!['en-US']}`);
const aiSubCategoryQuestion1Answer = await askAiProductSubCategoryEvalQuestionsSet1(aiTopLevelCategoryChoice, brandName, amazonProductName_en_US, features);
log('[procure-asin]', `AI then chooses within the top-level category to ${aiSubCategoryQuestion1Answer?'make a new sub-category':'use an existing sub-category'}.`);
const aiSubCategoryQuestion2Answer = await askAiProductSubCategoryEvalQuestionsSet2(apiResponse, aiTopLevelCategoryChoice, brandName, amazonProductName_en_US, features, aiSubCategoryQuestion1Answer);;
let aiProductCategoriesDto;
if (!aiSubCategoryQuestion1Answer) {
let aiSubCategoryQuestion2AnswerStr = aiSubCategoryQuestion2Answer as string;
log(`AI suggested existing sub-category ${aiSubCategoryQuestion2Answer}`);
aiProductCategoriesDto = await getProductCategoriesByIds(aiSubCategoryQuestion2AnswerStr);
}
else {
let aiSubCategoryQuestion2AnswerObj = aiSubCategoryQuestion2Answer as {
categoryName: Localized<string>,
description: Localized<string>,
parentCategory: NonLocalized<string[]>,
};
log(`AI suggested new sub-category ${aiSubCategoryQuestion2AnswerObj.categoryName['en-US']}`);
aiProductCategoriesDto = await getAddNewProductSubCategoryDto({ iv: [aiTopLevelCategoryChoice] }, aiSubCategoryQuestion2AnswerObj.categoryName, aiSubCategoryQuestion2AnswerObj.description);
}
let aiProductTags_en_US = await generateAITagsForProduct(amazonProductName_en_US, features);
let aiProductTags_es_US = await translateTags_from_en_US_to_es_US(aiProductTags_en_US);
let aiProductTags_fr_CA = await translateTags_from_en_US_to_fr_CA(aiProductTags_en_US);
let product: Product = {
brand: { iv: [brandsDto.items![0].id!] },
categories: { iv: aiProductCategoriesDto.items.map(category => category.id) },
productName: {
"en-US": amazonProductName_en_US,
"es-US": amazonProductName_es_US,
"fr-CA": amazonProductName_fr_CA,
},
slug: {
"en-US": `${brandsDto.items![0].data?.slug['en-US']}/${slugify(trimPeriods(removeQuotes(amazonProductName_en_US)), { lower: true, trim: true })}`,
"es-US": `${brandsDto.items![0].data?.slug['es-US']}/${slugify(trimPeriods(removeQuotes(amazonProductName_es_US)), { lower: true, trim: true })}`,
"fr-CA": `${brandsDto.items![0].data?.slug['fr-CA']}/${slugify(trimPeriods(removeQuotes(amazonProductName_fr_CA)), { lower: true, trim: true })}`,
},
description: {
"en-US": aiProductDescriptionResponse_en_US,
"es-US": aiProductDescriptionResponse_es_US,
"fr-CA": aiProductDescriptionResponse_fr_CA,
},
tags: {
"en-US": aiProductTags_en_US,
"es-US": aiProductTags_es_US,
"fr-CA": aiProductTags_fr_CA,
},
marketplaceConnections: {
iv: [
{
schemaId: schemaProductMarketplaceConnectionDto.id,
marketplace: [amazonMarketplaceDto.items[0].id],
connection: {
schemaId: schemaAmazonMarketplaceConnectionDto.id,
asin,
siteStripeUrl: amazonItem.DetailPageURL,
} as AmazonMarketplaceConnection | MarketplaceConnection
}
]
}
};
log(`New product to store:`, product);
let productsDto = await getAddNewProductDtoByProduct(product);
log(`Product Id: ${productsDto.items[0].id}, Product Name: ${productsDto.items[0].data?.productName['en-US']} created in database.`);
let listing: Listing = {
marketplace: { iv: [ amazonMarketplaceDto.items[0].id ] },
product: { iv: [ productsDto.items[0].id ]},
marketplaceDescription: {
"en-US": `${features.map((feature) => `- ${feature}`).join('\n')}\n`,
"es-US": await translateAmazonDescription_from_en_US_to_es_US(brandName, features),
"fr-CA": await translateAmazonDescription_from_en_US_to_fr_CA(brandName, features),
},
marketplaceImages: { iv: amazonAssetsIds },
};
log(`New product listing to store:`, listing);
let listingsDto = await getAddNewProductListingDtoByProduct(listing);
log(`Product Listing Id: ${listingsDto.items[0].id}, Product Id: ${listingsDto.items[0].data?.product.iv[0]}, Marketplace Id: ${listingsDto.items[0].data?.marketplace.iv[0]} created in database.`);
amazonItem.Offers?.Listings?.forEach(async (amazonListing) => {
log(`Amazon PA API Response contains Seller ${amazonListing.MerchantInfo?.Name} offer for ${amazonListing.Price?.Amount} in ${amazonListing.Condition?.Value} condition. Checking database for seller...`);
let sellersDto = await getSellerDtoByName(amazonListing.MerchantInfo!.Name!);
if (!sellersDto.items.length) {
log(`Seller not found in database. Listing requires creation of seller first.`);
sellersDto = await getAddNewSellerDtoByName(amazonListing.MerchantInfo!.Name!);
log(`Seller Id: ${sellersDto.items[0].id}, Seller Name: ${sellersDto.items[0].data?.sellerName['en-US']} created in database.`);
} else {
log(`Matching Seller ID: ${sellersDto.items[0].id}, Seller Name: ${sellersDto.items[0].data?.sellerName['en-US']}.`);
}
let offer: Offer = {
offerDate: { iv: new Date().toISOString() },
listing: { iv: [ listingsDto.items[0].id ] },
seller: { iv: [ sellersDto.items[0].id ] },
newPrice: { iv: null },
usedPrice: { iv: null },
};
if (amazonListing.Condition?.Value === 'New') {
offer.newPrice = { iv: amazonListing.Price?.Amount!};
offer.usedPrice = { iv: null };
}
else if (amazonListing.Condition?.Value === 'Used') {
offer.newPrice = { iv: null };
offer.usedPrice = { iv: amazonListing.Price?.Amount! }
}
log('Generated Offer:\n', offer);
let offersDto = await getAddNewOfferDto(offer);
});
await performSyncLocalizedSlugs(log);
});
}
});
command.configureHelp();
return command;
}

View File

@ -0,0 +1,17 @@
import type { Command } from "commander";
import { amazonAppendImagesCommand } from "./amazon-append-images";
import { amazonCrawleeProductsCommand } from "./amazon-crawlee-products";
import { amazonProcureASINsCommand } from "./amazon-procure-asins";
import { amazonGetItemsCommand } from "./get-items/get-items";
const COMMAND_NAME = 'amazon';
export const amazonCommand = (program: Command) => {
const amazonCommand = program.command(COMMAND_NAME).description('Amazon integration commands');
amazonAppendImagesCommand(amazonCommand);
amazonCrawleeProductsCommand(amazonCommand);
amazonGetItemsCommand(amazonCommand);
amazonProcureASINsCommand(amazonCommand);
// TODO: when I get back I need to build a command that lets me replay the procure-asins previous API request
return amazonCommand;
}

View File

@ -0,0 +1,28 @@
import type { Command } from "commander";
import * as core from '../../../../data/core/client'
import { getProductsUsingJsonQuery } from "../../../../data/api-client";
import type { AmazonMarketplaceConnection } from "../../../../data/models/components/AmazonMarketplaceConnection";
import { logForCommand } from "../../common/console";
import { SCHEMAS } from "../../../../data/models/schemas";
import type { AmazonGetItem } from "../../../../data/models/multis/AmazonGetItem";
import { DateTime } from "luxon";
export const COMMAND_NAME = 'logs';
const log = console.log;
export const amazonGetItemsLogsCommand = (amazonGetItemsCommand: Command) => {
const command = amazonGetItemsCommand.command(COMMAND_NAME).alias('log')
.description('Prints the get-items JSON response')
.argument('<log-ids...>')
.action(async (logIds: string[]) => {
for (let logId of logIds) {
let logDto = await core.getContentsByIds<AmazonGetItem>(SCHEMAS.AMAZON_GET_ITEMS, logId);
logDto.items.forEach((logDto, index) => {
log(JSON.stringify(logDto.data?.apiResponse, null, 2));
});
}
});
command.configureHelp();
return command;
}

View File

@ -0,0 +1,27 @@
import type { Command } from "commander";
import * as core from '../../../../data/core/client'
import { getProductsUsingJsonQuery } from "../../../../data/api-client";
import type { AmazonMarketplaceConnection } from "../../../../data/models/components/AmazonMarketplaceConnection";
import { logForCommand } from "../../common/console";
import { SCHEMAS } from "../../../../data/models/schemas";
import type { AmazonGetItem } from "../../../../data/models/multis/AmazonGetItem";
import { DateTime } from "luxon";
export const COMMAND_NAME = 'ls';
const log = logForCommand(COMMAND_NAME);
export const amazonGetItemsLsCommand = (amazonGetItemsCommand: Command) => {
const command = amazonGetItemsCommand.command(COMMAND_NAME).alias('list')
.description('List all the get-items logs')
.action(async (args: string[]) => {
let logsDto = await core.getContentsUsingJsonQuery<AmazonGetItem>(SCHEMAS.AMAZON_GET_ITEMS);
logsDto.items.forEach((logDto, index) => {
const logEach = logForCommand(`${COMMAND_NAME}:${index+1}`);
logEach(`ID: ${logDto.id} Request Date: ${DateTime.fromISO(logDto.data?.requestDate.iv!).toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}`)
});
log(`Returned ${logsDto.items.length} get-items logs.`)
});
command.configureHelp();
return command;
}

View File

@ -0,0 +1,12 @@
import type { Command } from "commander";
import { amazonGetItemsLsCommand } from "./amazon-get-items-ls";
import { amazonGetItemsLogsCommand } from "./amazon-get-items-logs";
const COMMAND_NAME = 'get-items';
export const amazonGetItemsCommand = (program: Command) => {
const amazonGetItemsCommand = program.command(COMMAND_NAME).alias('get-item').description('Amazon Get Items commands');
amazonGetItemsLsCommand(amazonGetItemsCommand);
amazonGetItemsLogsCommand(amazonGetItemsCommand);
return amazonGetItemsCommand;
}

View File

@ -0,0 +1,10 @@
import { program } from 'commander';
import { amazonCommand } from './amazon/amazon';
import { productsCommand } from './products/products';
import { siteCommands } from './site/site';
const amazon = amazonCommand(program);
const products = productsCommand(program);
const site = siteCommands(program);
program.parse();

View File

@ -1,25 +1,25 @@
import type { AmazonGetItem } from '../../data/models/multis/AmazonGetItem.ts';
import type { Brand } from '../../data/models/multis/Brand.ts';
import type { ContentsDto } from '../../data/internals/ContentsDtoT.ts';
import type { AmazonGetItem } from '../../../data/models/multis/AmazonGetItem.ts';
import type { Brand } from '../../../data/models/multis/Brand.ts';
import type { ContentsDto } from '../../../data/internals/ContentsDtoT.ts';
import type { GetItemsResponse, Item } from 'amazon-pa-api5-node-ts';
import type { Listing } from '../../data/models/multis/Listing.ts';
import type { Multilingual } from '../../data/internals/MultilingualT.ts';
import type { NonMultilingual } from '../../data/internals/NonMultilingualT.ts';
import type { Product } from '../../data/models/multis/Product.ts';
import type { ProductCategory } from '../../data/models/multis/ProductCategory.ts';
import type { Seller } from '../../data/models/multis/Seller.ts';
import type { Listing } from '../../../data/models/multis/Listing.ts';
import type { Localized } from '../../../data/internals/LocalizedT.ts';
import type { NonLocalized } from '../../../data/internals/NonLocalizedT.ts';
import type { Product } from '../../../data/models/multis/Product.ts';
import type { ProductCategory } from '../../../data/models/multis/ProductCategory.ts';
import type { Seller } from '../../../data/models/multis/Seller.ts';
import { arrayBuffer } from 'stream/consumers';
import { client, getContentsByIds, TIMEOUT_IN_SECONDS } from '../../data/core/client.ts';
import { client, getContentsByIds, TIMEOUT_IN_SECONDS } from '../../../data/core/client.ts';
import { createReadStream, type ReadStream } from 'fs';
import { getItems } from '../../apiclient/amazon.ts';
import { getBrandsUsingJsonQuery, getMarketplacesUsingJsonQuery, getProductCategoriesUsingJsonQuery, getProductsUsingJsonQuery, getSellersUsingJsonQuery } from '../../data/api-client.ts';
import { getItems } from '../../../apiclient/amazon.ts';
import { getBrandsUsingJsonQuery, getMarketplacesUsingJsonQuery, getProductCategoriesUsingJsonQuery, getProductsUsingJsonQuery, getSellersUsingJsonQuery } from '../../../data/api-client.ts';
import { memfs } from 'memfs';
import { response } from '../../old-data/brands/first-aid-only-example-query-getitems.ts';
import { SCHEMAS } from '../../data/models/schemas.ts';
import { response } from '../../../old-data/brands/first-aid-only-example-query-getitems.ts';
import { SCHEMAS } from '../../../data/models/schemas.ts';
import axios from 'axios';
import ollama from 'ollama';
import slugify from 'slugify';
import type { Offer } from '../../data/models/multis/Offer.ts';
import type { Offer } from '../../../data/models/multis/Offer.ts';
export function isValidASIN(asinOrNot: string) {
return asinOrNot.match(/[A-Z0-9]{10}/g) ? true : false;
@ -346,9 +346,9 @@ export async function designAiProductSubCategoryEvalQuestionsSet2(response: GetI
}
export async function askAiProductSubCategoryEvalQuestionsSet2(response: GetItemsResponse, parentProductCategoryId: string, brandName: string, productName_en_US: string, features: string[], shouldCreateNewProductCategory: boolean): Promise<{
categoryName: Multilingual<string>,
description: Multilingual<string>,
parentCategory: NonMultilingual<string[]>,
categoryName: Localized<string>,
description: Localized<string>,
parentCategory: NonLocalized<string[]>,
}|string> {
const aiSubCategoryEvalQuestion2 = await designAiProductSubCategoryEvalQuestionsSet2(response, parentProductCategoryId, brandName, productName_en_US, features, shouldCreateNewProductCategory);
console.log(`>>>${aiSubCategoryEvalQuestion2}`);
@ -358,8 +358,8 @@ export async function askAiProductSubCategoryEvalQuestionsSet2(response: GetItem
})).message.content;
console.log(`llama3.1>${answer}`);
const aiSubCategoryEvalQuestion2Answer = JSON.parse(answer) as {
categoryName: Multilingual<string>,
description: Multilingual<string>,
categoryName: Localized<string>,
description: Localized<string>,
parentCategory: string[],
}|string;
if (typeof aiSubCategoryEvalQuestion2Answer === 'string') {
@ -406,7 +406,7 @@ export async function translateTags_from_en_US_to_fr_CA(tags_en_US: string[]) {
return (JSON.parse(answer) as string[]).map(a => a.trim().toLowerCase());
}
export async function getAddNewProductSubCategoryDto(parentProductCategoryId: NonMultilingual<string[]>, categoryName: Multilingual<string>, description: Multilingual<string>) {
export async function getAddNewProductSubCategoryDto(parentProductCategoryId: NonLocalized<string[]>, categoryName: Localized<string>, description: Localized<string>) {
let productCategoryDto = await client.contents.postContent(SCHEMAS.PRODUCT_CATEGORIES, {
publish: false,
body: {

View File

@ -0,0 +1,9 @@
/**
* Returns a log function which upon calling will prepend the name of the command to the output. You can specify
* any log function, or if you don't specify one it will choose `console.log`.
* @param commandName Name of command to prepend in the logs.
* @param logFn The log function. If you don't provide a log function, it will automatically choose `console.log`.
* @returns A log function which upon calling it will prepend the command name to each output line.
*/
export const logForCommand = (commandName: string, logFn: ((...args: any[]) => void) = console.log) =>
(...args: any[]) => logFn.apply(null, [`[${commandName}]`, ...args]);

View File

@ -0,0 +1,31 @@
import type { Command } from "commander";
import { getProductsUsingJsonQuery } from "../../../data/api-client";
import type { AmazonMarketplaceConnection } from "../../../data/models/components/AmazonMarketplaceConnection";
import { logForCommand } from "../common/console";
export const COMMAND_NAME = 'ls';
const log = logForCommand(COMMAND_NAME);
export const productsLsCommand = (productsCommands: Command) => {
const command = productsCommands.command(COMMAND_NAME).alias('list')
.description('List all the products')
.action(async (args: string[]) => {
let productsDto = await getProductsUsingJsonQuery();
productsDto.items.forEach((productDto, index) => {
const logEach = logForCommand(`${COMMAND_NAME}:${index+1}`);
logEach(`ID: ${productDto.id}`)
logEach(`Product (en-US): ${productDto.data?.productName['en-US']}`);
logEach(`Product (es-US): ${productDto.data?.productName['es-US']}`);
logEach(`Product (fr-CA): ${productDto.data?.productName['fr-CA']}`);
let asin = productDto.data?.marketplaceConnections.iv.map((connection) => (connection.connection as AmazonMarketplaceConnection).asin).join('');
let siteStripUrl = productDto.data?.marketplaceConnections.iv.map((connection) => (connection.connection as AmazonMarketplaceConnection).siteStripeUrl).join('');
logEach(`ASIN: ${asin} ${!!(asin||'').match(/[A-Z0-9]{10}/g) ? 'Is a valid ASIN.' : 'Is not a valid ASIN.'}`)
logEach(`Amazon SiteStripe URL: ${siteStripUrl}`)
console.log();
});
log(`Returned ${productsDto.items.length} products.`)
});
command.configureHelp();
return command;
}

View File

@ -0,0 +1,10 @@
import type { Command } from "commander";
import { productsLsCommand } from "./products-ls";
export const COMMAND_NAME = 'products';
export const productsCommand = (program: Command) => {
const productsCommand = program.command(COMMAND_NAME).description('Products commands');
productsLsCommand(productsCommand);
return productsCommand;
}

View File

@ -0,0 +1,16 @@
import type { Command } from "commander";
import { getProductsUsingJsonQuery, performSyncLocalizedSlugs } from "../../../data/api-client";
import type { AmazonMarketplaceConnection } from "../../../data/models/components/AmazonMarketplaceConnection";
import { logForCommand } from "../common/console";
export const COMMAND_NAME = 'sync-slugs';
const log = logForCommand(COMMAND_NAME);
export const siteSyncSlugsCommand = (siteCommands: Command) =>
siteCommands.command(COMMAND_NAME)
.description('Sync URL slugs for each frontend endpoint.')
.action(async (asin: string, args: string[]) => {
await performSyncLocalizedSlugs(log);
})
.configureHelp();

View File

@ -0,0 +1,10 @@
import type { Command } from "commander";
import { siteSyncSlugsCommand } from "./site-sync-slugs";
export const COMMAND_NAME = 'site';
export const siteCommands = (program: Command) => {
const siteCommands = program.command(COMMAND_NAME).description('Site commands');
siteSyncSlugsCommand(siteCommands);
return siteCommands;
}

View File

@ -1,13 +1,14 @@
---
interface Banner {
editToken?: string,
import type { SquidexEditable } from './SharedProperties.js';
interface Banner extends SquidexEditable {
homePageLink: string,
siteName: string,
}
const { editToken, homePageLink, siteName } = Astro.props;
---
<h1 class="center" data-squidex-token={editToken||''}><a href={homePageLink}><span class="text-gradient">{siteName}</span></a></h1>
<h1 class="center" data-squidex-token={editToken}><a href={homePageLink}><span class="text-gradient">{siteName}</span></a></h1>
<style>
h1 {

View File

@ -1,13 +1,12 @@
---
import type { SquidexEditable, Multilingual } from './SharedProperties.js';
import type { Brand } from "../data/models/multis/Brand";
import { getAssetById, getLocaleField } from "../data/api-client";
import path from "node:path";
import { renderMarkdown } from "../lib/rendering";
interface Props {
interface Props extends Multilingual, SquidexEditable {
brand: Brand,
editToken?: string,
locale: string,
}
const { brand, editToken, locale } = Astro.props;
@ -22,7 +21,7 @@ let brandLogoImage = path.posix.join('/img', (await getAssetById(brand.logoImage
.join('/'));
---
<div class="brand" data-squidex-token={editToken||''}>
<div class="brand" data-squidex-token={editToken}>
<div class="flex">
{ brandLogoImage &&
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />

View File

@ -2,11 +2,10 @@
import type { Brand } from "../data/models/multis/Brand";
import { getAssetById } from "../data/api-client";
import path from "node:path";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
brand: Brand,
editToken?: string,
locale: string,
}
const { brand, editToken, locale } = Astro.props;
@ -25,7 +24,7 @@ let brandLogoImage = path.posix.join('/img', (await getAssetById(brand.logoImage
.join('/'));
---
<li class="brand-card" data-squidex-token={editToken||''}>
<li class="brand-card" data-squidex-token={editToken}>
<a href={`/${brand.slug[locale]}/`}>
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />
</a>

View File

@ -1,13 +1,14 @@
---
export interface Breadcrumb {
import type { SquidexEditable } from './SharedProperties';
export interface Breadcrumb extends SquidexEditable {
text: string;
url: string;
gradient?: boolean;
editToken?: string;
}
interface Props {
editToken?: string;
interface Props extends SquidexEditable {
breadcrumbs: Breadcrumb[];
}
@ -15,19 +16,19 @@ const { breadcrumbs, editToken } = Astro.props;
---
{breadcrumbs &&
breadcrumbs.length &&
<nav class="breadcrumbs" aria-label="breadcrumb" data-squidex-token={editToken||''}>
<nav class="breadcrumbs" aria-label="breadcrumb" data-squidex-token={editToken}>
<ol class="breadcrumb">
{breadcrumbs.map((breadcrumb: Breadcrumb, index: number, breadcrumbs: Breadcrumb[]) => {
let isLastCrumb = ((index + 1) === breadcrumbs.length);
if (isLastCrumb) {
//the last crumb doesn't get a link
return (
<li class="breadcrumb-item active" aria-current="page" data-squidex-token={breadcrumb.editToken||''}>{breadcrumb.text}</li>
<li class="breadcrumb-item active" aria-current="page" data-squidex-token={breadcrumb.editToken}>{breadcrumb.text}</li>
);
}
else {
return (
<li class="breadcrumb-item" data-squidex-token={breadcrumb.editToken||''}>
<li class="breadcrumb-item" data-squidex-token={breadcrumb.editToken}>
<a href={breadcrumb.url}>{(breadcrumb.gradient &&
<span class="text-gradient">{breadcrumb.text}</span>)
||

View File

@ -1,13 +1,14 @@
---
interface Callout {
editToken?: string,
import type { SquidexEditable } from "./SharedProperties";
interface Callout extends SquidexEditable {
text: string,
}
const { editToken, text } = Astro.props;
---
<div class="callout" data-squidex-token={editToken||''} set:html={text}></div>
<div class="callout" data-squidex-token={editToken} set:html={text}></div>
<style>
.callout {

View File

@ -5,6 +5,8 @@ import type { Component } from "../data/internals/Component";
import type { Marketplace } from "../data/models/multis/Marketplace";
import type { Page } from "../data/models/multis/Page";
import type { PageBrand } from "../data/models/components/PageBrand";
import type { PageBrandList } from "../data/models/components/PageBrandList";
import type { PageBrandSlots } from "../data/models/components/PageBrandSlots";
import type { PageProduct } from "../data/models/components/PageProduct";
import type { PageCallout } from "../data/models/components/PageCallout";
import type { PageContent } from "../data/models/components/PageContent";
@ -14,8 +16,8 @@ import type { ProductCategory } from "../data/models/multis/ProductCategory";
import type { Seller } from "../data/models/multis/Seller";
import type { Site } from "../data/models/singles/Site";
import type { QueryComponent } from "../data/models/components/QueryComponent";
import { SupportedLocales } from "../data/internals/MultilingualT";
import { getBrandsByIds, getBrandsUsingJsonQuery, getMarketplacesByIds, getMarketplacesUsingJsonQuery, getPagesByIds, getProductCategoriesByIds, getProductCategoriesUsingJsonQuery, getProductsByIds, getProductsUsingJsonQuery, getSellersByIds, getSellersUsingJsonQuery } from '../data/api-client';
import { SupportedLocales } from "../data/internals/LocalizedT";
import { getBrandsByIds, getBrandSlotsByIds, getBrandsUsingJsonQuery, getMarketplacesByIds, getMarketplacesUsingJsonQuery, getPagesByIds, getProductCategoriesByIds, getProductCategoriesUsingJsonQuery, getProductsByIds, getProductsUsingJsonQuery, getSellersByIds, getSellersUsingJsonQuery } from '../data/api-client';
import BrandComponent from './Brand.astro';
import ProductComponent from './Product.astro';
import Breadcrumbs, { type Breadcrumb } from './Breadcrumbs.astro';
@ -171,6 +173,53 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
})}
</ul>
);
case 'page-brands-list':
let brandsList = dynComponent as PageBrandList;
let brandsListDto = (await getBrandsByIds(brandsList.brands.join(',')));
let listedBrands = brandsListDto!.items;
let listedBrandsHeading: { [key: string]: string } = {
'en-US': "Brands",
'es-US': "Marcas",
'fr-CA': "Marques",
};
return (
listedBrands.length > 0 && <h3 class="section">{listedBrandsHeading[locale]}</h3>
<ul class="link-card-grid brands" data-squidex-token={pageEditToken}>
{listedBrands.sort((a, b) => a.data!.brandName[locale].localeCompare(b.data!.brandName[locale])).map(brandDto => {
return (
<BrandCard
locale={locale}
editToken={brandDto.editToken}
brand={brandDto.data!}
/>
);
})}
</ul>
);
case 'page-brands-slots':
let brandsSlots = dynComponent as PageBrandSlots;
let brandsSlotsDto = (await getBrandSlotsByIds(brandsSlots.brandSlots.join(',')));
let brandsSlotsListDto = (await getBrandsByIds(brandsSlotsDto.items[0].data!.brands.iv.join(',')));
let listedBrandSlots = brandsSlotsListDto!.items;
let listedBrandSlotsHeading: { [key: string]: string } = {
'en-US': "Brands",
'es-US': "Marcas",
'fr-CA': "Marques",
};
return (
listedBrandSlots.length > 0 && <h3 class="section">{listedBrandSlotsHeading[locale]}</h3>
<ul class="link-card-grid brands" data-squidex-token={pageEditToken}>
{listedBrandSlots.sort((a, b) => a.data!.brandName[locale].localeCompare(b.data!.brandName[locale])).map(brandDto => {
return (
<BrandCard
locale={locale}
editToken={brandDto.editToken}
brand={brandDto.data!}
/>
);
})}
</ul>
);
case 'page-brand':
let brandComponent = dynComponent as PageBrand;
let brandId = brandComponent.brand ? brandComponent.brand[0] : '';
@ -259,7 +308,7 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
let editToken = productCategoryDto.editToken;
return (
<ProductCategoryCard
editToken={editToken||''}
editToken={editToken}
locale={locale}
productCategory={productCategoryDto.data!}
/>
@ -284,7 +333,7 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
let editToken = productDto.editToken;
return (
<ProductCard
editToken={editToken||''}
editToken={editToken}
locale={locale}
productDto={productDto}
/>

View File

@ -1,6 +1,7 @@
---
interface Content {
editToken: string,
import type { SquidexEditable } from "./SharedProperties";
interface Content extends SquidexEditable {
text: string,
}

View File

@ -2,8 +2,9 @@
import type { Marketplace } from "../data/models/multis/Marketplace";
import { getMarketplacesUsingJsonQuery } from "../data/api-client";
import { renderMarkdown } from "../lib/rendering";
import type { Multilingual } from "./SharedProperties";
interface Props {
interface Props extends Multilingual {
locale: string,
}

View File

@ -3,11 +3,10 @@ import type { Marketplace } from "../data/models/multis/Marketplace";
import { getAssetById, getLocaleField } from "../data/api-client";
import path from "node:path";
import { renderMarkdown } from "../lib/rendering";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
marketplace: Marketplace,
editToken?: string,
locale: string,
}
const { marketplace, editToken, locale } = Astro.props;
@ -23,7 +22,7 @@ let marketplaceLogoImage = path.posix.join('/img',
.join('/'));
---
<div class="marketplace" data-squidex-token={editToken||''}>
<div class="marketplace" data-squidex-token={editToken}>
<div class="flex">
{ marketplaceLogoImage &&
marketplaceLogoImageAsset.metadata['background-color']
@ -46,7 +45,7 @@ let marketplaceLogoImage = path.posix.join('/img',
</div>
</div>
</div>
<div class="marketplace-dark" data-squidex-token={editToken||''}>
<div class="marketplace-dark" data-squidex-token={editToken}>
<div class="flex">
{marketplace.longDescription && marketplace.longDescription[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(marketplace.longDescription[locale]||'')} /></div> }
</div>

View File

@ -2,11 +2,10 @@
import path from "node:path";
import type { Marketplace } from "../data/models/multis/Marketplace";
import { getAssetById } from "../data/api-client";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
editToken?: string,
interface Props extends Multilingual, SquidexEditable {
marketplace: Marketplace,
locale: string,
}
const { editToken, marketplace, locale } = Astro.props;

View File

@ -1,6 +1,6 @@
---
import type { ContentsDto } from "../data/internals/ContentsDtoT";
import type { Multilingual } from "../data/internals/MultilingualT";
import type { Localized } from "../data/internals/LocalizedT";
import type { Product } from "../data/models/multis/Product";
import { getAssetById, getBrandsByIds, getMarketplacesByIds, getOffersByListingId } from "../data/api-client";
import path from "node:path";
@ -14,11 +14,10 @@ import ImageCarousel from "./ImageCarousel.astro";
import type { AmazonMarketplaceConnection } from "../data/models/components/AmazonMarketplaceConnection";
import { getSellersByIds } from "../data/api-client";
import { DateTime } from "luxon";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
productDto: ContentsDto<Product>,
editToken?: string,
locale: string,
}
let category={ } as unknown as any;let site={ } as unknown as any
const formatAsCurrency = (amount: number) => amount.toLocaleString(locale, { style: 'currency', currency: 'USD' });
@ -68,7 +67,7 @@ for (let listingDto of listingsDto.items) {
}
}
let i18n: { [key: string]: Multilingual<string> } = {
let i18n: { [key: string]: Localized<string> } = {
'Brand:': {
'en-US': "Brand:",
'es-US': "Marca :",

View File

@ -12,11 +12,10 @@ import { renderMarkdown } from "../lib/rendering";
import type { Marketplace } from "../data/models/multis/Marketplace";
import { getAssetById } from "../data/api-client";
import path from "node:path";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
productDto?: ContentDto<Product>,
locale: string,
editToken?: string,
}
const { productDto, locale, editToken } = Astro.props;

View File

@ -4,11 +4,10 @@ import { getAssetById, getLocaleField } from "../data/api-client";
import path from "node:path";
import { renderMarkdown } from "../lib/rendering";
import Callout from "./Callout.astro";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
productCategory: ProductCategory,
editToken?: string,
locale: string,
}
const { productCategory, editToken, locale } = Astro.props;
@ -24,7 +23,7 @@ let productCategoryImage = path.posix.join('/img',
.join('/'));
---
<div class="product-category" data-squidex-token={editToken||''}>
<div class="product-category" data-squidex-token={editToken}>
<div class="cover-image"></div>
<div class="content center">
<div class="flex-right">

View File

@ -3,12 +3,11 @@ import type { ProductCategory } from "../data/models/multis/ProductCategory";
import { getAssetById } from '../data/api-client';
import path from 'node:path';
import { renderMarkdown } from '../lib/rendering';
import type { Multilingual, SquidexEditable } from "./SharedProperties";
// import { BlocksRenderer, type BlocksContent } from '@strapi/blocks-react-renderer';
interface Props {
editToken: string,
interface Props extends Multilingual, SquidexEditable {
productCategory: ProductCategory
locale: string,
}
const { editToken, productCategory, locale } = Astro.props;

View File

@ -3,11 +3,10 @@ import type { Seller } from "../data/models/multis/Seller";
import { getAssetById, getLocaleField } from "../data/api-client";
import path from "node:path";
import { renderMarkdown } from "../lib/rendering";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
seller: Seller,
editToken?: string,
locale: string,
}
const { seller, editToken, locale } = Astro.props;
@ -23,7 +22,7 @@ let sellerLogoImage = sellerLogoImageAsset ? path.posix.join('/img',
.join('/')) : '';
---
<div class="seller" data-squidex-token={editToken||''}>
<div class="seller" data-squidex-token={editToken}>
<div class="flex">
{ sellerLogoImage ?
( sellerLogoImage &&
@ -45,7 +44,7 @@ let sellerLogoImage = sellerLogoImageAsset ? path.posix.join('/img',
}
</div>
</div>
<div class="seller-dark" data-squidex-token={editToken||''}>
<div class="seller-dark" data-squidex-token={editToken}>
<div class="flex">
{seller.sellerBio && seller.sellerBio[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(seller.sellerBio[locale]||'')} /></div> }
</div>

View File

@ -2,8 +2,9 @@
import path from "node:path";
import type { Seller } from "../data/models/multis/Seller";
import { getAssetById } from "../data/api-client";
import type { Multilingual, SquidexEditable } from "./SharedProperties";
interface Props {
interface Props extends Multilingual, SquidexEditable {
editToken?: string,
seller: Seller,
locale: string,

View File

@ -0,0 +1,20 @@
import { getAssetById } from "../data/api-client";
import path from "node:path";
export interface SquidexEditable {
editToken?: string,
}
export interface Multilingual {
locale: string,
}
export const imageUrlForAssetId = async (assetId: string) =>
path.posix.join('/img', (await getAssetById(assetId))
.links['content']
.href
.split('/')
.reverse()
.filter((_value, index, array) => index < (array.length - index - 2))
.reverse()
.join('/'));

3
src/components/TODOS.md Normal file
View File

@ -0,0 +1,3 @@
# Some things I need to do
1. Fix all these components so that all the logic is removed from the component. Need to pass in variables that are processed ahead of the Component.

View File

@ -1,7 +1,7 @@
import * as core from "./core/client";
import { SCHEMAS } from "./models/schemas";
import { getContents } from "./core/client.js";
import { SupportedLocales, type Multilingual } from "./internals/MultilingualT";
import { SupportedLocales, type Localized } from "./internals/LocalizedT.js";
import type { Component } from "./internals/Component";
import type { Brand } from "./models/multis/Brand";
import type { Page } from "./models/multis/Page";
@ -12,16 +12,18 @@ import type { ProductCategory } from "./models/multis/ProductCategory";
import type { Product } from "./models/multis/Product";
import type { Slug } from "./models/multis/Slug";
import type { Seller } from "./models/multis/Seller";
import type { NonMultilingual } from "./internals/NonMultilingualT";
import type { NonLocalized } from "./internals/NonLocalizedT.js";
import type { ContentsDto } from "./internals/ContentsDtoT";
import type { ContentData } from "@squidex/squidex/api/types/ContentData";
import type { ContentDto } from "./internals/ContentDtoT";
import type { Listing } from "./models/multis/Listing.js";
import type { Offer } from "./models/multis/Offer.js";
import type { Redirect } from "./models/multis/Redirect.js";
import type { BrandSlots } from "./models/components/BrandSlots.js";
/** Generic helpers */
export const getLocaleField = function <T>(locale: SupportedLocales|string, field: Multilingual<T>) {
export const getLocaleField = function <T>(locale: SupportedLocales|string, field: Localized<T>) {
if (field && field[locale.toString()])
return field[locale.toString()];
}
@ -45,6 +47,17 @@ export const getBrandsByLangSlug = async (forLang: SupportedLocales|string, slug
export const getBrandsUsingJsonQuery = async (jsonQuery: string|undefined = undefined) =>
await core.getContentsUsingJsonQuery<Brand>(SCHEMAS.BRANDS, jsonQuery);
/** Brands handlers */
export const getBrandSlotsByIds = async (ids: string) =>
await core.getContentsByIds<BrandSlots>(SCHEMAS.BRANDS_SLOTS, ids);
export const getBrandSlotsByLangSlug = async (forLang: SupportedLocales|string, slug: string) =>
await core.getContentsByLangSlug<BrandSlots>(SCHEMAS.BRANDS_SLOTS, forLang, slug);
export const getBrandSlotsUsingJsonQuery = async (jsonQuery: string|undefined = undefined) =>
await core.getContentsUsingJsonQuery<BrandSlots>(SCHEMAS.BRANDS_SLOTS, jsonQuery);
/** Marketplaces handlers */
export const getMarketplacesByIds = async (ids: string) =>
@ -115,7 +128,16 @@ export const getOffersByListingId = async (listingId: string) =>
filter: {
path: "data.listing.iv",
op: "eq",
value: listingId
value: listingId,
}
}));
export const getRedirectsByPreviousSlug = async (prevSlug: string) =>
await core.getContentsUsingJsonQuery<Redirect>(SCHEMAS.REDIRECTS, JSON.stringify({
filter: {
path: "data.prevSlug.iv",
op: "eq",
value: prevSlug,
}
}));
@ -152,7 +174,7 @@ export const getSiteConfig = async () =>
await getContents<SiteConfig>(SCHEMAS.SITE_CONFIG);
export async function performSyncLocalizedSlugs(logFn = console.log) {
logFn("[sync-slugs] Begin sync localized slugs.")
logFn("Begin sync localized slugs.")
let allSlugs = await getAllSlugs();
let allPages = await core.getContentsUsingJsonQuery<Page>(SCHEMAS.PAGES);
let allBrands = await core.getContentsUsingJsonQuery<Brand>(SCHEMAS.BRANDS);
@ -179,7 +201,7 @@ export async function performSyncLocalizedSlugs(logFn = console.log) {
}
}
}
const findSlugInSlugs = function(locale: SupportedLocales|string, slug: Multilingual<string>, schema: SCHEMAS|string, referenceId: string) {
const findSlugInSlugs = function(locale: SupportedLocales|string, slug: Localized<string>, schema: SCHEMAS|string, referenceId: string) {
for (let i = 0; i < allSlugs.items.length; i++) {
let testSlug = allSlugs.items[i].data!;
if (testSlug.localizedSlug.iv === slug[locale]
@ -298,13 +320,13 @@ export async function performSyncLocalizedSlugs(logFn = console.log) {
}
});
const MAX_TIME_TO_POST_SLUGS = 60;//s
logFn("[sync-slugs] Add", batchAddSlugsQueue.length, "slugs");
logFn("Add", batchAddSlugsQueue.length, "slugs");
let bulkAddResult = await core.client.contents.postContents(SCHEMAS.SLUGS, { datas: batchAddSlugsQueue as unknown as ContentData[], publish: true }, { timeoutInSeconds: MAX_TIME_TO_POST_SLUGS });
logFn("[sync-slugs] Remove by id", batchRemoveSlugsQueue.length, "slugs");
logFn("Remove by id", batchRemoveSlugsQueue.length, "slugs");
batchRemoveSlugsQueue.forEach(async (removeId) => {
await core.client.contents.deleteContent(SCHEMAS.SLUGS, removeId)
})
logFn("[sync-slugs] Finish sync localized slugs.")
logFn("Finish sync localized slugs.")
}
export class AmazonPAApiSyncClient {

View File

@ -1,7 +1,7 @@
import { config } from "../../config.js";
import { SquidexClient } from "@squidex/squidex";
import type { ContentsDto } from "../internals/ContentsDtoT.js";
import type { SupportedLocales } from "../internals/MultilingualT.js";
import type { SupportedLocales } from "../internals/LocalizedT.js";
import type { SCHEMAS } from "../models/schemas.js";
export const client = new SquidexClient({

View File

@ -4,7 +4,7 @@ export enum SupportedLocales {
'fr-CA' = 'fr-CA',
};
export interface Multilingual<T> {
export interface Localized<T> {
[key: string]: T,
'en-US': T,
'es-US': T,

View File

@ -0,0 +1,4 @@
export interface NonLocalized<T> {
[key: string]: T,
iv: T,
}

View File

@ -1,4 +0,0 @@
export interface NonMultilingual<T> {
[key: string]: T,
iv: T,
}

View File

@ -1,4 +1,3 @@
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { MarketplaceConnection } from "./MarketplaceConnection";
export interface AmazonMarketplaceConnection extends MarketplaceConnection {

View File

@ -0,0 +1,7 @@
import type { Component } from "../../internals/Component";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface BrandSlots extends Component {
slotName: NonLocalized<string>;
brands: NonLocalized<string[]>,
}

View File

@ -0,0 +1,5 @@
import type { Component } from "../../internals/Component";
export interface PageBrandList extends Component {
brands: string[],
}

View File

@ -0,0 +1,5 @@
import type { Component } from "../../internals/Component";
export interface PageBrandSlots extends Component {
brandSlots: string[],
}

View File

@ -1,5 +1,4 @@
import type { Component } from "../../internals/Component";
import type { Multilingual } from "../../internals/MultilingualT";
export interface PageCallout extends Component {
text: string,

View File

@ -1,5 +1,4 @@
import type { Component } from "../../internals/Component";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { MarketplaceConnection } from "./MarketplaceConnection";
export interface ProductMarketplaceConnection extends Component {

View File

@ -1,9 +1,9 @@
import type { AmazonPAGetItemsRequest } from "../components/AmazonPAGetItemsRequest";
import type { GetItemsResponse } from "amazon-pa-api5-node-ts";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface AmazonGetItem {
requestDate: NonMultilingual<string>,
getItemsRequest: NonMultilingual<AmazonPAGetItemsRequest>,
apiResponse: NonMultilingual<GetItemsResponse>,
requestDate: NonLocalized<string>,
getItemsRequest: NonLocalized<AmazonPAGetItemsRequest>,
apiResponse: NonLocalized<GetItemsResponse>,
}

View File

@ -1,11 +1,11 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Brand {
brandName: Multilingual<string>,
logoImage?: Multilingual<string>,
slug: Multilingual<string>,
shortDescription?: Multilingual<string>,
longDescription?: Multilingual<string>,
brandPage?: NonMultilingual<string[]>,
brandName: Localized<string>,
logoImage?: Localized<string>,
slug: Localized<string>,
shortDescription?: Localized<string>,
longDescription?: Localized<string>,
brandPage?: NonLocalized<string[]>,
}

View File

@ -1,9 +1,9 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Listing {
product: NonMultilingual<string[]>,
marketplace: NonMultilingual<string[]>,
marketplaceDescription: Multilingual<string>,
marketplaceImages: NonMultilingual<string[]>,
product: NonLocalized<string[]>,
marketplace: NonLocalized<string[]>,
marketplaceDescription: Localized<string>,
marketplaceImages: NonLocalized<string[]>,
}

View File

@ -1,12 +1,12 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Marketplace {
marketplaceName: Multilingual<string>,
slug: Multilingual<string>,
logoImage: Multilingual<string[]>,
marketplacePage: NonMultilingual<string[]>,
disclaimer: Multilingual<string>,
shortDescription: Multilingual<string>,
longDescription: Multilingual<string>,
marketplaceName: Localized<string>,
slug: Localized<string>,
logoImage: Localized<string[]>,
marketplacePage: NonLocalized<string[]>,
disclaimer: Localized<string>,
shortDescription: Localized<string>,
longDescription: Localized<string>,
}

View File

@ -1,9 +1,9 @@
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Offer {
offerDate: NonMultilingual<string>,
seller: NonMultilingual<string[]>,
listing: NonMultilingual<string[]>,
newPrice: NonMultilingual<number|null>,
usedPrice: NonMultilingual<number|null>,
offerDate: NonLocalized<string>,
seller: NonLocalized<string[]>,
listing: NonLocalized<string[]>,
newPrice: NonLocalized<number|null>,
usedPrice: NonLocalized<number|null>,
}

View File

@ -1,12 +1,12 @@
import type { PageSeo } from "../components/PageSeo";
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
import type { Component } from "../../internals/Component";
export interface Page {
title: Multilingual<string>,
slug: Multilingual<string>,
content: Multilingual<Component[]>,
seo: Multilingual<PageSeo>,
parentPage: NonMultilingual<string[]>,
title: Localized<string>,
slug: Localized<string>,
content: Localized<Component[]>,
seo: Localized<PageSeo>,
parentPage: NonLocalized<string[]>,
}

View File

@ -1,14 +1,14 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
import type { ProductMarketplaceConnection } from "../components/ProductMarketplaceConnection";
export interface Product {
productName: Multilingual<string>,
slug: Multilingual<string>,
brand: NonMultilingual<string[]>,
categories: NonMultilingual<string[]>,
tags: Multilingual<string[]>,
description: Multilingual<string>,
marketplaceConnections: NonMultilingual<ProductMarketplaceConnection[]>,
productPage?: NonMultilingual<string[]>,
productName: Localized<string>,
slug: Localized<string>,
brand: NonLocalized<string[]>,
categories: NonLocalized<string[]>,
tags: Localized<string[]>,
description: Localized<string>,
marketplaceConnections: NonLocalized<ProductMarketplaceConnection[]>,
productPage?: NonLocalized<string[]>,
}

View File

@ -1,10 +1,10 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface ProductCategory {
categoryName: Multilingual<string>,
slug: Multilingual<string>,
categoryImage: Multilingual<string>,
description: Multilingual<string>,
parentCategory: NonMultilingual<string[]>,
categoryName: Localized<string>,
slug: Localized<string>,
categoryImage: Localized<string>,
description: Localized<string>,
parentCategory: NonLocalized<string[]>,
}

View File

@ -0,0 +1,6 @@
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Redirect {
prevSlug: NonLocalized<string>,
newContent: NonLocalized<string[]>,
}

View File

@ -1,8 +1,8 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { Localized } from "../../internals/LocalizedT";
export interface Seller {
sellerName: Multilingual<string>,
sellerBio: Multilingual<string>,
slug: Multilingual<string>,
logoImage: Multilingual<string[]>,
sellerName: Localized<string>,
sellerBio: Localized<string>,
slug: Localized<string>,
logoImage: Localized<string[]>,
}

View File

@ -1,10 +1,10 @@
import type { SCHEMAS } from "../schemas";
import type { SupportedLocales } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { SupportedLocales } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Slug {
locale: NonMultilingual<SupportedLocales|string>,
localizedSlug: NonMultilingual<string>,
referenceSchema: NonMultilingual<SCHEMAS|string[]>,
reference: NonMultilingual<string[]>,
locale: NonLocalized<SupportedLocales|string>,
localizedSlug: NonLocalized<string>,
referenceSchema: NonLocalized<SCHEMAS|string[]>,
reference: NonLocalized<string[]>,
}

View File

@ -1,12 +1,14 @@
export const enum SCHEMAS {
AMAZON_GET_ITEMS = 'amazon-get-items',
BRANDS = 'brands',
BRANDS_SLOTS = 'brands-slots',
PAGES = 'pages',
LISTINGS = 'listings',
MARKETPLACES = 'marketplaces',
OFFERS = 'offers',
PRODUCT_CATEGORIES = 'product-categories',
PRODUCTS = 'products',
REDIRECTS = 'redirects',
SOCIAL_NETWORKS = 'social-networks',
SELLERS = 'sellers',
SITE = 'site',

View File

@ -1,11 +1,11 @@
import type { Multilingual } from "../../internals/MultilingualT";
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { Localized } from "../../internals/LocalizedT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
export interface Site {
siteName: Multilingual<string>,
siteUrl: Multilingual<string>,
homePage: NonMultilingual<string[]>,
siteBrand: Multilingual<string>,
copyright: Multilingual<string>,
footerLinks: Multilingual<string>,
siteName: Localized<string>,
siteUrl: Localized<string>,
homePage: NonLocalized<string[]>,
siteBrand: Localized<string>,
copyright: Localized<string>,
footerLinks: Localized<string>,
}

View File

@ -1,10 +1,10 @@
import type { NonMultilingual } from "../../internals/NonMultilingualT";
import type { NonLocalized } from "../../internals/NonLocalizedT";
import type { AmazonPAConfig } from "../components/AmazonPAConfig";
import type { GoogleAdSense } from "../components/GoogleAdSense";
import type { GoogleAnalytics } from "../components/GoogleAnalytics";
export interface SiteConfig {
amazonPAAPIConfig: NonMultilingual<AmazonPAConfig>,
googleAdSense: NonMultilingual<GoogleAdSense>,
googleAnalytics: NonMultilingual<GoogleAnalytics>,
amazonPAAPIConfig: NonLocalized<AmazonPAConfig>,
googleAdSense: NonLocalized<GoogleAdSense>,
googleAnalytics: NonLocalized<GoogleAnalytics>,
}

View File

@ -13,7 +13,7 @@ interface Props {
page: Page;
title: string;
site: Site;
siteEditToken: string;
siteEditToken?: string;
shouldEmbedSquidexSDK: boolean;
siteConfig: SiteConfig,
}
@ -80,7 +80,7 @@ const cssFixSquidex = `
</head>
<body>
<slot />
<div class="footer-container" data-data-squidex-token={siteEditToken}>
<div class="footer-container" data-squidex-token={siteEditToken}>
<div class="footer">
<div class="footer-header">
<Fragment set:html={interpolateString(getLocaleField(locale, site.siteBrand)!, renderContext)} />

View File

@ -140,7 +140,7 @@ try {
}
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page as Page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page as Page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
<meta name="description" content={metaDescription} />
{
@ -158,9 +158,9 @@ try {
}
</slot>
<main>
<Banner editToken={siteDto.items[0].editToken||''} homePageLink={`/${locale}/`} siteName={site.siteName[locale]} />
<Banner editToken={siteDto.items[0].editToken} homePageLink={`/${locale}/`} siteName={site.siteName[locale]} />
<ComponentRouter
pageEditToken={pageDto?.items[0].editToken||''}
pageEditToken={pageDto?.items[0].editToken}
componentRouter={getLocaleField(locale, page.content)||[]}
homePage={homePage as Page}
isHomePage={isHomePage}

View File

@ -1,8 +1,8 @@
---
import Layout from "../layouts/Layout.astro";
import { getBrandsByLangSlug, getLocaleField, getPagesByLangSlug, getProductCategoriesByLangSlug, getSite, getSlugByLangSlug, getSiteConfig, getSiteHomePage, getPagesByIds, getMarketplacesByLangSlug, getProductsByLangSlug, getSellersByLangSlug } from "../data/api-client";
import { getBrandsByLangSlug, getLocaleField, getPagesByLangSlug, getProductCategoriesByLangSlug, getSite, getSlugByLangSlug, getSiteConfig, getSiteHomePage, getPagesByIds, getMarketplacesByLangSlug, getProductsByLangSlug, getSellersByLangSlug, getRedirectsByPreviousSlug } from "../data/api-client";
import { genericPageForBrand, genericPageForMarketplace, genericPageForProduct, genericPageForProductCategory, genericPageForSeller } from "../lib/page-from-models";
import { SupportedLocales } from "../data/internals/MultilingualT";
import { SupportedLocales } from "../data/internals/LocalizedT";
import { SCHEMAS } from "../data/models/schemas";
import type { Brand } from "../data/models/multis/Brand";
import type { Page } from "../data/models/multis/Page";
@ -16,6 +16,7 @@ import Banner from "../components/Banner.astro";
import ComponentRouter from "../components/ComponentRouter.astro";
import { renderMarkdown } from "../lib/rendering";
import Disclaimers from "../components/Disclaimers.astro";
import { client, TIMEOUT_IN_SECONDS } from "../data/core/client";
const { routeLookup, resource, id } = Astro.params;
@ -80,13 +81,34 @@ else {
return Astro.rewrite(`/view/products/${locale}/${objectId}`);
case SCHEMAS.SELLERS:
return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
default:
return redirect404NotFound();
}
}
else {
return redirect404NotFound();
console.log("routeLookup:", routeLookup!);
const redirectDto = await getRedirectsByPreviousSlug(routeLookup!);
console.log("redirectDto:", redirectDto);
if (redirectDto && redirectDto.items && redirectDto.items.length > 0) {
console.log("getting redirectReferencesDto for", SCHEMAS.REDIRECTS, redirectDto.items[0].id);
let redirectReferencesDto = await client.contents.getReferences(SCHEMAS.REDIRECTS, redirectDto.items[0].id);
console.log("redirectReferencesDto:", redirectReferencesDto);
client.contents.getConten
// switch (redirectDto.items[0].data?.referenceSchema.iv) {
// case SCHEMAS.BRANDS:
// return Astro.rewrite(`/view/brands/${locale}/${objectId}`);
// case SCHEMAS.MARKETPLACES:
// return Astro.rewrite(`/view/marketplaces/${locale}/${objectId}`);
// case SCHEMAS.PAGES:
// return Astro.rewrite(`/view/pages/${locale}/${objectId}`);
// case SCHEMAS.PRODUCT_CATEGORIES:
// return Astro.rewrite(`/view/product-categories/${locale}/${objectId}`);
// case SCHEMAS.PRODUCTS:
// return Astro.rewrite(`/view/products/${locale}/${objectId}`);
// case SCHEMAS.SELLERS:
// return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
// }
// Astro.response.headers.set('Vary', 'Accept-Language');
// return Astro.redirect(,303)
}
return redirect404NotFound();
}
if (!page) {
@ -114,7 +136,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot slot="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -2,7 +2,7 @@
import Layout from "../../../layouts/Layout.astro";
import { getBrandsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { genericPageForBrand } from "../../../lib/page-from-models";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -65,7 +65,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -2,7 +2,7 @@
import Layout from "../../../layouts/Layout.astro";
import { getMarketplacesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { genericPageForMarketplace } from "../../../lib/page-from-models";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -65,7 +65,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -1,7 +1,7 @@
---
import Layout from "../../../layouts/Layout.astro";
import { getPagesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -71,7 +71,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -2,7 +2,7 @@
import Layout from "../../../layouts/Layout.astro";
import { getProductCategoriesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { genericPageForProductCategory } from "../../../lib/page-from-models";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -66,7 +66,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -2,7 +2,7 @@
import Layout from "../../../layouts/Layout.astro";
import { getProductsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { genericPageForProduct } from "../../../lib/page-from-models";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -69,7 +69,7 @@ const renderContext = {
// disclaimers,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{

View File

@ -2,7 +2,7 @@
import Layout from "../../../layouts/Layout.astro";
import { getSellersByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
import { genericPageForSeller } from "../../../lib/page-from-models";
import { SupportedLocales } from "../../../data/internals/MultilingualT";
import { SupportedLocales } from "../../../data/internals/LocalizedT";
import type { Page } from "../../../data/models/multis/Page";
import type { Site } from "../../../data/models/singles/Site";
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
@ -65,7 +65,7 @@ const renderContext = {
shouldEmbedSquidexSDK,
}
---
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken||''} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<Layout locale={locale} title={title} site={site} siteConfig={siteConfig} siteEditToken={siteDto.items[0].editToken} page={page} shouldEmbedSquidexSDK={shouldEmbedSquidexSDK}>
<slot name="head">
{ metaDescription && <meta name="description" content={metaDescription} /> }
{