This commit is contained in:
Your Name committed 2026-09-08 06:20:52 +07:00
1 parent f588091091
commit f8e11d5568
6 files changed
+930 -96

No files matched your search

+81 -1
View File
@@ -361,7 +361,34 @@
"soldLabel": "Sold:", "soldLabel": "Sold:",
"revenueLabel": "Revenue:", "revenueLabel": "Revenue:",
"avgPriceLabel": "Avg Price:", "avgPriceLabel": "Avg Price:",
"modalSearchPlaceholder": "Type to search master product, SKU code, or variant option...", "modalSearchPlaceholder": "Search by product name, SKU, barcode, variation, brand...",
"filterBySku": "SKU / Barcode",
"filterBySkuPlaceholder": "Search SKU or barcode...",
"filterByName": "Master Product Name",
"filterByNamePlaceholder": "Search product name...",
"filterByOption": "Variation / Option",
"filterByOptionPlaceholder": "e.g. 50ml, 100ml, scent...",
"filterCategory": "Category",
"allCategories": "All Categories",
"filterBrand": "Brand",
"allBrands": "All Brands",
"bomStatus": "BOM Status",
"allBom": "All BOM Status",
"hasBom": "Has BOM Recipe",
"noBomOnly": "No BOM Recipe",
"clearFilters": "Clear Filters",
"advancedFilters": "Detailed Filters",
"hideAdvancedFilters": "Hide Detailed Filters",
"quickSearchByMpSku": "🔍 Search by MP SKU: {{sku}}",
"quickSearchByMpName": "🔍 Search by MP Title",
"selectedMaster": "Selected Master Variant:",
"noSelection": "No master variant selected",
"resultsCount": "Showing {{count}} of {{total}} items",
"stockLabel": "Stock:",
"sortPriceAsc": "Price: Low -> High",
"sortPriceDesc": "Price: High -> Low",
"sortStock": "Highest Stock",
"sortSku": "SKU Code (A-Z)",
"noMasterFound": "No matching master catalog products found", "noMasterFound": "No matching master catalog products found",
"bomCostLabel": "BOM Cost:", "bomCostLabel": "BOM Cost:",
"unlink": "Unlink SKU", "unlink": "Unlink SKU",
@@ -1305,9 +1332,62 @@
"alertUrlCopied": "Image URL copied to clipboard!", "alertUrlCopied": "Image URL copied to clipboard!",
"alertUploadFailed": "Upload failed: {{error}}", "alertUploadFailed": "Upload failed: {{error}}",
"alertUploadGeneric": "Failed to upload media file." "alertUploadGeneric": "Failed to upload media file."
},
"cmsMockup": {
"title": "CMS Blocks Showcase",
"subtitle": "Visual preview and template schema payloads for all available Page Builder blocks.",
"searchPlaceholder": "Search blocks by name, type, or description...",
"showingBlocks": "Showing {{count}} of {{total}} blocks",
"noBlocksFound": "No blocks found matching \"{{search}}\"",
"viewPayload": "View Payload",
"hidePayload": "Hide Payload",
"copyPayload": "Copy Payload",
"copied": "Copied",
"categories": {
"all": "All Blocks",
"hero": "Banner & Hero",
"products": "Products & Shop",
"editorial": "Story & Editorial",
"articles": "Article Elements",
"forms": "Forms & Info"
},
"blocks": {
"ANNOUNCEMENT_BAR": "Top notice bar with custom styling and scrolling announcements.",
"HERO": "Premium full-screen cover with headline, background image, and CTA.",
"HERO_V2": "Split Layout Hero featuring text on one side and a premium image card on the other.",
"HERO_VIDEO": "Cinema-quality full background video banner with animated overlay typography and dual CTA buttons.",
"USP_BAR": "Unique Selling Proposition Bar highlighting product perks (Icons & Labels).",
"BANNER": "Clickable promotional block with headline overlay, ideal for campaigns.",
"CATEGORIES": "Visual category grid showcase to guide users to specific collections.",
"PRODUCTS": "Showcase grid or carousel for featured products, brand items, or a customized list with Quick Shop option.",
"PRODUCT_CAROUSEL": "Swipeable horizontal card carousel displaying specific product collections.",
"RELATED_PRODUCTS": "Dynamic block recommending items from the same category for the product page layout.",
"EDITORIAL_TILE": "Stunning editorial grid block presenting storytelling text alongside a curated campaign photo.",
"FEATURED_TILES": "Side-by-side campaign grid featuring square or tall images with centered titles and outline CTA buttons (Jo Malone aesthetic).",
"EDITORIAL_CAROUSEL": "Stories and values slider displaying image slideshows side-by-side with localized editorial statements.",
"SCENT_NOTES_BREAKDOWN": "Fragrance profile breakdowns containing Top, Heart/Middle, and Base notes lists.",
"QUOTE_CALLOUT": "Elegant text banner presenting callout quotations, ideal for customer testimonials.",
"TEXT": "Plain text component for headers, centered paragraphs, or brand catchphrases.",
"RICH_TEXT": "Styled markdown block designed for longer stories or HTML embedding.",
"HERO_ARTICLE": "Editorial article header with background blur, date, author name, and title overlays.",
"ARTICLE_HEADER": "Introductory page header for standard text articles.",
"ARTICLE_MEDIA": "Article paragraph paired with a side image (left/right alignment).",
"ARTICLE_GALLERY": "Photo collection grids to show off multiple illustrations or steps.",
"ARTICLE_FACTS": "Quick summaries or key takeaway bullets for editorial pieces.",
"ARTICLE_VIDEO": "Embedded video player wrapper for articles.",
"ARTICLE_REFERENCES": "Citation list or scientific reference cards.",
"ARTICLE_TABLE": "Comparison table displaying technical parameters or specs.",
"ARTICLE_DOWNLOAD": "File attachments download cards (PDF guides, catalogues).",
"ARTICLE_CTA": "Call-to-action blocks prompting next-step user clicks.",
"NEWSLETTER": "Email subscription block to attract newsletter sign-ups.",
"ACCORDION": "Question-answer accordions for FAQs, divided by custom categories.",
"CONTACT_INFO": "Beautiful information box showing address, telephone, and social details.",
"CONTACT_FORM": "General support email inquiry contact form."
}
} }
} }
+81 -1
View File
@@ -359,7 +359,34 @@
"soldLabel": "販売数:", "soldLabel": "販売数:",
"revenueLabel": "売上:", "revenueLabel": "売上:",
"avgPriceLabel": "平均単価:", "avgPriceLabel": "平均単価:",
"modalSearchPlaceholder": "マスター商品名、SKUコード、容量(例: 50ml)を入力して検索...", "modalSearchPlaceholder": "一括検索 (商品名、SKU、バーコード、容量、ブランド)...",
"filterBySku": "SKUコード / バーコード",
"filterBySkuPlaceholder": "SKUコードまたはバーコードを入力...",
"filterByName": "マスター商品名",
"filterByNamePlaceholder": "商品名を入力...",
"filterByOption": "規格 / バリエーション",
"filterByOptionPlaceholder": "例: 50ml, 100ml, 香り...",
"filterCategory": "カテゴリ",
"allCategories": "全カテゴリ",
"filterBrand": "ブランド",
"allBrands": "全ブランド",
"bomStatus": "BOMステータス",
"allBom": "すべてのBOM",
"hasBom": "BOM設定済",
"noBomOnly": "BOM未設定",
"clearFilters": "フィルター解除",
"advancedFilters": "詳細フィルター",
"hideAdvancedFilters": "詳細フィルターを閉じる",
"quickSearchByMpSku": "🔍 モールSKUで検索: {{sku}}",
"quickSearchByMpName": "🔍 モール商品名で検索",
"selectedMaster": "選択中のマスター規格:",
"noSelection": "マスター商品が選択されていません",
"resultsCount": "{{count}} / {{total}} 件を表示中",
"stockLabel": "在庫数:",
"sortPriceAsc": "価格が安い順",
"sortPriceDesc": "価格が高い順",
"sortStock": "在庫数が多い順",
"sortSku": "SKUコード順 (A-Z)",
"noMasterFound": "該当するマスター商品が見つかりません", "noMasterFound": "該当するマスター商品が見つかりません",
"bomCostLabel": "BOM原価:", "bomCostLabel": "BOM原価:",
"unlink": "紐付け解除", "unlink": "紐付け解除",
@@ -1302,9 +1329,62 @@
"alertUrlCopied": "画像URLをクリップボードにコピーしました!", "alertUrlCopied": "画像URLをクリップボードにコピーしました!",
"alertUploadFailed": "アップロードに失敗しました: {{error}}", "alertUploadFailed": "アップロードに失敗しました: {{error}}",
"alertUploadGeneric": "メディアファイルのアップロードに失敗しました。" "alertUploadGeneric": "メディアファイルのアップロードに失敗しました。"
},
"cmsMockup": {
"title": "CMSブロック見本 (Showcase)",
"subtitle": "ページビルダーで利用可能なすべてのブロックのプレビューとJSONテンプレートスキーマ",
"searchPlaceholder": "名前、タイプ、説明でブロックを検索...",
"showingBlocks": "{{total}}件中 {{count}}件のブロックを表示",
"noBlocksFound": "「{{search}}」に一致するブロックは見つかりませんでした",
"viewPayload": "Payloadを表示",
"hidePayload": "Payloadを非表示",
"copyPayload": "Payloadをコピー",
"copied": "コピー完了",
"categories": {
"all": "すべてのブロック",
"hero": "バナー & Hero",
"products": "商品 & ショップ",
"editorial": "ストーリー & 記事",
"articles": "記事要素",
"forms": "フォーム & 情報"
},
"blocks": {
"ANNOUNCEMENT_BAR": "カスタムスタイルとスクロールお知らせ付きのトップ告知バー",
"HERO": "見出し、背景画像、CTAボタンを備えたプレミアムフルスクリーンカバー",
"HERO_V2": "片側にテキスト、もう片側に商品カードを配置したスプリットレイアウトHero",
"HERO_VIDEO": "シネマ品質のフル背景動画バナー(アニメーションタイポグラフィとデュアルCTA付き)",
"USP_BAR": "商品の強みや特典を強調するUSPバー(アイコンとラベル)",
"BANNER": "画像オーバーレイ付きのクリック可能なプロモーションバナー(キャンペーンに最適)",
"CATEGORIES": "特定のコレクションに誘導するビジュアルカテゴリグリッド",
"PRODUCTS": "おすすめ商品や注目アイテムを紹介するグリッド/カルーセル(クイック注文対応)",
"PRODUCT_CAROUSEL": "特定の商品コレクションを横スクロールで表示するスワイプ対応カルーセル",
"RELATED_PRODUCTS": "商品詳細ページ向けに関連商品を自動推薦する動的ブロック",
"EDITORIAL_TILE": "厳選されたキャンペーン写真とともにストーリーを伝えるエディトリアルグリッド",
"FEATURED_TILES": "正方形または縦型画像を並べたキャンペーングリッド(Jo Malone風スタイル)",
"EDITORIAL_CAROUSEL": "ブランドの価値やストーリーを画像スライドショーとともに伝えるカルーセル",
"SCENT_NOTES_BREAKDOWN": "Top、Heart/Middle、Baseノートを視覚的に分解した香りの構成表",
"QUOTE_CALLOUT": "お客様の声や引用文を上品に表示するテキストバナー",
"TEXT": "見出し、中央揃えテキスト、ブランドスローガン向けのプレーンテキストブロック",
"RICH_TEXT": "長文ストーリーやHTML埋め込みに対応した装飾付きリッチテキスト",
"HERO_ARTICLE": "背景ぼかし、日付、著者名、タイトルを表示する記事ヘッダー",
"ARTICLE_HEADER": "標準テキスト記事用の導入ヘッダー",
"ARTICLE_MEDIA": "横並び画像付きの記事パラグラフ(左右配置対応)",
"ARTICLE_GALLERY": "複数のイラストや手順を並べて表示するフォトギャラリーグリッド",
"ARTICLE_FACTS": "記事の重要ポイントや要約を箇条書きでまとめるファクトボックス",
"ARTICLE_VIDEO": "記事埋め込み用の動画プレイヤーラッパー",
"ARTICLE_REFERENCES": "学術引用や参考文献リンクを整理して表示するリスト",
"ARTICLE_TABLE": "仕様や技術パラメーターを分かりやすく比較するデータテーブル",
"ARTICLE_DOWNLOAD": "添付ファイルダウンロードカード(PDFガイド、カタログなど)",
"ARTICLE_CTA": "読者の次のアクションを促すCall to Actionブロック",
"NEWSLETTER": "限定特典や割引を提供するニュースレター購読フォーム",
"ACCORDION": "よくある質問(FAQ)をカテゴリごとに整理できるアコーディオン",
"CONTACT_INFO": "住所、電話番号、SNSなどを美しくまとめた問い合わせ先情報ボックス",
"CONTACT_FORM": "一般的な問い合わせを受け付けるメールフォーム"
}
} }
} }
+81 -1
View File
@@ -361,7 +361,34 @@
"soldLabel": "ขายแล้ว:", "soldLabel": "ขายแล้ว:",
"revenueLabel": "ยอดขาย:", "revenueLabel": "ยอดขาย:",
"avgPriceLabel": "ราคาเฉลี่ย:", "avgPriceLabel": "ราคาเฉลี่ย:",
"modalSearchPlaceholder": "พิมพ์ค้นหาชื่อสินค้าหลัก, รหัส SKU, หรือขนาด (เช่น 50ml, 120ml)...", "modalSearchPlaceholder": "ค้นหาทั่วไป (ชื่อสินค้า, SKU, บาร์โค้ด, ขนาด, แบรนด์)...",
"filterBySku": "รหัส SKU / Barcode",
"filterBySkuPlaceholder": "ระบุรหัส SKU หรือ บาร์โค้ด...",
"filterByName": "ชื่อสินค้า Master",
"filterByNamePlaceholder": "ระบุชื่อสินค้า...",
"filterByOption": "ขนาด / ตัวเลือก",
"filterByOptionPlaceholder": "เช่น 50ml, 100ml, กลิ่น...",
"filterCategory": "หมวดหมู่",
"allCategories": "ทุกหมวดหมู่",
"filterBrand": "แบรนด์",
"allBrands": "ทุกแบรนด์",
"bomStatus": "สถานะ BOM",
"allBom": "ทุกสถานะ BOM",
"hasBom": "มีสูตร BOM แล้ว",
"noBomOnly": "ยังไม่มีสูตร BOM",
"clearFilters": "ล้างตัวกรอง",
"advancedFilters": "ตัวกรองละเอียด",
"hideAdvancedFilters": "ซ่อนตัวกรองละเอียด",
"quickSearchByMpSku": "🔍 ค้นหาด้วย SKU Marketplace: {{sku}}",
"quickSearchByMpName": "🔍 ค้นหาด้วยชื่อสินค้านี้",
"selectedMaster": "รายการ Master ที่เลือก:",
"noSelection": "ยังไม่ได้เลือกรายการใด",
"resultsCount": "พบ {{count}} จาก {{total}} รายการ",
"stockLabel": "คงเหลือ:",
"sortPriceAsc": "ราคา: ต่ำ -> สูง",
"sortPriceDesc": "ราคา: สูง -> ต่ำ",
"sortStock": "สต็อกคงเหลือสูงสุด",
"sortSku": "รหัส SKU (A-Z)",
"noMasterFound": "ไม่พบรายการสินค้า Master ในระบบที่ตรงกับคำค้นหา", "noMasterFound": "ไม่พบรายการสินค้า Master ในระบบที่ตรงกับคำค้นหา",
"bomCostLabel": "ต้นทุน BOM:", "bomCostLabel": "ต้นทุน BOM:",
"unlink": "ยกเลิกการผูก", "unlink": "ยกเลิกการผูก",
@@ -1304,9 +1331,62 @@
"alertUrlCopied": "คัดลอก URL รูปภาพลงคลิปบอร์ดแล้ว!", "alertUrlCopied": "คัดลอก URL รูปภาพลงคลิปบอร์ดแล้ว!",
"alertUploadFailed": "อัปโหลดไม่สำเร็จ: {{error}}", "alertUploadFailed": "อัปโหลดไม่สำเร็จ: {{error}}",
"alertUploadGeneric": "ไม่สามารถอัปโหลดไฟล์สื่อได้" "alertUploadGeneric": "ไม่สามารถอัปโหลดไฟล์สื่อได้"
},
"cmsMockup": {
"title": "คลังตัวอย่าง CMS Blocks (Showcase)",
"subtitle": "ตัวอย่างการแสดงผลจริงและโครงสร้างข้อมูล JSON Template สำหรับบล็อกทั้งหมดในระบบ Page Builder",
"searchPlaceholder": "ค้นหาบล็อกตามชื่อ ประเภท หรือคำอธิบาย...",
"showingBlocks": "แสดง {{count}} จากทั้งหมด {{total}} บล็อก",
"noBlocksFound": "ไม่พบบล็อกที่ตรงกับ \"{{search}}\"",
"viewPayload": "ดู Payload",
"hidePayload": "ซ่อน Payload",
"copyPayload": "คัดลอก Payload",
"copied": "คัดลอกแล้ว",
"categories": {
"all": "ทุกบล็อก",
"hero": "แบนเนอร์ & Hero",
"products": "สินค้า & ร้านค้า",
"editorial": "เรื่องราว & บทความ",
"articles": "องค์ประกอบบทความ",
"forms": "ฟอร์ม & ข้อมูลติดต่อ"
},
"blocks": {
"ANNOUNCEMENT_BAR": "แถบประกาศด้านบนสุด พร้อมข้อความวิ่งและการจัดสไตล์ที่กำหนดเองได้",
"HERO": "แบนเนอร์เต็มจอระดับพรีเมียม พร้อมหัวข้อ รูปพื้นหลัง และปุ่ม Call-to-Action",
"HERO_V2": "แบนเนอร์ Hero แบบแยกสองฝั่ง จัดวางข้อความเด่นฝั่งหนึ่งและการ์ดภาพสินค้าอีกฝั่ง",
"HERO_VIDEO": "แบนเนอร์วิดีโอพื้นหลังคุณภาพระดับภาพยนตร์ พร้อมตัวอักษรและปุ่ม CTA คู่",
"USP_BAR": "แถบจุดเด่นสินค้า (USP) แสดงไอคอนและข้อความการันตีคุณภาพ",
"BANNER": "แบนเนอร์โปรโมชันแบบคลิกได้ พร้อมข้อความทับบนรูปภาพ เหมาะสำหรับแคมเปญ",
"CATEGORIES": "กริดแสดงหมวดหมู่สินค้าเพื่อนำทางลูกค้าไปยังคอลเลกชันที่ต้องการ",
"PRODUCTS": "บล็อกแสดงสินค้าแบบกริดหรือสไลด์สำหรับสินค้าแนะนำ หรือเลือกสินค้าพร้อมระบบสั่งด่วน",
"PRODUCT_CAROUSEL": "สไลด์การ์ดสินค้าแนวนอน เลื่อนดูสินค้าในคอลเลกชันได้อย่างลื่นไหล",
"RELATED_PRODUCTS": "บล็อกแนะนำสินค้าที่เกี่ยวข้องจากหมวดหมู่เดียวกัน สำหรับหน้ารายละเอียดสินค้า",
"EDITORIAL_TILE": "บล็อกเล่าเรื่องสไตล์ Editorial จัดวางเนื้อหาคู่กับภาพถ่ายแคมเปญ",
"FEATURED_TILES": "กริดแคมเปญภาพคู่แบบจัตุรัสหรือแนวตั้ง พร้อมหัวข้อกึ่งกลางและปุ่มเรียบหรู (Jo Malone Style)",
"EDITORIAL_CAROUSEL": "สไลด์เรื่องราวและคุณค่าของแบรนด์ แสดงภาพคู่กับข้อความบรรยายอย่างลงตัว",
"SCENT_NOTES_BREAKDOWN": "โครงสร้างจำแนกระดับกลิ่นหอม แสดงโน้ต Top, Heart/Middle และ Base",
"QUOTE_CALLOUT": "กล่องโควทข้อความและคำคมระดับพรีเมียม เหมาะสำหรับรีวิวและคำนิยม",
"TEXT": "คอมโพเนนต์ข้อความทั่วไป สำหรับหัวข้อ ย่อหน้า หรือสโลแกนของแบรนด์",
"RICH_TEXT": "บล็อกข้อความจัดรูปแบบ Markdown/HTML สำหรับบทความยาวหรือเนื้อหาเชิงลึก",
"HERO_ARTICLE": "ส่วนหัวบทความสไตล์นิตยสาร พร้อมพื้นหลังเบลอ วันที่ ชื่อผู้เขียน และหัวข้อ",
"ARTICLE_HEADER": "ส่วนหัวบทความมาตรฐานสำหรับบทความทั่วไป",
"ARTICLE_MEDIA": "ย่อหน้าเนื้อหาบทความจับคู่กับรูปภาพประกอบ (จัดชิดซ้ายหรือขวา)",
"ARTICLE_GALLERY": "แกลเลอรีภาพถ่ายสำหรับแสดงภาพขั้นตอนหรือรูปภาพประกอบหลายภาพ",
"ARTICLE_FACTS": "กล่องสรุปข้อเท็จจริง สาระสำคัญ หรือประเด็นสำคัญของบทความ",
"ARTICLE_VIDEO": "ตัวเล่นวิดีโอฝังสำหรับบทความ (รองรับ YouTube/วิดีโอ)",
"ARTICLE_REFERENCES": "รายการเอกสารอ้างอิง แหล่งข้อมูลทางวิชาการ หรือลิงก์ที่เกี่ยวข้อง",
"ARTICLE_TABLE": "ตารางเปรียบเทียบข้อมูลจำเพาะหรือพารามิเตอร์ทางเทคนิค",
"ARTICLE_DOWNLOAD": "การ์ดดาวน์โหลดไฟล์แนบ (คู่มือ PDF แคตตาล็อก ฯลฯ)",
"ARTICLE_CTA": "บล็อกกระตุ้นการตัดสินใจ (Call to Action) สำหรับชวนผู้อ่านคลิกทำรายการต่อ",
"NEWSLETTER": "ฟอร์มรับสมัครจดหมายข่าว เพื่อรับส่วนลดและข่าวสารโปรโมชัน",
"ACCORDION": "แถบพับเปิด-ปิดสำหรับคำถามที่พบบ่อย (FAQs) แยกตามหมวดหมู่",
"CONTACT_INFO": "กล่องข้อมูลติดต่อ แสดงที่อยู่ เบอร์โทร อีเมล และโซเชียลมีเดียอย่างสวยงาม",
"CONTACT_FORM": "ฟอร์มส่งข้อความติดต่อสอบถามทั่วไปทางอีเมล"
}
} }
} }
+81 -1
View File
@@ -359,7 +359,34 @@
"soldLabel": "销量:", "soldLabel": "销量:",
"revenueLabel": "销售额:", "revenueLabel": "销售额:",
"avgPriceLabel": "均价:", "avgPriceLabel": "均价:",
"modalSearchPlaceholder": "输入搜索主商品名、SKU 编码或容量规格(如 50ml)...", "modalSearchPlaceholder": "通用搜索(商品名、SKU、条形码、容量、品牌)...",
"filterBySku": "SKU 编码 / 条形码",
"filterBySkuPlaceholder": "输入 SKU 编码或条码...",
"filterByName": "主商品名称",
"filterByNamePlaceholder": "输入主商品名称...",
"filterByOption": "规格 / 选项",
"filterByOptionPlaceholder": "例如 50ml, 100ml...",
"filterCategory": "分类",
"allCategories": "全部分类",
"filterBrand": "品牌",
"allBrands": "全部品牌",
"bomStatus": "BOM 状态",
"allBom": "全部 BOM 状态",
"hasBom": "已有 BOM 配方",
"noBomOnly": "尚无 BOM 配方",
"clearFilters": "重置筛选",
"advancedFilters": "详细筛选",
"hideAdvancedFilters": "隐藏详细筛选",
"quickSearchByMpSku": "🔍 按平台 SKU 搜索: {{sku}}",
"quickSearchByMpName": "🔍 按平台品名搜索",
"selectedMaster": "当前选中的主商品规格:",
"noSelection": "未选择任何主商品规格",
"resultsCount": "共找到 {{count}} / {{total}} 项",
"stockLabel": "库存:",
"sortPriceAsc": "价格: 低 -> 高",
"sortPriceDesc": "价格: 高 -> 低",
"sortStock": "按库存高到低",
"sortSku": "SKU 编码 (A-Z)",
"noMasterFound": "未找到匹配的主商品记录", "noMasterFound": "未找到匹配的主商品记录",
"bomCostLabel": "BOM 成本:", "bomCostLabel": "BOM 成本:",
"unlink": "取消关联", "unlink": "取消关联",
@@ -1303,9 +1330,62 @@
"alertUrlCopied": "图片 URL 已复制到剪贴板!", "alertUrlCopied": "图片 URL 已复制到剪贴板!",
"alertUploadFailed": "上传失败:{{error}}", "alertUploadFailed": "上传失败:{{error}}",
"alertUploadGeneric": "上传媒体文件失败。" "alertUploadGeneric": "上传媒体文件失败。"
},
"cmsMockup": {
"title": "CMS 组件样例 (Showcase)",
"subtitle": "页面构建器中所有可用区块的实时预览及 JSON 模板 Schema 数据",
"searchPlaceholder": "按名称、类型或描述搜索区块...",
"showingBlocks": "显示 {{total}} 个区块中的 {{count}} 个",
"noBlocksFound": "未找到与“{{search}}”匹配的区块",
"viewPayload": "查看 Payload",
"hidePayload": "隐藏 Payload",
"copyPayload": "复制 Payload",
"copied": "已复制",
"categories": {
"all": "全部区块",
"hero": "横幅 & Hero",
"products": "商品 & 商店",
"editorial": "故事 & 社论",
"articles": "文章元素",
"forms": "表单 & 信息"
},
"blocks": {
"ANNOUNCEMENT_BAR": "顶部通知栏,支持自定义样式与滚动通知内容",
"HERO": "高端全屏封面,包含大标题、背景图片与 CTA 按钮",
"HERO_V2": "双栏分割式 Hero,一侧为文字排版,另一侧为商品卡片",
"HERO_VIDEO": "电影级全景背景视频横幅,带有动效文字排版与双 CTA 按钮",
"USP_BAR": "独特卖点栏(USP),展示图标与品质承诺标签",
"BANNER": "可点击的促销横幅,带有文字覆盖层,适合各类营销活动",
"CATEGORIES": "视觉化分类网格,引导用户浏览特定产品系列",
"PRODUCTS": "精选商品或定制列表的网格/轮播展示,支持快捷购买",
"PRODUCT_CAROUSEL": "可滑动的横向商品卡片轮播,流畅展示指定系列商品",
"RELATED_PRODUCTS": "商品详情页中的动态相关商品推荐模块",
"EDITORIAL_TILE": "图文社论区块,将品牌故事与精美宣传照片优雅结合",
"FEATURED_TILES": "并排活动网格,方形或长条画幅搭配居中标题与线条按钮(祖·玛珑风格)",
"EDITORIAL_CAROUSEL": "品牌故事与理念轮播,配图展示深度社论内容",
"SCENT_NOTES_BREAKDOWN": "香氛金字塔解析,清晰展示前调、中调与后调成分",
"QUOTE_CALLOUT": "高雅名言引言横幅,非常适合展示品牌箴言或客户好评",
"TEXT": "基础文本组件,用于标题、居中段落或品牌标语",
"RICH_TEXT": "支持 Markdown/HTML 的富文本排版区块,适合长篇报道",
"HERO_ARTICLE": "杂志风格文章封面,具备背景虚化、发布日期、作者与标题排版",
"ARTICLE_HEADER": "标准文本文章的导读标题区块",
"ARTICLE_MEDIA": "图文配对文章段落(支持左图或右图对齐)",
"ARTICLE_GALLERY": "多图相册网格,用于呈现步骤解说或多角度插图",
"ARTICLE_FACTS": "重点速览与核心要点摘要,适合知识性科普内容",
"ARTICLE_VIDEO": "文章内嵌视频播放包装组件",
"ARTICLE_REFERENCES": "学术文献引用列表与参考来源卡片",
"ARTICLE_TABLE": "对比数据表格,清晰陈列规格参数或技术指标",
"ARTICLE_DOWNLOAD": "附件下载卡片(PDF 指南、产品手册等)",
"ARTICLE_CTA": "行动号召(Call to Action)模块,引导用户点击进入下一步",
"NEWSLETTER": "邮件订阅模块,用于吸引用户订阅并获取首单优惠",
"ACCORDION": "常见问题(FAQ)折叠面板,支持按类别分组",
"CONTACT_INFO": "精美联系方式展示框,汇集地址、电话、邮箱及社交媒体",
"CONTACT_FORM": "通用客户咨询与支持邮件表单"
}
} }
} }
+110 -27
View File
@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Layers, Layers,
Copy, Copy,
@@ -13,17 +14,19 @@ import {
Play, Play,
Download, Download,
Info, Info,
ChevronDown ChevronDown,
Search,
X
} from 'lucide-react'; } from 'lucide-react';
// Define categories for easy filtering // Define categories for easy filtering
const CATEGORIES = [ const CATEGORY_KEYS = [
{ id: 'all', name: 'All Blocks' }, { id: 'all', key: 'cmsMockup.categories.all', fallback: 'All Blocks' },
{ id: 'hero', name: 'Banner & Hero' }, { id: 'hero', key: 'cmsMockup.categories.hero', fallback: 'Banner & Hero' },
{ id: 'products', name: 'Products & Shop' }, { id: 'products', key: 'cmsMockup.categories.products', fallback: 'Products & Shop' },
{ id: 'editorial', name: 'Story & Editorial' }, { id: 'editorial', key: 'cmsMockup.categories.editorial', fallback: 'Story & Editorial' },
{ id: 'articles', name: 'Article Elements' }, { id: 'articles', key: 'cmsMockup.categories.articles', fallback: 'Article Elements' },
{ id: 'forms', name: 'Forms & Info' } { id: 'forms', key: 'cmsMockup.categories.forms', fallback: 'Forms & Info' }
]; ];
interface MockBlock { interface MockBlock {
@@ -35,7 +38,9 @@ interface MockBlock {
} }
export default function CmsMockupPage() { export default function CmsMockupPage() {
const { t } = useTranslation();
const [activeCategory, setActiveCategory] = useState('all'); const [activeCategory, setActiveCategory] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [copiedType, setCopiedType] = useState<string | null>(null); const [copiedType, setCopiedType] = useState<string | null>(null);
const [showJsonFor, setShowJsonFor] = useState<string | null>(null); const [showJsonFor, setShowJsonFor] = useState<string | null>(null);
@@ -996,46 +1001,118 @@ export default function CmsMockupPage() {
} }
]; ];
const filteredBlocks = activeCategory === 'all' const filteredBlocks = mockBlocks.filter((block) => {
? mockBlocks const matchesCategory = activeCategory === 'all' || block.category === activeCategory;
: mockBlocks.filter(b => b.category === activeCategory); if (!matchesCategory) return false;
if (!searchQuery.trim()) return true;
const q = searchQuery.toLowerCase().trim();
const localizedDesc = t(`cmsMockup.blocks.${block.type}`, block.description).toLowerCase();
const originalDesc = block.description.toLowerCase();
const blockType = block.type.toLowerCase();
const categoryName = t(`cmsMockup.categories.${block.category}`, block.category).toLowerCase();
return (
blockType.includes(q) ||
localizedDesc.includes(q) ||
originalDesc.includes(q) ||
categoryName.includes(q)
);
});
return ( return (
<div className="space-y-8 animate-fade-in text-slate-100"> <div className="space-y-8 animate-fade-in text-slate-100">
{/* Page Header */} {/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h1 className="text-3xl font-extrabold flex items-center gap-3"> <h1 className="text-3xl font-extrabold flex items-center gap-3">
<Layers className="w-8 h-8 text-cyan-glow" /> <Layers className="w-8 h-8 text-cyan-glow" />
CMS Blocks Showcase {t('cmsMockup.title', 'CMS Blocks Showcase')}
</h1> </h1>
<p className="text-xs text-slate-400 mt-1"> <p className="text-xs text-slate-400 mt-1">
Visual preview and template schema payloads for all available Page Builder blocks. {t('cmsMockup.subtitle', 'Visual preview and template schema payloads for all available Page Builder blocks.')}
</p> </p>
</div> </div>
{/* Search Input Bar */}
<div className="relative min-w-[260px] sm:w-80">
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="text"
placeholder={t('cmsMockup.searchPlaceholder', 'Search blocks by name, type, or description...')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-night-900/80 border border-white/10 text-white rounded-2xl pl-10 pr-9 py-2.5 text-xs focus:outline-none focus:border-cyan-glow/50 focus:ring-1 focus:ring-cyan-glow/50 transition-all placeholder:text-slate-500"
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white p-0.5 rounded-full hover:bg-white/10 transition-colors cursor-pointer"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
</div> </div>
{/* Categories Filter Tabs */} {/* Categories Filter Tabs & Stats */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="bg-night-950/40 p-1.5 rounded-2xl border border-white/5 flex gap-1.5 overflow-x-auto hide-scrollbar shrink-0"> <div className="bg-night-950/40 p-1.5 rounded-2xl border border-white/5 flex gap-1.5 overflow-x-auto hide-scrollbar shrink-0">
{CATEGORIES.map((cat) => ( {CATEGORY_KEYS.map((cat) => (
<button <button
key={cat.id} key={cat.id}
type="button" type="button"
onClick={() => setActiveCategory(cat.id)} onClick={() => setActiveCategory(cat.id)}
className={`px-5 py-2 text-xs font-bold transition-all rounded-xl whitespace-nowrap ${ className={`px-5 py-2 text-xs font-bold transition-all rounded-xl whitespace-nowrap cursor-pointer ${
activeCategory === cat.id activeCategory === cat.id
? 'bg-cyan-glow text-night-900 shadow-lg shadow-cyan-glow/20' ? 'bg-cyan-glow text-night-900 shadow-lg shadow-cyan-glow/20'
: 'text-slate-400 hover:text-white hover:bg-white/5' : 'text-slate-400 hover:text-white hover:bg-white/5'
}`} }`}
> >
{cat.name} {t(cat.key, cat.fallback)}
</button> </button>
))} ))}
</div> </div>
<div className="text-xs text-slate-400 font-medium px-2">
{t('cmsMockup.showingBlocks', {
count: filteredBlocks.length,
total: mockBlocks.length,
defaultValue: `Showing ${filteredBlocks.length} of ${mockBlocks.length} blocks`
})}
</div>
</div>
{/* Blocks Listing Grid */} {/* Blocks Listing Grid */}
{filteredBlocks.length === 0 ? (
<div className="bg-night-900/40 border border-white/5 rounded-3xl p-12 text-center space-y-4">
<Sparkles className="w-12 h-12 text-slate-600 mx-auto" />
<p className="text-sm text-slate-400">
{t('cmsMockup.noBlocksFound', {
search: searchQuery,
defaultValue: `No blocks found matching "${searchQuery}"`
})}
</p>
<button
type="button"
onClick={() => {
setSearchQuery('');
setActiveCategory('all');
}}
className="px-4 py-2 bg-white/5 hover:bg-white/10 text-cyan-glow border border-cyan-glow/20 rounded-xl text-xs font-semibold transition-all cursor-pointer"
>
Reset Filters
</button>
</div>
) : (
<div className="grid grid-cols-1 gap-8"> <div className="grid grid-cols-1 gap-8">
{filteredBlocks.map((block) => ( {filteredBlocks.map((block) => {
const localizedDesc = t(`cmsMockup.blocks.${block.type}`, block.description);
const localizedCategory = t(`cmsMockup.categories.${block.category}`, block.category);
return (
<div <div
key={block.type} key={block.type}
className="bg-night-900/50 border border-white/5 hover:border-white/10 rounded-3xl p-6 shadow-xl space-y-6 transition-all" className="bg-night-900/50 border border-white/5 hover:border-white/10 rounded-3xl p-6 shadow-xl space-y-6 transition-all"
@@ -1047,11 +1124,11 @@ export default function CmsMockupPage() {
<span className="text-sm font-extrabold text-white font-mono tracking-wide"> <span className="text-sm font-extrabold text-white font-mono tracking-wide">
{block.type} {block.type}
</span> </span>
<span className="px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider bg-white/5 border border-white/10 text-slate-400 rounded-md"> <span className="px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-wider bg-white/5 border border-white/10 text-cyan-glow/90 rounded-md">
{block.category} {localizedCategory}
</span> </span>
</div> </div>
<p className="text-xs text-slate-400 leading-relaxed max-w-2xl">{block.description}</p> <p className="text-xs text-slate-300 leading-relaxed max-w-2xl">{localizedDesc}</p>
</div> </div>
{/* Action buttons */} {/* Action buttons */}
@@ -1059,22 +1136,26 @@ export default function CmsMockupPage() {
<button <button
type="button" type="button"
onClick={() => setShowJsonFor(showJsonFor === block.type ? null : block.type)} onClick={() => setShowJsonFor(showJsonFor === block.type ? null : block.type)}
className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 ${ className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 cursor-pointer ${
showJsonFor === block.type ? 'bg-cyan-glow text-night-900 border-cyan-glow/30' : 'bg-white/5 hover:bg-white/10 text-slate-300' showJsonFor === block.type ? 'bg-cyan-glow text-night-900 border-cyan-glow/30' : 'bg-white/5 hover:bg-white/10 text-slate-300'
}`} }`}
> >
<Code className="w-3.5 h-3.5" /> <Code className="w-3.5 h-3.5" />
{showJsonFor === block.type ? 'Hide Payload' : 'View Payload'} {showJsonFor === block.type
? t('cmsMockup.hidePayload', 'Hide Payload')
: t('cmsMockup.viewPayload', 'View Payload')}
</button> </button>
<button <button
type="button" type="button"
onClick={() => copyPayload(block.type, block.payload)} onClick={() => copyPayload(block.type, block.payload)}
className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 ${ className={`px-3 py-1.5 rounded-xl border border-white/10 text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-1.5 cursor-pointer ${
copiedType === block.type ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10 text-slate-300' copiedType === block.type ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10 text-slate-300'
}`} }`}
> >
{copiedType === block.type ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />} {copiedType === block.type ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
{copiedType === block.type ? 'Copied' : 'Copy Payload'} {copiedType === block.type
? t('cmsMockup.copied', 'Copied')
: t('cmsMockup.copyPayload', 'Copy Payload')}
</button> </button>
</div> </div>
</div> </div>
@@ -1093,8 +1174,10 @@ export default function CmsMockupPage() {
</div> </div>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)}
</div> </div>
); );
} }
@@ -16,6 +16,14 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Loader2, Loader2,
SlidersHorizontal,
RotateCcw,
Tag,
Barcode,
Check,
Building2,
Layers,
Sparkle,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types'; import { API_BASE } from '@homaroma/types';
@@ -57,10 +65,20 @@ export default function MarketplaceProductsPage() {
// Master variants for mapping picker // Master variants for mapping picker
const [masterVariants, setMasterVariants] = useState<MasterVariant[]>([]); const [masterVariants, setMasterVariants] = useState<MasterVariant[]>([]);
const [mappingModalItem, setMappingModalItem] = useState<any>(null); const [mappingModalItem, setMappingModalItem] = useState<any>(null);
const [variantSearch, setVariantSearch] = useState('');
const [selectedVariantId, setSelectedVariantId] = useState<number | null>(null); const [selectedVariantId, setSelectedVariantId] = useState<number | null>(null);
const [savingMapping, setSavingMapping] = useState(false); const [savingMapping, setSavingMapping] = useState(false);
// Modal Advanced Filter States
const [modalSearch, setModalSearch] = useState('');
const [modalSkuSearch, setModalSkuSearch] = useState('');
const [modalNameSearch, setModalNameSearch] = useState('');
const [modalOptionSearch, setModalOptionSearch] = useState('');
const [modalCategory, setModalCategory] = useState('all');
const [modalBrand, setModalBrand] = useState('all');
const [modalBomFilter, setModalBomFilter] = useState<'all' | 'has_bom' | 'no_bom'>('all');
const [modalSortBy, setModalSortBy] = useState<'name' | 'sku' | 'price_asc' | 'price_desc' | 'stock'>('name');
const [showAdvancedFilters, setShowAdvancedFilters] = useState(true);
// Toast / notification message // Toast / notification message
const [toast, setToast] = useState<{ type: 'success' | 'info' | 'error'; message: string } | null>(null); const [toast, setToast] = useState<{ type: 'success' | 'info' | 'error'; message: string } | null>(null);
@@ -174,6 +192,8 @@ export default function MarketplaceProductsPage() {
'success' 'success'
); );
fetchData(); fetchData();
} else {
showToast(t('marketplaceProducts.toastAutoMatchError', 'เกิดข้อผิดพลาดในการ Auto-Match'), 'error');
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -214,19 +234,136 @@ export default function MarketplaceProductsPage() {
} }
}; };
// Categories and brands extracted dynamically from Master Variants
const masterCategories = useMemo(() => {
const set = new Set<string>();
masterVariants.forEach((v) => {
if (v.categoryName?.trim()) set.add(v.categoryName.trim());
});
return Array.from(set).sort();
}, [masterVariants]);
const masterBrands = useMemo(() => {
const set = new Set<string>();
masterVariants.forEach((v) => {
if (v.brandName?.trim()) set.add(v.brandName.trim());
});
return Array.from(set).sort();
}, [masterVariants]);
const hasActiveModalFilters = Boolean(
modalSearch.trim() ||
modalSkuSearch.trim() ||
modalNameSearch.trim() ||
modalOptionSearch.trim() ||
modalCategory !== 'all' ||
modalBrand !== 'all' ||
modalBomFilter !== 'all'
);
const resetModalFilters = () => {
setModalSearch('');
setModalSkuSearch('');
setModalNameSearch('');
setModalOptionSearch('');
setModalCategory('all');
setModalBrand('all');
setModalBomFilter('all');
setModalSortBy('name');
};
const openMappingModal = (item: any) => {
setMappingModalItem(item);
setSelectedVariantId(item.masterVariantId || null);
resetModalFilters();
setShowAdvancedFilters(true);
};
// Filter master variants inside modal // Filter master variants inside modal
const filteredMasterVariants = useMemo(() => { const filteredMasterVariants = useMemo(() => {
if (!variantSearch.trim()) return masterVariants; const qGlobal = modalSearch.toLowerCase().trim();
const q = variantSearch.toLowerCase(); const qSku = modalSkuSearch.toLowerCase().trim();
return masterVariants.filter( const qName = modalNameSearch.toLowerCase().trim();
(v) => const qOption = modalOptionSearch.toLowerCase().trim();
v.productName.toLowerCase().includes(q) ||
v.sku.toLowerCase().includes(q) || return masterVariants
v.barcode.toLowerCase().includes(q) || .filter((v) => {
v.optionsStr.toLowerCase().includes(q) || // 1. Global search
v.categoryName.toLowerCase().includes(q) if (qGlobal) {
); const match =
}, [masterVariants, variantSearch]); v.productName.toLowerCase().includes(qGlobal) ||
v.sku.toLowerCase().includes(qGlobal) ||
v.barcode.toLowerCase().includes(qGlobal) ||
v.optionsStr.toLowerCase().includes(qGlobal) ||
v.categoryName.toLowerCase().includes(qGlobal) ||
v.brandName.toLowerCase().includes(qGlobal);
if (!match) return false;
}
// 2. Specific SKU / Barcode search
if (qSku) {
const match = v.sku.toLowerCase().includes(qSku) || v.barcode.toLowerCase().includes(qSku);
if (!match) return false;
}
// 3. Specific Product Name search
if (qName) {
if (!v.productName.toLowerCase().includes(qName)) return false;
}
// 4. Specific Option / Variation search
if (qOption) {
if (!v.optionsStr.toLowerCase().includes(qOption)) return false;
}
// 5. Category filter
if (modalCategory !== 'all' && v.categoryName !== modalCategory) {
return false;
}
// 6. Brand filter
if (modalBrand !== 'all' && v.brandName !== modalBrand) {
return false;
}
// 7. BOM filter
if (modalBomFilter === 'has_bom' && v.bomCost <= 0) return false;
if (modalBomFilter === 'no_bom' && v.bomCost > 0) return false;
return true;
})
.sort((a, b) => {
if (modalSortBy === 'sku') return a.sku.localeCompare(b.sku);
if (modalSortBy === 'price_asc') return a.price - b.price;
if (modalSortBy === 'price_desc') return b.price - a.price;
if (modalSortBy === 'stock') return b.stock - a.stock;
return a.productName.localeCompare(b.productName);
});
}, [
masterVariants,
modalSearch,
modalSkuSearch,
modalNameSearch,
modalOptionSearch,
modalCategory,
modalBrand,
modalBomFilter,
modalSortBy,
]);
const selectedVariant = useMemo(() => {
if (!selectedVariantId) return null;
return masterVariants.find((v) => v.id === selectedVariantId) || null;
}, [masterVariants, selectedVariantId]);
const activeAdvancedFilterCount = [
Boolean(modalSkuSearch.trim()),
Boolean(modalNameSearch.trim()),
Boolean(modalOptionSearch.trim()),
modalCategory !== 'all',
modalBrand !== 'all',
modalBomFilter !== 'all',
].filter(Boolean).length;
const kpi = data?.kpi; const kpi = data?.kpi;
const items = data?.items || []; const items = data?.items || [];
@@ -790,11 +927,7 @@ export default function MarketplaceProductsPage() {
{/* Actions */} {/* Actions */}
<td className="px-4 py-3.5 text-center"> <td className="px-4 py-3.5 text-center">
<button <button
onClick={() => { onClick={() => openMappingModal(item)}
setMappingModalItem(item);
setSelectedVariantId(item.masterVariantId);
setVariantSearch('');
}}
className="px-2.5 py-1.5 rounded-xl bg-white/5 hover:bg-amber-500/20 hover:text-amber-300 text-slate-300 border border-white/10 text-xs font-semibold transition-all inline-flex items-center gap-1.5" className="px-2.5 py-1.5 rounded-xl bg-white/5 hover:bg-amber-500/20 hover:text-amber-300 text-slate-300 border border-white/10 text-xs font-semibold transition-all inline-flex items-center gap-1.5"
> >
<Link2 className="w-3.5 h-3.5" /> <Link2 className="w-3.5 h-3.5" />
@@ -843,14 +976,14 @@ export default function MarketplaceProductsPage() {
)} )}
</div> </div>
{/* ── Mapping Modal ────────────────────────────────────────────────────── */} {/* ── Mapping Modal (Wide & Rich Filters) ─────────────────────────────────── */}
{mappingModalItem && ( {mappingModalItem && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-fade-in"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/85 backdrop-blur-md animate-fade-in">
<div className="bg-slate-900 border border-white/10 rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl overflow-hidden"> <div className="bg-slate-900 border border-white/15 rounded-3xl w-full max-w-5xl max-h-[92vh] flex flex-col shadow-2xl shadow-black/80 overflow-hidden">
{/* Modal Header */} {/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/[0.02]"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/[0.02]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-500/20 border border-amber-500/30 flex items-center justify-center"> <div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-amber-500/20 to-orange-500/20 border border-amber-500/30 flex items-center justify-center shadow-lg shadow-amber-500/10">
<Link2 className="w-5 h-5 text-amber-400" /> <Link2 className="w-5 h-5 text-amber-400" />
</div> </div>
<div> <div>
@@ -870,25 +1003,30 @@ export default function MarketplaceProductsPage() {
</button> </button>
</div> </div>
{/* Target Marketplace Item Summary */} {/* Target Marketplace Item Banner */}
<div className="p-4 mx-6 mt-4 bg-white/5 border border-white/10 rounded-xl space-y-1.5 text-xs"> <div className="p-4 mx-6 mt-4 bg-gradient-to-r from-amber-500/10 via-orange-500/5 to-transparent border border-amber-500/25 rounded-2xl space-y-2 text-xs">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-slate-400"> <span
{t('marketplaceProducts.productOn', 'สินค้าบน')} {mappingModalItem.channel.toUpperCase()} ({mappingModalItem.shop}): className={`px-2 py-0.5 rounded text-[10px] font-bold border uppercase ${
mappingModalItem.channel === 'tiktok'
? 'bg-rose-500/20 text-rose-300 border-rose-500/30'
: mappingModalItem.channel === 'shopee'
? 'bg-orange-500/20 text-orange-300 border-orange-500/30'
: 'bg-blue-500/20 text-blue-300 border-blue-500/30'
}`}
>
{mappingModalItem.channel}
</span> </span>
<span className="text-slate-400 font-mono">ร้าน: {mappingModalItem.shop}</span>
{mappingModalItem.marketplaceSku && ( {mappingModalItem.marketplaceSku && (
<span className="px-2 py-0.2 rounded bg-black/40 text-amber-300 font-mono"> <span className="px-2 py-0.5 rounded bg-black/50 text-cyan-300 font-mono border border-cyan-500/30">
SKU: {mappingModalItem.marketplaceSku} SKU: {mappingModalItem.marketplaceSku}
</span> </span>
)} )}
</div> </div>
<p className="font-bold text-white text-sm">{mappingModalItem.productName}</p>
{mappingModalItem.variation && ( <div className="flex items-center gap-3 text-slate-300 font-mono">
<p className="text-amber-400">
{t('marketplaceProducts.variationPrefix', 'ตัวเลือก:')} {mappingModalItem.variation}
</p>
)}
<div className="flex items-center gap-4 text-slate-400 font-mono pt-1">
<span> <span>
{t('marketplaceProducts.soldLabel', 'ขายแล้ว:')}{' '} {t('marketplaceProducts.soldLabel', 'ขายแล้ว:')}{' '}
<strong className="text-white">{fmtNum(mappingModalItem.totalSoldQty)}</strong>{' '} <strong className="text-white">{fmtNum(mappingModalItem.totalSoldQty)}</strong>{' '}
@@ -905,87 +1043,380 @@ export default function MarketplaceProductsPage() {
</div> </div>
</div> </div>
{/* Search & Variants List */} <div>
<div className="p-6 flex-1 overflow-hidden flex flex-col space-y-3"> <p className="font-bold text-white text-sm leading-snug">{mappingModalItem.productName}</p>
<div className="relative"> {mappingModalItem.variation && (
<p className="text-amber-300 text-xs mt-0.5">
{t('marketplaceProducts.variationPrefix', 'ตัวเลือก:')} {mappingModalItem.variation}
</p>
)}
</div>
{/* Quick Search Suggestions */}
<div className="flex items-center gap-2 pt-1 flex-wrap border-t border-white/5">
<span className="text-[11px] text-slate-400 flex items-center gap-1">
<Sparkles className="w-3.5 h-3.5 text-amber-400" />
ตัวช่วยค้นหาเร็ว:
</span>
{mappingModalItem.marketplaceSku && (
<button
type="button"
onClick={() => {
setModalSkuSearch(mappingModalItem.marketplaceSku);
setModalSearch('');
setShowAdvancedFilters(true);
}}
className="px-2.5 py-1 rounded-lg bg-cyan-500/15 hover:bg-cyan-500/25 text-cyan-300 border border-cyan-500/30 text-[11px] font-mono transition-all flex items-center gap-1"
>
<Barcode className="w-3 h-3" />
<span>{t('marketplaceProducts.quickSearchByMpSku', { sku: mappingModalItem.marketplaceSku })}</span>
</button>
)}
<button
type="button"
onClick={() => {
// Extract first 2-3 words for convenient matching
const words = mappingModalItem.productName.split(' ').slice(0, 3).join(' ');
setModalSearch(words);
}}
className="px-2.5 py-1 rounded-lg bg-amber-500/15 hover:bg-amber-500/25 text-amber-300 border border-amber-500/30 text-[11px] transition-all flex items-center gap-1"
>
<Search className="w-3 h-3" />
<span>{t('marketplaceProducts.quickSearchByMpName', '🔍 ค้นหาด้วยชื่อสินค้านี้')}</span>
</button>
</div>
</div>
{/* Search & Filter Toolbar */}
<div className="p-4 mx-6 mt-3 bg-white/[0.03] border border-white/10 rounded-2xl space-y-3">
{/* Primary Search Bar */}
<div className="flex flex-wrap items-center gap-2.5">
<div className="relative flex-1 min-w-[240px]">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" /> <Search className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
<input <input
type="text" type="text"
placeholder={t( placeholder={t(
'marketplaceProducts.modalSearchPlaceholder', 'marketplaceProducts.modalSearchPlaceholder',
'พิมพ์ค้นหาชื่อสินค้าหลัก, รหัส SKU, หรือขนาด (เช่น 50ml, 120ml)...' 'ค้นหาทั่วไป (ชื่อสินค้า, SKU, บาร์โค้ด, ขนาด, แบรนด์)...'
)} )}
value={variantSearch} value={modalSearch}
onChange={(e) => setVariantSearch(e.target.value)} onChange={(e) => setModalSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 rounded-xl bg-black/40 border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-amber-500/40" className="w-full pl-9 pr-8 py-2 rounded-xl bg-black/50 border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-amber-500/50 transition-colors"
autoFocus autoFocus
/> />
{modalSearch && (
<button
onClick={() => setModalSearch('')}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* Toggle Detailed Filters */}
<button
type="button"
onClick={() => setShowAdvancedFilters((prev) => !prev)}
className={`px-3 py-2 rounded-xl text-xs font-semibold border transition-all flex items-center gap-1.5 ${
showAdvancedFilters || activeAdvancedFilterCount > 0
? 'bg-amber-500/20 text-amber-300 border-amber-500/40 shadow-sm'
: 'bg-white/5 text-slate-300 border-white/10 hover:bg-white/10'
}`}
>
<SlidersHorizontal className="w-3.5 h-3.5" />
<span>
{showAdvancedFilters
? t('marketplaceProducts.hideAdvancedFilters', 'ซ่อนตัวกรองละเอียด')
: t('marketplaceProducts.advancedFilters', 'ตัวกรองละเอียด')}
</span>
{activeAdvancedFilterCount > 0 && (
<span className="w-4 h-4 rounded-full bg-amber-500 text-slate-950 font-bold text-[10px] flex items-center justify-center">
{activeAdvancedFilterCount}
</span>
)}
</button>
{/* Sort selector */}
<div className="flex items-center gap-1.5 text-xs">
<select
value={modalSortBy}
onChange={(e: any) => setModalSortBy(e.target.value)}
className="px-3 py-2 rounded-xl bg-black/50 border border-white/10 text-white text-xs font-medium focus:outline-none focus:border-amber-500/40"
>
<option value="name">{t('marketplaceProducts.sortName', 'ชื่อสินค้า (A-Z)')}</option>
<option value="sku">{t('marketplaceProducts.sortSku', 'รหัส SKU (A-Z)')}</option>
<option value="price_asc">{t('marketplaceProducts.sortPriceAsc', 'ราคา: ต่ำ -> สูง')}</option>
<option value="price_desc">{t('marketplaceProducts.sortPriceDesc', 'ราคา: สูง -> ต่ำ')}</option>
<option value="stock">{t('marketplaceProducts.sortStock', 'สต็อกคงเหลือสูงสุด')}</option>
</select>
</div>
{/* Clear all filters */}
{hasActiveModalFilters && (
<button
type="button"
onClick={resetModalFilters}
className="px-3 py-2 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-300 border border-rose-500/30 text-xs font-medium transition-all flex items-center gap-1"
>
<RotateCcw className="w-3 h-3" />
<span>{t('marketplaceProducts.clearFilters', 'ล้างตัวกรอง')}</span>
</button>
)}
</div>
{/* Advanced Filters Expandable Grid */}
{showAdvancedFilters && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2.5 pt-3 border-t border-white/5 animate-fade-in">
{/* 1. SKU Filter */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<Barcode className="w-3 h-3 text-cyan-400" />
{t('marketplaceProducts.filterBySku', 'รหัส SKU / Barcode')}
</label>
<input
type="text"
placeholder={t('marketplaceProducts.filterBySkuPlaceholder', 'ระบุรหัส SKU...')}
value={modalSkuSearch}
onChange={(e) => setModalSkuSearch(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500/40"
/>
</div> </div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1 max-h-[300px]"> {/* 2. Product Name Filter */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<Package className="w-3 h-3 text-amber-400" />
{t('marketplaceProducts.filterByName', 'ชื่อสินค้า Master')}
</label>
<input
type="text"
placeholder={t('marketplaceProducts.filterByNamePlaceholder', 'ระบุชื่อสินค้า...')}
value={modalNameSearch}
onChange={(e) => setModalNameSearch(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-amber-500/40"
/>
</div>
{/* 3. Option / Size Filter */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<Tag className="w-3 h-3 text-rose-400" />
{t('marketplaceProducts.filterByOption', 'ขนาด / ตัวเลือก')}
</label>
<input
type="text"
placeholder={t('marketplaceProducts.filterByOptionPlaceholder', 'เช่น 50ml, Vanilla...')}
value={modalOptionSearch}
onChange={(e) => setModalOptionSearch(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-rose-500/40"
/>
</div>
{/* 4. Category Dropdown */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<Layers className="w-3 h-3 text-emerald-400" />
{t('marketplaceProducts.filterCategory', 'หมวดหมู่')}
</label>
<select
value={modalCategory}
onChange={(e) => setModalCategory(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white focus:outline-none focus:border-emerald-500/40"
>
<option value="all">{t('marketplaceProducts.allCategories', 'ทุกหมวดหมู่')}</option>
{masterCategories.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
{/* 5. Brand Dropdown */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<Building2 className="w-3 h-3 text-purple-400" />
{t('marketplaceProducts.filterBrand', 'แบรนด์')}
</label>
<select
value={modalBrand}
onChange={(e) => setModalBrand(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white focus:outline-none focus:border-purple-500/40"
>
<option value="all">{t('marketplaceProducts.allBrands', 'ทุกแบรนด์')}</option>
{masterBrands.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
</div>
{/* 6. BOM Status Filter */}
<div className="space-y-1">
<label className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-orange-400" />
{t('marketplaceProducts.bomStatus', 'สถานะ BOM')}
</label>
<select
value={modalBomFilter}
onChange={(e: any) => setModalBomFilter(e.target.value)}
className="w-full px-2.5 py-1.5 rounded-lg bg-black/40 border border-white/10 text-xs text-white focus:outline-none focus:border-orange-500/40"
>
<option value="all">{t('marketplaceProducts.allBom', 'ทุกสถานะ BOM')}</option>
<option value="has_bom">{t('marketplaceProducts.hasBom', 'มีสูตร BOM แล้ว')}</option>
<option value="no_bom">{t('marketplaceProducts.noBomOnly', 'ยังไม่มีสูตร BOM')}</option>
</select>
</div>
</div>
)}
</div>
{/* Results Count & Variants Grid */}
<div className="p-6 pt-3 flex-1 overflow-hidden flex flex-col space-y-2">
<div className="flex items-center justify-between text-xs px-1 text-slate-400">
<p>
{t('marketplaceProducts.resultsCount', {
count: fmtNum(filteredMasterVariants.length),
total: fmtNum(masterVariants.length),
defaultValue: `พบ ${fmtNum(filteredMasterVariants.length)} จากทั้งหมด ${fmtNum(masterVariants.length)} รายการ`,
})}
</p>
{selectedVariant && (
<p className="text-amber-400 font-medium">
{t('marketplaceProducts.selectedMaster', 'รายการ Master ที่เลือก:')}{' '}
<strong className="text-white">{selectedVariant.productName}</strong>
{selectedVariant.optionsStr && <span> ({selectedVariant.optionsStr})</span>}
</p>
)}
</div>
<div className="flex-1 overflow-y-auto space-y-2.5 pr-1 max-h-[380px]">
{filteredMasterVariants.length === 0 ? ( {filteredMasterVariants.length === 0 ? (
<div className="py-8 text-center text-slate-500 text-xs"> <div className="py-16 text-center text-slate-500 text-xs space-y-2">
{t('marketplaceProducts.noMasterFound', 'ไม่พบรายการสินค้า Master ในระบบที่ตรงกับคำค้นหา')} <p>{t('marketplaceProducts.noMasterFound', 'ไม่พบรายการสินค้า Master ในระบบที่ตรงกับคำค้นหา')}</p>
{hasActiveModalFilters && (
<button
onClick={resetModalFilters}
className="px-3 py-1.5 rounded-xl bg-white/5 hover:bg-white/10 text-amber-300 border border-white/10 text-xs font-semibold transition-all"
>
{t('marketplaceProducts.clearFilters', 'ล้างตัวกรอง')}
</button>
)}
</div> </div>
) : ( ) : (
filteredMasterVariants.map((variant) => { <div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
{filteredMasterVariants.map((variant) => {
const isSelected = selectedVariantId === variant.id; const isSelected = selectedVariantId === variant.id;
const marginPercent =
variant.price > 0 && variant.bomCost > 0
? ((variant.price - variant.bomCost) / variant.price) * 100
: 0;
return ( return (
<div <div
key={variant.id} key={variant.id}
onClick={() => setSelectedVariantId(variant.id)} onClick={() => setSelectedVariantId(variant.id)}
className={`p-3 rounded-xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${ className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex flex-col justify-between gap-3 ${
isSelected isSelected
? 'bg-amber-500/20 border-amber-500/40 shadow-sm' ? 'bg-amber-500/15 border-amber-500/60 ring-1 ring-amber-500/40 shadow-lg shadow-amber-500/10'
: 'bg-white/[0.02] border-white/5 hover:bg-white/5 hover:border-white/10' : 'bg-white/[0.02] border-white/5 hover:bg-white/5 hover:border-white/15'
}`} }`}
> >
<div className="space-y-0.5"> <div className="flex items-start justify-between gap-2">
<p className="text-xs font-bold text-white">{variant.productName}</p> <div className="flex items-start gap-2.5 min-w-0">
<div className="flex items-center gap-2 text-[11px] text-slate-400 flex-wrap"> <div
<span className="font-mono text-cyan-300">SKU: {variant.sku || '-'}</span> className={`w-5 h-5 rounded-full flex items-center justify-center shrink-0 mt-0.5 border transition-all ${
{variant.optionsStr && <span className="text-amber-300">• {variant.optionsStr}</span>} isSelected
{variant.categoryName && <span className="text-slate-500">• {variant.categoryName}</span>} ? 'bg-amber-500 border-amber-400 text-slate-950'
: 'border-white/20 bg-black/40 text-transparent'
}`}
>
<Check className="w-3 h-3 stroke-[3]" />
</div>
<div className="space-y-1 min-w-0">
<p className="text-xs font-bold text-white leading-snug line-clamp-2">
{variant.productName}
</p>
<div className="flex items-center gap-1.5 text-[11px] flex-wrap font-mono">
<span className="px-2 py-0.5 rounded bg-black/50 text-cyan-300 border border-cyan-500/30">
SKU: {variant.sku || '-'}
</span>
{variant.optionsStr && (
<span className="px-2 py-0.5 rounded bg-amber-500/10 text-amber-300 border border-amber-500/20">
{variant.optionsStr}
</span>
)}
{variant.categoryName && (
<span className="text-slate-400 text-[10px]">
• {variant.categoryName}
</span>
)}
</div>
</div> </div>
</div> </div>
<div className="text-right shrink-0 font-mono text-xs"> <div className="text-right shrink-0 font-mono">
<p className="text-emerald-400 font-bold">{fmtBaht(variant.price)}</p> <p className="text-emerald-400 font-bold text-sm">{fmtBaht(variant.price)}</p>
<p className="text-[10px] text-slate-400"> <span className="text-[10px] text-slate-400">
{t('marketplaceProducts.bomCostLabel', 'ต้นทุน BOM:')}{' '} {t('marketplaceProducts.stockLabel', 'คงเหลือ:')}{' '}
<span className="text-amber-300">{fmtBaht(variant.bomCost)}</span> <strong className={variant.stock > 0 ? 'text-white' : 'text-rose-400'}>
</p> {fmtNum(variant.stock)}
</strong>
</span>
</div>
</div>
<div className="flex items-center justify-between text-[11px] font-mono pt-2 border-t border-white/5 text-slate-400">
<div>
<span>{t('marketplaceProducts.bomCostLabel', 'ต้นทุน BOM:')} </span>
<span className={variant.bomCost > 0 ? 'text-amber-300 font-bold' : 'text-slate-500'}>
{variant.bomCost > 0 ? fmtBaht(variant.bomCost) : t('marketplaceProducts.noBom', 'ยังไม่ตั้ง BOM')}
</span>
</div>
{variant.bomCost > 0 && (
<div className="text-emerald-400 font-bold">
Margin ~{marginPercent.toFixed(1)}%
</div>
)}
</div> </div>
</div> </div>
); );
}) })}
</div>
)} )}
</div> </div>
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
<div className="p-4 px-6 border-t border-white/10 flex items-center justify-between bg-white/[0.02]"> <div className="p-4 px-6 border-t border-white/10 flex flex-wrap items-center justify-between gap-3 bg-white/[0.02]">
{mappingModalItem.masterVariantId ? ( {mappingModalItem.masterVariantId ? (
<button <button
type="button"
onClick={() => handleSaveMapping(mappingModalItem.id, null)} onClick={() => handleSaveMapping(mappingModalItem.id, null)}
disabled={savingMapping} disabled={savingMapping}
className="px-4 py-2 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-300 border border-rose-500/30 text-xs font-bold transition-all flex items-center gap-1.5" className="px-4 py-2 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-300 border border-rose-500/30 text-xs font-bold transition-all flex items-center gap-1.5 disabled:opacity-50"
> >
<Unlink className="w-4 h-4" /> {t('marketplaceProducts.unlink', 'ยกเลิกการผูก')} <Unlink className="w-4 h-4" /> {t('marketplaceProducts.unlink', 'ยกเลิกการผูก')}
</button> </button>
) : <div />} ) : (
<div />
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button"
onClick={() => setMappingModalItem(null)} onClick={() => setMappingModalItem(null)}
className="px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-bold transition-all" className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-bold transition-all"
> >
{t('common.close', 'ปิด')} {t('common.close', 'ปิด')}
</button> </button>
<button <button
type="button"
onClick={() => handleSaveMapping(mappingModalItem.id, selectedVariantId)} onClick={() => handleSaveMapping(mappingModalItem.id, selectedVariantId)}
disabled={savingMapping || !selectedVariantId} disabled={savingMapping || !selectedVariantId}
className="px-5 py-2 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-slate-950 font-black text-xs shadow-lg shadow-orange-500/20 transition-all disabled:opacity-50 flex items-center gap-1.5" className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500 hover:from-amber-600 hover:to-rose-600 text-slate-950 font-black text-xs shadow-lg shadow-orange-500/20 transition-all disabled:opacity-50 flex items-center gap-2"
> >
{savingMapping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Link2 className="w-4 h-4" />} {savingMapping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Link2 className="w-4 h-4" />}
<span>{t('marketplaceProducts.saveMapping', 'บันทึกการผูกสินค้า')}</span> <span>{t('marketplaceProducts.saveMapping', 'บันทึกการผูกสินค้า')}</span>