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",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
|
"catalog": "bun run src/apps/catalog/catalog.ts",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"test": "vitest",
|
"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 { AmazonGetItem } from '../../../data/models/multis/AmazonGetItem.ts';
|
||||||
import type { Brand } from '../../data/models/multis/Brand.ts';
|
import type { Brand } from '../../../data/models/multis/Brand.ts';
|
||||||
import type { ContentsDto } from '../../data/internals/ContentsDtoT.ts';
|
import type { ContentsDto } from '../../../data/internals/ContentsDtoT.ts';
|
||||||
import type { GetItemsResponse, Item } from 'amazon-pa-api5-node-ts';
|
import type { GetItemsResponse, Item } from 'amazon-pa-api5-node-ts';
|
||||||
import type { Listing } from '../../data/models/multis/Listing.ts';
|
import type { Listing } from '../../../data/models/multis/Listing.ts';
|
||||||
import type { Multilingual } from '../../data/internals/MultilingualT.ts';
|
import type { Localized } from '../../../data/internals/LocalizedT.ts';
|
||||||
import type { NonMultilingual } from '../../data/internals/NonMultilingualT.ts';
|
import type { NonLocalized } from '../../../data/internals/NonLocalizedT.ts';
|
||||||
import type { Product } from '../../data/models/multis/Product.ts';
|
import type { Product } from '../../../data/models/multis/Product.ts';
|
||||||
import type { ProductCategory } from '../../data/models/multis/ProductCategory.ts';
|
import type { ProductCategory } from '../../../data/models/multis/ProductCategory.ts';
|
||||||
import type { Seller } from '../../data/models/multis/Seller.ts';
|
import type { Seller } from '../../../data/models/multis/Seller.ts';
|
||||||
import { arrayBuffer } from 'stream/consumers';
|
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 { createReadStream, type ReadStream } from 'fs';
|
||||||
import { getItems } from '../../apiclient/amazon.ts';
|
import { getItems } from '../../../apiclient/amazon.ts';
|
||||||
import { getBrandsUsingJsonQuery, getMarketplacesUsingJsonQuery, getProductCategoriesUsingJsonQuery, getProductsUsingJsonQuery, getSellersUsingJsonQuery } from '../../data/api-client.ts';
|
import { getBrandsUsingJsonQuery, getMarketplacesUsingJsonQuery, getProductCategoriesUsingJsonQuery, getProductsUsingJsonQuery, getSellersUsingJsonQuery } from '../../../data/api-client.ts';
|
||||||
import { memfs } from 'memfs';
|
import { memfs } from 'memfs';
|
||||||
import { response } from '../../old-data/brands/first-aid-only-example-query-getitems.ts';
|
import { response } from '../../../old-data/brands/first-aid-only-example-query-getitems.ts';
|
||||||
import { SCHEMAS } from '../../data/models/schemas.ts';
|
import { SCHEMAS } from '../../../data/models/schemas.ts';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import ollama from 'ollama';
|
import ollama from 'ollama';
|
||||||
import slugify from 'slugify';
|
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) {
|
export function isValidASIN(asinOrNot: string) {
|
||||||
return asinOrNot.match(/[A-Z0-9]{10}/g) ? true : false;
|
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<{
|
export async function askAiProductSubCategoryEvalQuestionsSet2(response: GetItemsResponse, parentProductCategoryId: string, brandName: string, productName_en_US: string, features: string[], shouldCreateNewProductCategory: boolean): Promise<{
|
||||||
categoryName: Multilingual<string>,
|
categoryName: Localized<string>,
|
||||||
description: Multilingual<string>,
|
description: Localized<string>,
|
||||||
parentCategory: NonMultilingual<string[]>,
|
parentCategory: NonLocalized<string[]>,
|
||||||
}|string> {
|
}|string> {
|
||||||
const aiSubCategoryEvalQuestion2 = await designAiProductSubCategoryEvalQuestionsSet2(response, parentProductCategoryId, brandName, productName_en_US, features, shouldCreateNewProductCategory);
|
const aiSubCategoryEvalQuestion2 = await designAiProductSubCategoryEvalQuestionsSet2(response, parentProductCategoryId, brandName, productName_en_US, features, shouldCreateNewProductCategory);
|
||||||
console.log(`>>>${aiSubCategoryEvalQuestion2}`);
|
console.log(`>>>${aiSubCategoryEvalQuestion2}`);
|
||||||
|
@ -358,8 +358,8 @@ export async function askAiProductSubCategoryEvalQuestionsSet2(response: GetItem
|
||||||
})).message.content;
|
})).message.content;
|
||||||
console.log(`llama3.1>${answer}`);
|
console.log(`llama3.1>${answer}`);
|
||||||
const aiSubCategoryEvalQuestion2Answer = JSON.parse(answer) as {
|
const aiSubCategoryEvalQuestion2Answer = JSON.parse(answer) as {
|
||||||
categoryName: Multilingual<string>,
|
categoryName: Localized<string>,
|
||||||
description: Multilingual<string>,
|
description: Localized<string>,
|
||||||
parentCategory: string[],
|
parentCategory: string[],
|
||||||
}|string;
|
}|string;
|
||||||
if (typeof aiSubCategoryEvalQuestion2Answer === '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());
|
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, {
|
let productCategoryDto = await client.contents.postContent(SCHEMAS.PRODUCT_CATEGORIES, {
|
||||||
publish: false,
|
publish: false,
|
||||||
body: {
|
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 {
|
import type { SquidexEditable } from './SharedProperties.js';
|
||||||
editToken?: string,
|
|
||||||
|
interface Banner extends SquidexEditable {
|
||||||
homePageLink: string,
|
homePageLink: string,
|
||||||
siteName: string,
|
siteName: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { editToken, homePageLink, siteName } = Astro.props;
|
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>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
---
|
---
|
||||||
|
import type { SquidexEditable, Multilingual } from './SharedProperties.js';
|
||||||
import type { Brand } from "../data/models/multis/Brand";
|
import type { Brand } from "../data/models/multis/Brand";
|
||||||
import { getAssetById, getLocaleField } from "../data/api-client";
|
import { getAssetById, getLocaleField } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
brand: Brand,
|
brand: Brand,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { brand, editToken, locale } = Astro.props;
|
const { brand, editToken, locale } = Astro.props;
|
||||||
|
@ -22,7 +21,7 @@ let brandLogoImage = path.posix.join('/img', (await getAssetById(brand.logoImage
|
||||||
.join('/'));
|
.join('/'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="brand" data-squidex-token={editToken||''}>
|
<div class="brand" data-squidex-token={editToken}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
{ brandLogoImage &&
|
{ brandLogoImage &&
|
||||||
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />
|
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
import type { Brand } from "../data/models/multis/Brand";
|
import type { Brand } from "../data/models/multis/Brand";
|
||||||
import { getAssetById } from "../data/api-client";
|
import { getAssetById } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
brand: Brand,
|
brand: Brand,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { brand, editToken, locale } = Astro.props;
|
const { brand, editToken, locale } = Astro.props;
|
||||||
|
@ -25,7 +24,7 @@ let brandLogoImage = path.posix.join('/img', (await getAssetById(brand.logoImage
|
||||||
.join('/'));
|
.join('/'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="brand-card" data-squidex-token={editToken||''}>
|
<li class="brand-card" data-squidex-token={editToken}>
|
||||||
<a href={`/${brand.slug[locale]}/`}>
|
<a href={`/${brand.slug[locale]}/`}>
|
||||||
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />
|
<img src={brandLogoImage} alt={brand.brandName[locale]} title={brand.brandName[locale]} />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
---
|
||||||
export interface Breadcrumb {
|
import type { SquidexEditable } from './SharedProperties';
|
||||||
|
|
||||||
|
export interface Breadcrumb extends SquidexEditable {
|
||||||
text: string;
|
text: string;
|
||||||
url: string;
|
url: string;
|
||||||
gradient?: boolean;
|
gradient?: boolean;
|
||||||
editToken?: string;
|
editToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props extends SquidexEditable {
|
||||||
editToken?: string;
|
|
||||||
breadcrumbs: Breadcrumb[];
|
breadcrumbs: Breadcrumb[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,19 +16,19 @@ const { breadcrumbs, editToken } = Astro.props;
|
||||||
---
|
---
|
||||||
{breadcrumbs &&
|
{breadcrumbs &&
|
||||||
breadcrumbs.length &&
|
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">
|
<ol class="breadcrumb">
|
||||||
{breadcrumbs.map((breadcrumb: Breadcrumb, index: number, breadcrumbs: Breadcrumb[]) => {
|
{breadcrumbs.map((breadcrumb: Breadcrumb, index: number, breadcrumbs: Breadcrumb[]) => {
|
||||||
let isLastCrumb = ((index + 1) === breadcrumbs.length);
|
let isLastCrumb = ((index + 1) === breadcrumbs.length);
|
||||||
if (isLastCrumb) {
|
if (isLastCrumb) {
|
||||||
//the last crumb doesn't get a link
|
//the last crumb doesn't get a link
|
||||||
return (
|
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 {
|
else {
|
||||||
return (
|
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 &&
|
<a href={breadcrumb.url}>{(breadcrumb.gradient &&
|
||||||
<span class="text-gradient">{breadcrumb.text}</span>)
|
<span class="text-gradient">{breadcrumb.text}</span>)
|
||||||
||
|
||
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
---
|
||||||
interface Callout {
|
import type { SquidexEditable } from "./SharedProperties";
|
||||||
editToken?: string,
|
|
||||||
|
interface Callout extends SquidexEditable {
|
||||||
text: string,
|
text: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { editToken, text } = Astro.props;
|
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>
|
<style>
|
||||||
.callout {
|
.callout {
|
||||||
|
|
|
@ -5,6 +5,8 @@ import type { Component } from "../data/internals/Component";
|
||||||
import type { Marketplace } from "../data/models/multis/Marketplace";
|
import type { Marketplace } from "../data/models/multis/Marketplace";
|
||||||
import type { Page } from "../data/models/multis/Page";
|
import type { Page } from "../data/models/multis/Page";
|
||||||
import type { PageBrand } from "../data/models/components/PageBrand";
|
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 { PageProduct } from "../data/models/components/PageProduct";
|
||||||
import type { PageCallout } from "../data/models/components/PageCallout";
|
import type { PageCallout } from "../data/models/components/PageCallout";
|
||||||
import type { PageContent } from "../data/models/components/PageContent";
|
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 { Seller } from "../data/models/multis/Seller";
|
||||||
import type { Site } from "../data/models/singles/Site";
|
import type { Site } from "../data/models/singles/Site";
|
||||||
import type { QueryComponent } from "../data/models/components/QueryComponent";
|
import type { QueryComponent } from "../data/models/components/QueryComponent";
|
||||||
import { SupportedLocales } from "../data/internals/MultilingualT";
|
import { SupportedLocales } from "../data/internals/LocalizedT";
|
||||||
import { getBrandsByIds, getBrandsUsingJsonQuery, getMarketplacesByIds, getMarketplacesUsingJsonQuery, getPagesByIds, getProductCategoriesByIds, getProductCategoriesUsingJsonQuery, getProductsByIds, getProductsUsingJsonQuery, getSellersByIds, getSellersUsingJsonQuery } from '../data/api-client';
|
import { getBrandsByIds, getBrandSlotsByIds, getBrandsUsingJsonQuery, getMarketplacesByIds, getMarketplacesUsingJsonQuery, getPagesByIds, getProductCategoriesByIds, getProductCategoriesUsingJsonQuery, getProductsByIds, getProductsUsingJsonQuery, getSellersByIds, getSellersUsingJsonQuery } from '../data/api-client';
|
||||||
import BrandComponent from './Brand.astro';
|
import BrandComponent from './Brand.astro';
|
||||||
import ProductComponent from './Product.astro';
|
import ProductComponent from './Product.astro';
|
||||||
import Breadcrumbs, { type Breadcrumb } from './Breadcrumbs.astro';
|
import Breadcrumbs, { type Breadcrumb } from './Breadcrumbs.astro';
|
||||||
|
@ -171,6 +173,53 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
|
||||||
})}
|
})}
|
||||||
</ul>
|
</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':
|
case 'page-brand':
|
||||||
let brandComponent = dynComponent as PageBrand;
|
let brandComponent = dynComponent as PageBrand;
|
||||||
let brandId = brandComponent.brand ? brandComponent.brand[0] : '';
|
let brandId = brandComponent.brand ? brandComponent.brand[0] : '';
|
||||||
|
@ -259,7 +308,7 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
|
||||||
let editToken = productCategoryDto.editToken;
|
let editToken = productCategoryDto.editToken;
|
||||||
return (
|
return (
|
||||||
<ProductCategoryCard
|
<ProductCategoryCard
|
||||||
editToken={editToken||''}
|
editToken={editToken}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
productCategory={productCategoryDto.data!}
|
productCategory={productCategoryDto.data!}
|
||||||
/>
|
/>
|
||||||
|
@ -284,7 +333,7 @@ async function flatWalkSubCategories (productCategoryId: string): Promise<Conten
|
||||||
let editToken = productDto.editToken;
|
let editToken = productDto.editToken;
|
||||||
return (
|
return (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
editToken={editToken||''}
|
editToken={editToken}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
productDto={productDto}
|
productDto={productDto}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
interface Content {
|
import type { SquidexEditable } from "./SharedProperties";
|
||||||
editToken: string,
|
|
||||||
|
interface Content extends SquidexEditable {
|
||||||
text: string,
|
text: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
import type { Marketplace } from "../data/models/multis/Marketplace";
|
import type { Marketplace } from "../data/models/multis/Marketplace";
|
||||||
import { getMarketplacesUsingJsonQuery } from "../data/api-client";
|
import { getMarketplacesUsingJsonQuery } from "../data/api-client";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
|
import type { Multilingual } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual {
|
||||||
locale: string,
|
locale: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,10 @@ import type { Marketplace } from "../data/models/multis/Marketplace";
|
||||||
import { getAssetById, getLocaleField } from "../data/api-client";
|
import { getAssetById, getLocaleField } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
marketplace: Marketplace,
|
marketplace: Marketplace,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { marketplace, editToken, locale } = Astro.props;
|
const { marketplace, editToken, locale } = Astro.props;
|
||||||
|
@ -23,7 +22,7 @@ let marketplaceLogoImage = path.posix.join('/img',
|
||||||
.join('/'));
|
.join('/'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="marketplace" data-squidex-token={editToken||''}>
|
<div class="marketplace" data-squidex-token={editToken}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
{ marketplaceLogoImage &&
|
{ marketplaceLogoImage &&
|
||||||
marketplaceLogoImageAsset.metadata['background-color']
|
marketplaceLogoImageAsset.metadata['background-color']
|
||||||
|
@ -46,7 +45,7 @@ let marketplaceLogoImage = path.posix.join('/img',
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="marketplace-dark" data-squidex-token={editToken||''}>
|
<div class="marketplace-dark" data-squidex-token={editToken}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
{marketplace.longDescription && marketplace.longDescription[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(marketplace.longDescription[locale]||'')} /></div> }
|
{marketplace.longDescription && marketplace.longDescription[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(marketplace.longDescription[locale]||'')} /></div> }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Marketplace } from "../data/models/multis/Marketplace";
|
import type { Marketplace } from "../data/models/multis/Marketplace";
|
||||||
import { getAssetById } from "../data/api-client";
|
import { getAssetById } from "../data/api-client";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
editToken?: string,
|
|
||||||
marketplace: Marketplace,
|
marketplace: Marketplace,
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { editToken, marketplace, locale } = Astro.props;
|
const { editToken, marketplace, locale } = Astro.props;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import type { ContentsDto } from "../data/internals/ContentsDtoT";
|
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 type { Product } from "../data/models/multis/Product";
|
||||||
import { getAssetById, getBrandsByIds, getMarketplacesByIds, getOffersByListingId } from "../data/api-client";
|
import { getAssetById, getBrandsByIds, getMarketplacesByIds, getOffersByListingId } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
@ -14,11 +14,10 @@ import ImageCarousel from "./ImageCarousel.astro";
|
||||||
import type { AmazonMarketplaceConnection } from "../data/models/components/AmazonMarketplaceConnection";
|
import type { AmazonMarketplaceConnection } from "../data/models/components/AmazonMarketplaceConnection";
|
||||||
import { getSellersByIds } from "../data/api-client";
|
import { getSellersByIds } from "../data/api-client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
productDto: ContentsDto<Product>,
|
productDto: ContentsDto<Product>,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
let category={ } as unknown as any;let site={ } as unknown as any
|
let category={ } as unknown as any;let site={ } as unknown as any
|
||||||
const formatAsCurrency = (amount: number) => amount.toLocaleString(locale, { style: 'currency', currency: 'USD' });
|
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:': {
|
'Brand:': {
|
||||||
'en-US': "Brand:",
|
'en-US': "Brand:",
|
||||||
'es-US': "Marca :",
|
'es-US': "Marca :",
|
||||||
|
|
|
@ -12,11 +12,10 @@ import { renderMarkdown } from "../lib/rendering";
|
||||||
import type { Marketplace } from "../data/models/multis/Marketplace";
|
import type { Marketplace } from "../data/models/multis/Marketplace";
|
||||||
import { getAssetById } from "../data/api-client";
|
import { getAssetById } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
productDto?: ContentDto<Product>,
|
productDto?: ContentDto<Product>,
|
||||||
locale: string,
|
|
||||||
editToken?: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { productDto, locale, editToken } = Astro.props;
|
const { productDto, locale, editToken } = Astro.props;
|
||||||
|
|
|
@ -4,11 +4,10 @@ import { getAssetById, getLocaleField } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
import Callout from "./Callout.astro";
|
import Callout from "./Callout.astro";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
productCategory: ProductCategory,
|
productCategory: ProductCategory,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
const { productCategory, editToken, locale } = Astro.props;
|
const { productCategory, editToken, locale } = Astro.props;
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@ let productCategoryImage = path.posix.join('/img',
|
||||||
.join('/'));
|
.join('/'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="product-category" data-squidex-token={editToken||''}>
|
<div class="product-category" data-squidex-token={editToken}>
|
||||||
<div class="cover-image"></div>
|
<div class="cover-image"></div>
|
||||||
<div class="content center">
|
<div class="content center">
|
||||||
<div class="flex-right">
|
<div class="flex-right">
|
||||||
|
|
|
@ -3,12 +3,11 @@ import type { ProductCategory } from "../data/models/multis/ProductCategory";
|
||||||
import { getAssetById } from '../data/api-client';
|
import { getAssetById } from '../data/api-client';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { renderMarkdown } from '../lib/rendering';
|
import { renderMarkdown } from '../lib/rendering';
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
// import { BlocksRenderer, type BlocksContent } from '@strapi/blocks-react-renderer';
|
// import { BlocksRenderer, type BlocksContent } from '@strapi/blocks-react-renderer';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
editToken: string,
|
|
||||||
productCategory: ProductCategory
|
productCategory: ProductCategory
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { editToken, productCategory, locale } = Astro.props;
|
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 { getAssetById, getLocaleField } from "../data/api-client";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
seller: Seller,
|
seller: Seller,
|
||||||
editToken?: string,
|
|
||||||
locale: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { seller, editToken, locale } = Astro.props;
|
const { seller, editToken, locale } = Astro.props;
|
||||||
|
@ -23,7 +22,7 @@ let sellerLogoImage = sellerLogoImageAsset ? path.posix.join('/img',
|
||||||
.join('/')) : '';
|
.join('/')) : '';
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="seller" data-squidex-token={editToken||''}>
|
<div class="seller" data-squidex-token={editToken}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
{ sellerLogoImage ?
|
{ sellerLogoImage ?
|
||||||
( sellerLogoImage &&
|
( sellerLogoImage &&
|
||||||
|
@ -45,7 +44,7 @@ let sellerLogoImage = sellerLogoImageAsset ? path.posix.join('/img',
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="seller-dark" data-squidex-token={editToken||''}>
|
<div class="seller-dark" data-squidex-token={editToken}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
{seller.sellerBio && seller.sellerBio[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(seller.sellerBio[locale]||'')} /></div> }
|
{seller.sellerBio && seller.sellerBio[locale] && <div class="after-flex"><Fragment set:html={renderMarkdown(seller.sellerBio[locale]||'')} /></div> }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Seller } from "../data/models/multis/Seller";
|
import type { Seller } from "../data/models/multis/Seller";
|
||||||
import { getAssetById } from "../data/api-client";
|
import { getAssetById } from "../data/api-client";
|
||||||
|
import type { Multilingual, SquidexEditable } from "./SharedProperties";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Multilingual, SquidexEditable {
|
||||||
editToken?: string,
|
editToken?: string,
|
||||||
seller: Seller,
|
seller: Seller,
|
||||||
locale: string,
|
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 * as core from "./core/client";
|
||||||
import { SCHEMAS } from "./models/schemas";
|
import { SCHEMAS } from "./models/schemas";
|
||||||
import { getContents } from "./core/client.js";
|
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 { Component } from "./internals/Component";
|
||||||
import type { Brand } from "./models/multis/Brand";
|
import type { Brand } from "./models/multis/Brand";
|
||||||
import type { Page } from "./models/multis/Page";
|
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 { Product } from "./models/multis/Product";
|
||||||
import type { Slug } from "./models/multis/Slug";
|
import type { Slug } from "./models/multis/Slug";
|
||||||
import type { Seller } from "./models/multis/Seller";
|
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 { ContentsDto } from "./internals/ContentsDtoT";
|
||||||
import type { ContentData } from "@squidex/squidex/api/types/ContentData";
|
import type { ContentData } from "@squidex/squidex/api/types/ContentData";
|
||||||
import type { ContentDto } from "./internals/ContentDtoT";
|
import type { ContentDto } from "./internals/ContentDtoT";
|
||||||
import type { Listing } from "./models/multis/Listing.js";
|
import type { Listing } from "./models/multis/Listing.js";
|
||||||
import type { Offer } from "./models/multis/Offer.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 */
|
/** 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()])
|
if (field && field[locale.toString()])
|
||||||
return 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) =>
|
export const getBrandsUsingJsonQuery = async (jsonQuery: string|undefined = undefined) =>
|
||||||
await core.getContentsUsingJsonQuery<Brand>(SCHEMAS.BRANDS, jsonQuery);
|
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 */
|
/** Marketplaces handlers */
|
||||||
|
|
||||||
export const getMarketplacesByIds = async (ids: string) =>
|
export const getMarketplacesByIds = async (ids: string) =>
|
||||||
|
@ -115,7 +128,16 @@ export const getOffersByListingId = async (listingId: string) =>
|
||||||
filter: {
|
filter: {
|
||||||
path: "data.listing.iv",
|
path: "data.listing.iv",
|
||||||
op: "eq",
|
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);
|
await getContents<SiteConfig>(SCHEMAS.SITE_CONFIG);
|
||||||
|
|
||||||
export async function performSyncLocalizedSlugs(logFn = console.log) {
|
export async function performSyncLocalizedSlugs(logFn = console.log) {
|
||||||
logFn("[sync-slugs] Begin sync localized slugs.")
|
logFn("Begin sync localized slugs.")
|
||||||
let allSlugs = await getAllSlugs();
|
let allSlugs = await getAllSlugs();
|
||||||
let allPages = await core.getContentsUsingJsonQuery<Page>(SCHEMAS.PAGES);
|
let allPages = await core.getContentsUsingJsonQuery<Page>(SCHEMAS.PAGES);
|
||||||
let allBrands = await core.getContentsUsingJsonQuery<Brand>(SCHEMAS.BRANDS);
|
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++) {
|
for (let i = 0; i < allSlugs.items.length; i++) {
|
||||||
let testSlug = allSlugs.items[i].data!;
|
let testSlug = allSlugs.items[i].data!;
|
||||||
if (testSlug.localizedSlug.iv === slug[locale]
|
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
|
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 });
|
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) => {
|
batchRemoveSlugsQueue.forEach(async (removeId) => {
|
||||||
await core.client.contents.deleteContent(SCHEMAS.SLUGS, removeId)
|
await core.client.contents.deleteContent(SCHEMAS.SLUGS, removeId)
|
||||||
})
|
})
|
||||||
logFn("[sync-slugs] Finish sync localized slugs.")
|
logFn("Finish sync localized slugs.")
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AmazonPAApiSyncClient {
|
export class AmazonPAApiSyncClient {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { config } from "../../config.js";
|
import { config } from "../../config.js";
|
||||||
import { SquidexClient } from "@squidex/squidex";
|
import { SquidexClient } from "@squidex/squidex";
|
||||||
import type { ContentsDto } from "../internals/ContentsDtoT.js";
|
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";
|
import type { SCHEMAS } from "../models/schemas.js";
|
||||||
|
|
||||||
export const client = new SquidexClient({
|
export const client = new SquidexClient({
|
||||||
|
|
|
@ -4,7 +4,7 @@ export enum SupportedLocales {
|
||||||
'fr-CA' = 'fr-CA',
|
'fr-CA' = 'fr-CA',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Multilingual<T> {
|
export interface Localized<T> {
|
||||||
[key: string]: T,
|
[key: string]: T,
|
||||||
'en-US': T,
|
'en-US': T,
|
||||||
'es-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";
|
import type { MarketplaceConnection } from "./MarketplaceConnection";
|
||||||
|
|
||||||
export interface AmazonMarketplaceConnection extends 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 { Component } from "../../internals/Component";
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
|
||||||
|
|
||||||
export interface PageCallout extends Component {
|
export interface PageCallout extends Component {
|
||||||
text: string,
|
text: string,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { Component } from "../../internals/Component";
|
import type { Component } from "../../internals/Component";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
|
||||||
import type { MarketplaceConnection } from "./MarketplaceConnection";
|
import type { MarketplaceConnection } from "./MarketplaceConnection";
|
||||||
|
|
||||||
export interface ProductMarketplaceConnection extends Component {
|
export interface ProductMarketplaceConnection extends Component {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { AmazonPAGetItemsRequest } from "../components/AmazonPAGetItemsRequest";
|
import type { AmazonPAGetItemsRequest } from "../components/AmazonPAGetItemsRequest";
|
||||||
import type { GetItemsResponse } from "amazon-pa-api5-node-ts";
|
import type { GetItemsResponse } from "amazon-pa-api5-node-ts";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface AmazonGetItem {
|
export interface AmazonGetItem {
|
||||||
requestDate: NonMultilingual<string>,
|
requestDate: NonLocalized<string>,
|
||||||
getItemsRequest: NonMultilingual<AmazonPAGetItemsRequest>,
|
getItemsRequest: NonLocalized<AmazonPAGetItemsRequest>,
|
||||||
apiResponse: NonMultilingual<GetItemsResponse>,
|
apiResponse: NonLocalized<GetItemsResponse>,
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Brand {
|
export interface Brand {
|
||||||
brandName: Multilingual<string>,
|
brandName: Localized<string>,
|
||||||
logoImage?: Multilingual<string>,
|
logoImage?: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
shortDescription?: Multilingual<string>,
|
shortDescription?: Localized<string>,
|
||||||
longDescription?: Multilingual<string>,
|
longDescription?: Localized<string>,
|
||||||
brandPage?: NonMultilingual<string[]>,
|
brandPage?: NonLocalized<string[]>,
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Listing {
|
export interface Listing {
|
||||||
product: NonMultilingual<string[]>,
|
product: NonLocalized<string[]>,
|
||||||
marketplace: NonMultilingual<string[]>,
|
marketplace: NonLocalized<string[]>,
|
||||||
marketplaceDescription: Multilingual<string>,
|
marketplaceDescription: Localized<string>,
|
||||||
marketplaceImages: NonMultilingual<string[]>,
|
marketplaceImages: NonLocalized<string[]>,
|
||||||
}
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Marketplace {
|
export interface Marketplace {
|
||||||
marketplaceName: Multilingual<string>,
|
marketplaceName: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
logoImage: Multilingual<string[]>,
|
logoImage: Localized<string[]>,
|
||||||
marketplacePage: NonMultilingual<string[]>,
|
marketplacePage: NonLocalized<string[]>,
|
||||||
disclaimer: Multilingual<string>,
|
disclaimer: Localized<string>,
|
||||||
shortDescription: Multilingual<string>,
|
shortDescription: Localized<string>,
|
||||||
longDescription: Multilingual<string>,
|
longDescription: Localized<string>,
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Offer {
|
export interface Offer {
|
||||||
offerDate: NonMultilingual<string>,
|
offerDate: NonLocalized<string>,
|
||||||
seller: NonMultilingual<string[]>,
|
seller: NonLocalized<string[]>,
|
||||||
listing: NonMultilingual<string[]>,
|
listing: NonLocalized<string[]>,
|
||||||
newPrice: NonMultilingual<number|null>,
|
newPrice: NonLocalized<number|null>,
|
||||||
usedPrice: NonMultilingual<number|null>,
|
usedPrice: NonLocalized<number|null>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { PageSeo } from "../components/PageSeo";
|
import type { PageSeo } from "../components/PageSeo";
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
import type { Component } from "../../internals/Component";
|
import type { Component } from "../../internals/Component";
|
||||||
|
|
||||||
export interface Page {
|
export interface Page {
|
||||||
title: Multilingual<string>,
|
title: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
content: Multilingual<Component[]>,
|
content: Localized<Component[]>,
|
||||||
seo: Multilingual<PageSeo>,
|
seo: Localized<PageSeo>,
|
||||||
parentPage: NonMultilingual<string[]>,
|
parentPage: NonLocalized<string[]>,
|
||||||
}
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
import type { ProductMarketplaceConnection } from "../components/ProductMarketplaceConnection";
|
import type { ProductMarketplaceConnection } from "../components/ProductMarketplaceConnection";
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
productName: Multilingual<string>,
|
productName: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
brand: NonMultilingual<string[]>,
|
brand: NonLocalized<string[]>,
|
||||||
categories: NonMultilingual<string[]>,
|
categories: NonLocalized<string[]>,
|
||||||
tags: Multilingual<string[]>,
|
tags: Localized<string[]>,
|
||||||
description: Multilingual<string>,
|
description: Localized<string>,
|
||||||
marketplaceConnections: NonMultilingual<ProductMarketplaceConnection[]>,
|
marketplaceConnections: NonLocalized<ProductMarketplaceConnection[]>,
|
||||||
productPage?: NonMultilingual<string[]>,
|
productPage?: NonLocalized<string[]>,
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface ProductCategory {
|
export interface ProductCategory {
|
||||||
categoryName: Multilingual<string>,
|
categoryName: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
categoryImage: Multilingual<string>,
|
categoryImage: Localized<string>,
|
||||||
description: Multilingual<string>,
|
description: Localized<string>,
|
||||||
parentCategory: NonMultilingual<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 {
|
export interface Seller {
|
||||||
sellerName: Multilingual<string>,
|
sellerName: Localized<string>,
|
||||||
sellerBio: Multilingual<string>,
|
sellerBio: Localized<string>,
|
||||||
slug: Multilingual<string>,
|
slug: Localized<string>,
|
||||||
logoImage: Multilingual<string[]>,
|
logoImage: Localized<string[]>,
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import type { SCHEMAS } from "../schemas";
|
import type { SCHEMAS } from "../schemas";
|
||||||
import type { SupportedLocales } from "../../internals/MultilingualT";
|
import type { SupportedLocales } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Slug {
|
export interface Slug {
|
||||||
locale: NonMultilingual<SupportedLocales|string>,
|
locale: NonLocalized<SupportedLocales|string>,
|
||||||
localizedSlug: NonMultilingual<string>,
|
localizedSlug: NonLocalized<string>,
|
||||||
referenceSchema: NonMultilingual<SCHEMAS|string[]>,
|
referenceSchema: NonLocalized<SCHEMAS|string[]>,
|
||||||
reference: NonMultilingual<string[]>,
|
reference: NonLocalized<string[]>,
|
||||||
}
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
export const enum SCHEMAS {
|
export const enum SCHEMAS {
|
||||||
AMAZON_GET_ITEMS = 'amazon-get-items',
|
AMAZON_GET_ITEMS = 'amazon-get-items',
|
||||||
BRANDS = 'brands',
|
BRANDS = 'brands',
|
||||||
|
BRANDS_SLOTS = 'brands-slots',
|
||||||
PAGES = 'pages',
|
PAGES = 'pages',
|
||||||
LISTINGS = 'listings',
|
LISTINGS = 'listings',
|
||||||
MARKETPLACES = 'marketplaces',
|
MARKETPLACES = 'marketplaces',
|
||||||
OFFERS = 'offers',
|
OFFERS = 'offers',
|
||||||
PRODUCT_CATEGORIES = 'product-categories',
|
PRODUCT_CATEGORIES = 'product-categories',
|
||||||
PRODUCTS = 'products',
|
PRODUCTS = 'products',
|
||||||
|
REDIRECTS = 'redirects',
|
||||||
SOCIAL_NETWORKS = 'social-networks',
|
SOCIAL_NETWORKS = 'social-networks',
|
||||||
SELLERS = 'sellers',
|
SELLERS = 'sellers',
|
||||||
SITE = 'site',
|
SITE = 'site',
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Multilingual } from "../../internals/MultilingualT";
|
import type { Localized } from "../../internals/LocalizedT";
|
||||||
import type { NonMultilingual } from "../../internals/NonMultilingualT";
|
import type { NonLocalized } from "../../internals/NonLocalizedT";
|
||||||
|
|
||||||
export interface Site {
|
export interface Site {
|
||||||
siteName: Multilingual<string>,
|
siteName: Localized<string>,
|
||||||
siteUrl: Multilingual<string>,
|
siteUrl: Localized<string>,
|
||||||
homePage: NonMultilingual<string[]>,
|
homePage: NonLocalized<string[]>,
|
||||||
siteBrand: Multilingual<string>,
|
siteBrand: Localized<string>,
|
||||||
copyright: Multilingual<string>,
|
copyright: Localized<string>,
|
||||||
footerLinks: Multilingual<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 { AmazonPAConfig } from "../components/AmazonPAConfig";
|
||||||
import type { GoogleAdSense } from "../components/GoogleAdSense";
|
import type { GoogleAdSense } from "../components/GoogleAdSense";
|
||||||
import type { GoogleAnalytics } from "../components/GoogleAnalytics";
|
import type { GoogleAnalytics } from "../components/GoogleAnalytics";
|
||||||
|
|
||||||
export interface SiteConfig {
|
export interface SiteConfig {
|
||||||
amazonPAAPIConfig: NonMultilingual<AmazonPAConfig>,
|
amazonPAAPIConfig: NonLocalized<AmazonPAConfig>,
|
||||||
googleAdSense: NonMultilingual<GoogleAdSense>,
|
googleAdSense: NonLocalized<GoogleAdSense>,
|
||||||
googleAnalytics: NonMultilingual<GoogleAnalytics>,
|
googleAnalytics: NonLocalized<GoogleAnalytics>,
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
page: Page;
|
page: Page;
|
||||||
title: string;
|
title: string;
|
||||||
site: Site;
|
site: Site;
|
||||||
siteEditToken: string;
|
siteEditToken?: string;
|
||||||
shouldEmbedSquidexSDK: boolean;
|
shouldEmbedSquidexSDK: boolean;
|
||||||
siteConfig: SiteConfig,
|
siteConfig: SiteConfig,
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ const cssFixSquidex = `
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<slot />
|
<slot />
|
||||||
<div class="footer-container" data-data-squidex-token={siteEditToken}>
|
<div class="footer-container" data-squidex-token={siteEditToken}>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-header">
|
<div class="footer-header">
|
||||||
<Fragment set:html={interpolateString(getLocaleField(locale, site.siteBrand)!, renderContext)} />
|
<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">
|
<slot name="head">
|
||||||
<meta name="description" content={metaDescription} />
|
<meta name="description" content={metaDescription} />
|
||||||
{
|
{
|
||||||
|
@ -158,9 +158,9 @@ try {
|
||||||
}
|
}
|
||||||
</slot>
|
</slot>
|
||||||
<main>
|
<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
|
<ComponentRouter
|
||||||
pageEditToken={pageDto?.items[0].editToken||''}
|
pageEditToken={pageDto?.items[0].editToken}
|
||||||
componentRouter={getLocaleField(locale, page.content)||[]}
|
componentRouter={getLocaleField(locale, page.content)||[]}
|
||||||
homePage={homePage as Page}
|
homePage={homePage as Page}
|
||||||
isHomePage={isHomePage}
|
isHomePage={isHomePage}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro";
|
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 { 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 { SCHEMAS } from "../data/models/schemas";
|
||||||
import type { Brand } from "../data/models/multis/Brand";
|
import type { Brand } from "../data/models/multis/Brand";
|
||||||
import type { Page } from "../data/models/multis/Page";
|
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 ComponentRouter from "../components/ComponentRouter.astro";
|
||||||
import { renderMarkdown } from "../lib/rendering";
|
import { renderMarkdown } from "../lib/rendering";
|
||||||
import Disclaimers from "../components/Disclaimers.astro";
|
import Disclaimers from "../components/Disclaimers.astro";
|
||||||
|
import { client, TIMEOUT_IN_SECONDS } from "../data/core/client";
|
||||||
|
|
||||||
const { routeLookup, resource, id } = Astro.params;
|
const { routeLookup, resource, id } = Astro.params;
|
||||||
|
|
||||||
|
@ -80,13 +81,34 @@ else {
|
||||||
return Astro.rewrite(`/view/products/${locale}/${objectId}`);
|
return Astro.rewrite(`/view/products/${locale}/${objectId}`);
|
||||||
case SCHEMAS.SELLERS:
|
case SCHEMAS.SELLERS:
|
||||||
return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
|
return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
|
||||||
default:
|
|
||||||
return redirect404NotFound();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
console.log("routeLookup:", routeLookup!);
|
||||||
return redirect404NotFound();
|
const redirectDto = await getRedirectsByPreviousSlug(routeLookup!);
|
||||||
|
console.log("redirectDto:", redirectDto);
|
||||||
|
if (redirectDto && redirectDto.items && redirectDto.items.length > 0) {
|
||||||
|
console.log("getting redirectReferencesDto for", SCHEMAS.REDIRECTS, redirectDto.items[0].id);
|
||||||
|
let redirectReferencesDto = await client.contents.getReferences(SCHEMAS.REDIRECTS, redirectDto.items[0].id);
|
||||||
|
console.log("redirectReferencesDto:", redirectReferencesDto);
|
||||||
|
client.contents.getConten
|
||||||
|
// switch (redirectDto.items[0].data?.referenceSchema.iv) {
|
||||||
|
// case SCHEMAS.BRANDS:
|
||||||
|
// return Astro.rewrite(`/view/brands/${locale}/${objectId}`);
|
||||||
|
// case SCHEMAS.MARKETPLACES:
|
||||||
|
// return Astro.rewrite(`/view/marketplaces/${locale}/${objectId}`);
|
||||||
|
// case SCHEMAS.PAGES:
|
||||||
|
// return Astro.rewrite(`/view/pages/${locale}/${objectId}`);
|
||||||
|
// case SCHEMAS.PRODUCT_CATEGORIES:
|
||||||
|
// return Astro.rewrite(`/view/product-categories/${locale}/${objectId}`);
|
||||||
|
// case SCHEMAS.PRODUCTS:
|
||||||
|
// return Astro.rewrite(`/view/products/${locale}/${objectId}`);
|
||||||
|
// case SCHEMAS.SELLERS:
|
||||||
|
// return Astro.rewrite(`/view/sellers/${locale}/${objectId}`);
|
||||||
|
// }
|
||||||
|
// Astro.response.headers.set('Vary', 'Accept-Language');
|
||||||
|
// return Astro.redirect(,303)
|
||||||
}
|
}
|
||||||
|
return redirect404NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
@ -114,7 +136,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot slot="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getBrandsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
import { getBrandsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
||||||
import { genericPageForBrand } from "../../../lib/page-from-models";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -65,7 +65,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getMarketplacesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
import { getMarketplacesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
||||||
import { genericPageForMarketplace } from "../../../lib/page-from-models";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -65,7 +65,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getPagesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -71,7 +71,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getProductCategoriesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
import { getProductCategoriesByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
||||||
import { genericPageForProductCategory } from "../../../lib/page-from-models";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -66,7 +66,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getProductsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
import { getProductsByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
||||||
import { genericPageForProduct } from "../../../lib/page-from-models";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -69,7 +69,7 @@ const renderContext = {
|
||||||
// disclaimers,
|
// 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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { getSellersByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
import { getSellersByIds, getSite, getSiteConfig, getSiteHomePage } from "../../../data/api-client";
|
||||||
import { genericPageForSeller } from "../../../lib/page-from-models";
|
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 { Page } from "../../../data/models/multis/Page";
|
||||||
import type { Site } from "../../../data/models/singles/Site";
|
import type { Site } from "../../../data/models/singles/Site";
|
||||||
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
import { pick as pickLanguage } from "../../../lib/accept-language-parser";
|
||||||
|
@ -65,7 +65,7 @@ const renderContext = {
|
||||||
shouldEmbedSquidexSDK,
|
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">
|
<slot name="head">
|
||||||
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
{ metaDescription && <meta name="description" content={metaDescription} /> }
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user