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:
parent
b0d4fecd8f
commit
3cfa093d01
|
@ -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",
|
||||
|
|
|
@ -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();
|
111
src/apps/catalog/amazon/amazon-append-images.ts
Normal file
111
src/apps/catalog/amazon/amazon-append-images.ts
Normal 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();
|
34
src/apps/catalog/amazon/amazon-crawlee-products.ts
Normal file
34
src/apps/catalog/amazon/amazon-crawlee-products.ts
Normal 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();
|
251
src/apps/catalog/amazon/amazon-procure-asins.ts
Normal file
251
src/apps/catalog/amazon/amazon-procure-asins.ts
Normal 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;
|
||||
}
|
17
src/apps/catalog/amazon/amazon.ts
Normal file
17
src/apps/catalog/amazon/amazon.ts
Normal 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;
|
||||
}
|
28
src/apps/catalog/amazon/get-items/amazon-get-items-logs.ts
Normal file
28
src/apps/catalog/amazon/get-items/amazon-get-items-logs.ts
Normal 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;
|
||||
}
|
27
src/apps/catalog/amazon/get-items/amazon-get-items-ls.ts
Normal file
27
src/apps/catalog/amazon/get-items/amazon-get-items-ls.ts
Normal 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;
|
||||
}
|
12
src/apps/catalog/amazon/get-items/get-items.ts
Normal file
12
src/apps/catalog/amazon/get-items/get-items.ts
Normal 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;
|
||||
}
|
10
src/apps/catalog/catalog.ts
Normal file
10
src/apps/catalog/catalog.ts
Normal 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();
|
|
@ -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: {
|
9
src/apps/catalog/common/console.ts
Normal file
9
src/apps/catalog/common/console.ts
Normal 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]);
|
31
src/apps/catalog/products/products-ls.ts
Normal file
31
src/apps/catalog/products/products-ls.ts
Normal 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;
|
||||
}
|
10
src/apps/catalog/products/products.ts
Normal file
10
src/apps/catalog/products/products.ts
Normal 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;
|
||||
}
|
16
src/apps/catalog/site/site-sync-slugs.ts
Normal file
16
src/apps/catalog/site/site-sync-slugs.ts
Normal 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();
|
10
src/apps/catalog/site/site.ts
Normal file
10
src/apps/catalog/site/site.ts
Normal 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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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]} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>)
|
||||
||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
interface Content {
|
||||
editToken: string,
|
||||
import type { SquidexEditable } from "./SharedProperties";
|
||||
|
||||
interface Content extends SquidexEditable {
|
||||
text: string,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 :",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
20
src/components/SharedProperties.ts
Normal file
20
src/components/SharedProperties.ts
Normal 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
3
src/components/TODOS.md
Normal 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.
|
|
@ -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 {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
4
src/data/internals/NonLocalizedT.ts
Normal file
4
src/data/internals/NonLocalizedT.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface NonLocalized<T> {
|
||||
[key: string]: T,
|
||||
iv: T,
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export interface NonMultilingual<T> {
|
||||
[key: string]: T,
|
||||
iv: T,
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
||||
import type { MarketplaceConnection } from "./MarketplaceConnection";
|
||||
|
||||
export interface AmazonMarketplaceConnection extends MarketplaceConnection {
|
||||
|
|
7
src/data/models/components/BrandSlots.ts
Normal file
7
src/data/models/components/BrandSlots.ts
Normal 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[]>,
|
||||
}
|
5
src/data/models/components/PageBrandList.ts
Normal file
5
src/data/models/components/PageBrandList.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { Component } from "../../internals/Component";
|
||||
|
||||
export interface PageBrandList extends Component {
|
||||
brands: string[],
|
||||
}
|
5
src/data/models/components/PageBrandSlots.ts
Normal file
5
src/data/models/components/PageBrandSlots.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { Component } from "../../internals/Component";
|
||||
|
||||
export interface PageBrandSlots extends Component {
|
||||
brandSlots: string[],
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import type { Component } from "../../internals/Component";
|
||||
import type { Multilingual } from "../../internals/MultilingualT";
|
||||
|
||||
export interface PageCallout extends Component {
|
||||
text: string,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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[]>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
6
src/data/models/multis/Redirect.ts
Normal file
6
src/data/models/multis/Redirect.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||
|
||||
export interface Redirect {
|
||||
prevSlug: NonLocalized<string>,
|
||||
newContent: NonLocalized<string[]>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
|
@ -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[]>,
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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)} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,14 +81,35 @@ else {
|
|||
return Astro.rewrite(`/view/products/${locale}/${objectId}`);
|
||||
case SCHEMAS.SELLERS:
|
||||
return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
|
||||
default:
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
else {
|
||||
return redirect404NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
//this code can never be reached, but if it can be reached then the compiler
|
||||
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
|
@ -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} /> }
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue
Block a user