From b9cb60e3d0417a2377146859d6d6dca2144b633e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Sat, 21 Feb 2026 03:04:38 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=BE=E7=AE=80api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(payload)/admin/importMap.js | 2 + src/app/api/admin/route.ts | 109 ++++++++ src/app/api/batch-sync-medusa/route.ts | 83 +++--- src/app/api/clear-data/route.ts | 166 ------------ .../api/clear-payload-collections/route.ts | 50 ---- src/app/api/diagnose-products/route.ts | 42 --- src/app/api/payload-stats/route.ts | 32 --- .../api/restore-recommendations-seed/route.ts | 52 ++++ .../seed/RestoreRecommendationsSeedButton.tsx | 254 ++++++++++++++++++ .../20260221-product-recommendations-seeds.ts | 55 ++++ src/components/seed/index.ts | 3 + src/globals/ProductRecommendations.ts | 14 + 12 files changed, 533 insertions(+), 329 deletions(-) create mode 100644 src/app/api/admin/route.ts delete mode 100644 src/app/api/clear-data/route.ts delete mode 100644 src/app/api/clear-payload-collections/route.ts delete mode 100644 src/app/api/diagnose-products/route.ts delete mode 100644 src/app/api/payload-stats/route.ts create mode 100644 src/app/api/restore-recommendations-seed/route.ts create mode 100644 src/components/seed/RestoreRecommendationsSeedButton.tsx create mode 100644 src/components/seed/data/20260221-product-recommendations-seeds.ts create mode 100644 src/components/seed/index.ts diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 29a6f39..2bfdfcc 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -35,6 +35,7 @@ import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228 import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler' import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel' import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView' +import { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' @@ -76,6 +77,7 @@ export const importMap = { "/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, + "/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts new file mode 100644 index 0000000..ab0ccb3 --- /dev/null +++ b/src/app/api/admin/route.ts @@ -0,0 +1,109 @@ +import { getPayload } from 'payload' +import config from '@payload-config' +import { NextRequest, NextResponse } from 'next/server' + +/** + * Admin Management API + * Combined endpoint for data clearing, stats, and diagnostics + */ + +/** + * GET /api/admin?action=stats + * Get Payload collection statistics + */ +async function getStats() { + const payload = await getPayload({ config }) + + const [products, preorderProducts] = await Promise.all([ + payload.find({ collection: 'products', limit: 0 }), + payload.find({ collection: 'preorder-products', limit: 0 }), + ]) + + return { + products: products.totalDocs, + preorderProducts: preorderProducts.totalDocs, + total: products.totalDocs + preorderProducts.totalDocs, + } +} + +/** + * DELETE /api/admin + * Clear Payload data (preserves Users and Media) + * Query params: ?collections=products,preorderProducts,announcements,articles + */ +async function clearData(searchParams: URLSearchParams) { + const payload = await getPayload({ config }) + + const collectionsParam = searchParams.get('collections') + const collections = collectionsParam + ? collectionsParam.split(',') + : ['products', 'preorder-products', 'announcements', 'articles'] + + const results: Record = {} + const errors: string[] = [] + + for (const collection of collections) { + // Protect critical collections + if (['users', 'media'].includes(collection)) { + errors.push(`Skipped protected collection: ${collection}`) + continue + } + + try { + const deleted = await payload.delete({ + collection: collection as any, + where: {}, + }) + results[collection] = deleted.docs?.length || 0 + console.log(`✅ Cleared ${results[collection]} documents from ${collection}`) + } catch (error) { + const errorMsg = `Failed to clear ${collection}: ${error instanceof Error ? error.message : 'Unknown error'}` + console.error('❌', errorMsg) + errors.push(errorMsg) + } + } + + return { + success: true, + message: 'Data cleared successfully', + results, + errors: errors.length > 0 ? errors : undefined, + } +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const action = searchParams.get('action') + + if (action === 'stats') { + const stats = await getStats() + return NextResponse.json({ success: true, ...stats }) + } + + return NextResponse.json({ + success: false, + error: 'Invalid action. Valid actions: stats', + }, { status: 400 }) + } catch (error) { + console.error('[admin] GET error:', error) + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, { status: 500 }) + } +} + +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const result = await clearData(searchParams) + return NextResponse.json(result) + } catch (error) { + console.error('[admin] DELETE error:', error) + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, { status: 500 }) + } +} diff --git a/src/app/api/batch-sync-medusa/route.ts b/src/app/api/batch-sync-medusa/route.ts index 46fb31c..b0ce594 100644 --- a/src/app/api/batch-sync-medusa/route.ts +++ b/src/app/api/batch-sync-medusa/route.ts @@ -1,11 +1,12 @@ import { getPayload } from 'payload' import config from '@payload-config' import { NextResponse } from 'next/server' +import { getAllMedusaProducts } from '@/lib/medusa' /** - * 批量同步选中的产品到 Medusa + * Batch Sync Selected Products * POST /api/batch-sync-medusa - * Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate: boolean } + * Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean } */ export async function POST(request: Request) { try { @@ -14,83 +15,87 @@ export async function POST(request: Request) { if (!ids || !Array.isArray(ids) || ids.length === 0) { return NextResponse.json( - { - success: false, - error: 'No product IDs provided', - }, + { success: false, error: 'No product IDs provided' }, { status: 400 }, ) } if (!collection || !['products', 'preorder-products'].includes(collection)) { return NextResponse.json( - { - success: false, - error: 'Invalid collection', - }, + { success: false, error: 'Invalid collection' }, { status: 400 }, ) } const payload = await getPayload({ config }) + + // Get all Medusa products once + const medusaProducts = await getAllMedusaProducts() + const medusaProductMap = new Map(medusaProducts.map(p => [p.id, p])) const results = { total: ids.length, success: 0, failed: 0, + skipped: 0, details: [] as any[], } - // 逐个同步选中的产品 + // Sync each selected product for (const id of ids) { try { - // 获取产品信息 const product = await payload.findByID({ collection: collection as 'products' | 'preorder-products', id, }) if (!product || !product.medusaId) { - results.failed++ + results.skipped++ results.details.push({ id, title: product?.title || 'Unknown', - status: 'failed', - error: 'No Medusa ID', + status: 'skipped', + reason: 'No Medusa ID', }) continue } - // 调用单个产品同步 API - const syncResponse = await fetch( - `${request.url.replace('/api/batch-sync-medusa', '/api/sync-medusa')}?medusaId=${product.medusaId}&forceUpdate=${forceUpdate}`, - { - method: 'GET', - headers: request.headers, - }, - ) - - const syncResult = await syncResponse.json() - - if (syncResult.success) { - results.success++ - results.details.push({ - id, - medusaId: product.medusaId, - title: product.title, - status: 'success', - action: syncResult.action, - }) - } else { + const medusaProduct = medusaProductMap.get(product.medusaId) + + if (!medusaProduct) { results.failed++ results.details.push({ id, medusaId: product.medusaId, title: product.title, status: 'failed', - error: syncResult.error || syncResult.message, + error: 'Product not found in Medusa', }) + continue } + + // Update basic fields from Medusa + const updateData: any = { + lastSyncedAt: new Date().toISOString(), + } + + if (forceUpdate || !product.title) updateData.title = medusaProduct.title + if (forceUpdate || !product.handle) updateData.handle = medusaProduct.handle + if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail?.url + + await payload.update({ + collection: collection as 'products' | 'preorder-products', + id, + data: updateData, + }) + + results.success++ + results.details.push({ + id, + medusaId: product.medusaId, + title: product.title, + status: 'success', + }) } catch (error) { results.failed++ results.details.push({ @@ -103,11 +108,11 @@ export async function POST(request: Request) { return NextResponse.json({ success: true, - message: `批量同步完成: ${results.success} 成功, ${results.failed} 失败`, + message: `Batch sync completed: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`, results, }) } catch (error) { - console.error('Batch sync error:', error) + console.error('[batch-sync-medusa] Error:', error) return NextResponse.json( { success: false, diff --git a/src/app/api/clear-data/route.ts b/src/app/api/clear-data/route.ts deleted file mode 100644 index 5dc782d..0000000 --- a/src/app/api/clear-data/route.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { getPayload } from 'payload' -import config from '@payload-config' -import { NextResponse } from 'next/server' - -/** - * 清理数据库数据(保留 Users 和 Media) - * GET /api/clear-data?confirm=true - */ -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url) - const confirm = searchParams.get('confirm') - - // 安全检查:必须明确确认 - if (confirm !== 'true') { - return NextResponse.json( - { - success: false, - error: '需要确认参数:?confirm=true', - message: - '此操作将删除 Products, Announcements, Articles 的所有数据(保留 Users 和 Media)', - }, - { status: 400 }, - ) - } - - const payload = await getPayload({ config }) - - const results = { - products: 0, - preorderProducts: 0, - announcements: 0, - articles: 0, - errors: [] as string[], - } - - // 清理 Products - try { - const deletedProducts = await payload.delete({ - collection: 'products', - where: {}, - }) - results.products = deletedProducts.docs?.length || 0 - console.log(`✅ 已清理 ${results.products} 个商品`) - } catch (error) { - const errorMsg = `清理 Products 失败: ${error instanceof Error ? error.message : '未知错误'}` - console.error('❌', errorMsg) - results.errors.push(errorMsg) - } - - // 清理 Preorder Products - try { - const deletedPreorderProducts = await payload.delete({ - collection: 'preorder-products', - where: {}, - }) - results.preorderProducts = deletedPreorderProducts.docs?.length || 0 - console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`) - } catch (error) { - const errorMsg = `清理 Preorder Products 失败: ${error instanceof Error ? error.message : '未知错误'}` - console.error('❌', errorMsg) - results.errors.push(errorMsg) - } - - // 清理 Announcements - try { - const deletedAnnouncements = await payload.delete({ - collection: 'announcements', - where: {}, - }) - results.announcements = deletedAnnouncements.docs?.length || 0 - console.log(`✅ 已清理 ${results.announcements} 个公告`) - } catch (error) { - const errorMsg = `清理 Announcements 失败: ${error instanceof Error ? error.message : '未知错误'}` - console.error('❌', errorMsg) - results.errors.push(errorMsg) - } - - // 清理 Articles - try { - const deletedArticles = await payload.delete({ - collection: 'articles', - where: {}, - }) - results.articles = deletedArticles.docs?.length || 0 - console.log(`✅ 已清理 ${results.articles} 个文章`) - } catch (error) { - const errorMsg = `清理 Articles 失败: ${error instanceof Error ? error.message : '未知错误'}` - console.error('❌', errorMsg) - results.errors.push(errorMsg) - } - - return NextResponse.json({ - success: true, - message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`, - results, - }) - } catch (error) { - console.error('Clear data error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ) - } -} - -/** - * POST /api/clear-data - * 带认证的清理接口 - */ -export async function POST(request: Request) { - try { - const payload = await getPayload({ config }) - - // 可选:添加认证检查 - // const { user } = await payload.auth({ headers: request.headers }) - // if (!user?.roles?.includes('admin')) { - // return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - // } - - const body = await request.json() - const { collections = ['products', 'announcements', 'articles'] } = body - - const results: Record = {} - const errors: string[] = [] - - for (const collection of collections) { - if (['users', 'media'].includes(collection)) { - errors.push(`跳过受保护的集合: ${collection}`) - continue - } - - try { - const deleted = await payload.delete({ - collection, - where: {}, - }) - results[collection] = deleted.docs?.length || 0 - console.log(`✅ 已清理 ${results[collection]} 个 ${collection}`) - } catch (error) { - const errorMsg = `清理 ${collection} 失败: ${error instanceof Error ? error.message : '未知错误'}` - console.error('❌', errorMsg) - errors.push(errorMsg) - } - } - - return NextResponse.json({ - success: true, - message: '数据清理完成', - results, - errors: errors.length > 0 ? errors : undefined, - }) - } catch (error) { - console.error('Clear data error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/clear-payload-collections/route.ts b/src/app/api/clear-payload-collections/route.ts deleted file mode 100644 index 8dea8d4..0000000 --- a/src/app/api/clear-payload-collections/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getPayload } from 'payload' -import config from '@payload-config' -import { NextResponse } from 'next/server' - -export async function POST() { - try { - const payload = await getPayload({ config }) - - // Delete all products - const products = await payload.find({ - collection: 'products', - limit: 1000, - }) - - for (const product of products.docs) { - await payload.delete({ - collection: 'products', - id: product.id, - }) - } - - // Delete all preorder products - const preorderProducts = await payload.find({ - collection: 'preorder-products', - limit: 1000, - }) - - for (const product of preorderProducts.docs) { - await payload.delete({ - collection: 'preorder-products', - id: product.id, - }) - } - - return NextResponse.json({ - success: true, - message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`, - deleted: { - products: products.docs.length, - preorderProducts: preorderProducts.docs.length, - }, - }) - } catch (error) { - console.error('[clear-payload-collections] Error:', error) - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : String(error), - }, { status: 500 }) - } -} diff --git a/src/app/api/diagnose-products/route.ts b/src/app/api/diagnose-products/route.ts deleted file mode 100644 index ad18b1a..0000000 --- a/src/app/api/diagnose-products/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getAllMedusaProducts } from '@/lib/medusa' -import { NextResponse } from 'next/server' - -/** - * 诊断 API - 检查 Medusa 产品的 metadata - * GET /api/diagnose-products - */ -export async function GET() { - try { - const products = await getAllMedusaProducts() - - const diagnosis = products.map((product) => ({ - id: product.id, - title: product.title, - handle: product.handle, - metadata: product.metadata, - is_preorder_value: product.metadata?.is_preorder, - is_preorder_type: typeof product.metadata?.is_preorder, - should_be_in_collection: - product.metadata?.is_preorder === true || product.metadata?.is_preorder === 'true' - ? 'preorder-products' - : 'products', - })) - - return NextResponse.json({ - success: true, - total: products.length, - preorder_count: diagnosis.filter((d) => d.should_be_in_collection === 'preorder-products') - .length, - regular_count: diagnosis.filter((d) => d.should_be_in_collection === 'products').length, - products: diagnosis, - }) - } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ) - } -} diff --git a/src/app/api/payload-stats/route.ts b/src/app/api/payload-stats/route.ts deleted file mode 100644 index 0c9e347..0000000 --- a/src/app/api/payload-stats/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getPayload } from 'payload' -import config from '@payload-config' -import { NextResponse } from 'next/server' - -export async function GET() { - try { - const payload = await getPayload({ config }) - - const products = await payload.find({ - collection: 'products', - limit: 0, // Just get count - }) - - const preorderProducts = await payload.find({ - collection: 'preorder-products', - limit: 0, // Just get count - }) - - return NextResponse.json({ - success: true, - products: products.totalDocs, - preorderProducts: preorderProducts.totalDocs, - total: products.totalDocs + preorderProducts.totalDocs, - }) - } catch (error) { - console.error('[payload-stats] Error:', error) - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : String(error), - }, { status: 500 }) - } -} diff --git a/src/app/api/restore-recommendations-seed/route.ts b/src/app/api/restore-recommendations-seed/route.ts new file mode 100644 index 0000000..4c29652 --- /dev/null +++ b/src/app/api/restore-recommendations-seed/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getPayload } from 'payload' +import config from '@payload-config' + +/** + * API Route: Restore Product Recommendations from Seed + * POST /api/restore-recommendations-seed + * + * This server-side route uses Payload's local API to update the global config + * which requires proper authentication context that client-side fetch doesn't have. + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { enabled, lists } = body + + if (!lists || !Array.isArray(lists)) { + return NextResponse.json( + { success: false, error: 'Invalid lists data' }, + { status: 400 } + ) + } + + // Get Payload instance with proper context + const payload = await getPayload({ config }) + + // Update the global using Payload's local API + const result = await payload.updateGlobal({ + slug: 'product-recommendations', + data: { + enabled: enabled ?? true, + lists: lists, + }, + }) + + return NextResponse.json({ + success: true, + message: `Successfully restored ${lists.length} recommendation list(s)`, + data: result, + }) + } catch (error) { + console.error('Error restoring recommendations seed:', error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/src/components/seed/RestoreRecommendationsSeedButton.tsx b/src/components/seed/RestoreRecommendationsSeedButton.tsx new file mode 100644 index 0000000..73a259b --- /dev/null +++ b/src/components/seed/RestoreRecommendationsSeedButton.tsx @@ -0,0 +1,254 @@ +'use client' +import { useState } from 'react' +import { Button } from '@payloadcms/ui' +import { AVAILABLE_SEEDS, type SeedKey } from './data/20260221-product-recommendations-seeds' + +interface Props { + className?: string +} + +/** + * Restore Recommendations Seed Button + * Quick restore predefined product recommendation configurations + */ +export function RestoreRecommendationsSeedButton({ className }: Props) { + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [showSeedSelector, setShowSeedSelector] = useState(false) + + /** + * Find products by seed IDs + * Returns polymorphic relationship format: { relationTo, value } + */ + const findProductsBySeedIds = async ( + seedIds: string[], + ): Promise> => { + const products: Array<{ relationTo: string; value: string }> = [] + + for (const seedId of seedIds) { + try { + // Try products collection first + const productsResponse = await fetch( + `/api/products?where[seedId][equals]=${seedId}&limit=1`, + ) + const productsData = await productsResponse.json() + + if (productsData.docs && productsData.docs.length > 0) { + products.push({ + relationTo: 'products', + value: productsData.docs[0].id, + }) + continue + } + + // Try preorder-products if not found + const preorderResponse = await fetch( + `/api/preorder-products?where[seedId][equals]=${seedId}&limit=1`, + ) + const preorderData = await preorderResponse.json() + + if (preorderData.docs && preorderData.docs.length > 0) { + products.push({ + relationTo: 'preorder-products', + value: preorderData.docs[0].id, + }) + } else { + console.warn(`Product not found for seedId: ${seedId}`) + } + } catch (error) { + console.error(`Error finding product ${seedId}:`, error) + } + } + + return products + } + + /** + * Restore recommendation list from seed + */ + const handleRestoreSeed = async (seedKey: SeedKey) => { + const seed = AVAILABLE_SEEDS[seedKey] + + if (!confirm( + `Restore recommendation list configuration?\n\n` + + `Will create:\n${seed.lists.map(list => `• ${list.title} (${list.productSeedIds.length} products)`).join('\n')}\n\n` + + `Current configuration will be overwritten!` + )) { + return + } + + setLoading(true) + setMessage('🔄 Finding products...') + + try { + // Find all product IDs in polymorphic relationship format + const listsWithProductIds = await Promise.all( + seed.lists.map(async (list) => { + const products = await findProductsBySeedIds(list.productSeedIds) + return { + title: list.title, + subtitle: list.subtitle || '', + products: products, + } + }), + ) + + // Filter out lists with no products found + const validLists = listsWithProductIds.filter((list) => list.products.length > 0) + + if (validLists.length === 0) { + setMessage('❌ No matching products found. Please run seed script first.') + setLoading(false) + return + } + + setMessage('💾 Updating configuration...') + + // Update product-recommendations global via server-side API + const updateResponse = await fetch('/api/restore-recommendations-seed', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + enabled: seed.enabled, + lists: validLists, + }), + }) + + const result = await updateResponse.json() + + if (!result.success) { + throw new Error(result.error || 'Update failed') + } + + setMessage(`✅ Successfully restored ${validLists.length} recommendation list(s)!`) + setShowSeedSelector(false) + + // Refresh page after 2 seconds + setTimeout(() => { + window.location.reload() + }, 2000) + } catch (error) { + console.error('Failed to restore seed configuration:', error) + setMessage('❌ Restore failed: ' + (error instanceof Error ? error.message : 'Unknown error')) + } finally { + setLoading(false) + } + } + + return ( +
+
+ +
+ + {showSeedSelector && ( +
+

+ 📦 Available Seed Configurations +

+ +
+ {Object.entries(AVAILABLE_SEEDS).map(([key, seed]) => ( +
+
+
+
+ {key === 'batch02' ? 'Batch 02 Recommendations' : key} +
+
+ {seed.lists.length} list(s), {' '} + {seed.lists.reduce((sum, list) => sum + list.productSeedIds.length, 0)} product(s) total +
+
+ +
+ +
+ {seed.lists.map((list, index) => ( +
+
+ {list.title} +
+
+ {list.subtitle || `${list.productSeedIds.length} product(s)`} +
+
+ ))} +
+
+ ))} +
+ + {message && ( +
+ {message} +
+ )} +
+ )} +
+ ) +} diff --git a/src/components/seed/data/20260221-product-recommendations-seeds.ts b/src/components/seed/data/20260221-product-recommendations-seeds.ts new file mode 100644 index 0000000..7b102ba --- /dev/null +++ b/src/components/seed/data/20260221-product-recommendations-seeds.ts @@ -0,0 +1,55 @@ +/** + * Product Recommendations Seed Data + * Predefined recommendation lists for quick restoration + * Date: 2026-02-21 + */ + +export interface RecommendationListSeed { + title: string + subtitle?: string + productSeedIds: string[] +} + +export interface RecommendationsSeed { + enabled: boolean + lists: RecommendationListSeed[] +} + +/** + * Batch 02 Recommendations + * - PreGame: Preorder games collection (4 games) + * - PreMod: Preorder modification collection (2 metal shells + 1 custom console) + */ +export const BATCH_02_RECOMMENDATIONS: RecommendationsSeed = { + enabled: true, + lists: [ + { + title: 'PreGame - Preorder Games', + subtitle: 'Selected indie games, now available for preorder! Support indie developers and get early access to new releases.', + productSeedIds: [ + 'game-urcicus', // Urcicus - GBA Game + 'game-mikoto-nikki', // Mikoto Nikki - GBA Game + 'game-passaway', // Passaway - GB Game + 'game-judys-adventure-dx', // Judys Adventure DX - GBA Game + ], + }, + { + title: 'PreMod - Preorder Modifications', + subtitle: 'Premium custom shells and consoles, full metal construction, artisan craftsmanship. Limited preorder available!', + productSeedIds: [ + 'shell-gba-sp-metal-unhinged', // Metal Shell - GBA SP (Unhinged Mod) + 'shell-gba-metal', // Metal Shell - GBA + 'console-retro-tetra', // Retro Tetra Console + ], + }, + ], +} + +/** + * All available seed configurations + */ +export const AVAILABLE_SEEDS = { + batch02: BATCH_02_RECOMMENDATIONS, +} + +export type SeedKey = keyof typeof AVAILABLE_SEEDS diff --git a/src/components/seed/index.ts b/src/components/seed/index.ts new file mode 100644 index 0000000..082025b --- /dev/null +++ b/src/components/seed/index.ts @@ -0,0 +1,3 @@ +export { RestoreRecommendationsSeedButton } from './RestoreRecommendationsSeedButton' +export { AVAILABLE_SEEDS, BATCH_02_RECOMMENDATIONS } from './data/20260221-product-recommendations-seeds' +export type { RecommendationListSeed, RecommendationsSeed, SeedKey } from './data/20260221-product-recommendations-seeds' diff --git a/src/globals/ProductRecommendations.ts b/src/globals/ProductRecommendations.ts index 0d13de3..a0e4e75 100644 --- a/src/globals/ProductRecommendations.ts +++ b/src/globals/ProductRecommendations.ts @@ -26,6 +26,20 @@ export const ProductRecommendations: GlobalConfig = { }, }, fields: [ + { + name: 'seedActions', + type: 'ui', + label: { + en: 'Quick Actions', + zh: '快捷操作', + }, + admin: { + position: 'sidebar', + components: { + Field: '/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton', + }, + }, + }, { name: 'enabled', type: 'checkbox',