This commit is contained in:
Your Name committed 2026-09-07 05:20:50 +07:00
1 parent b7004b63a3
commit fc059ea652
8 files changed
+525 -101

No files matched your search

+12 -10
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types';
import { X, Upload, Loader2, Image as ImageIcon, Video } from 'lucide-react';
@@ -16,6 +17,7 @@ interface MediaItem {
}
export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLibraryModalProps) {
const { t } = useTranslation();
const [media, setMedia] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
@@ -63,11 +65,11 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
}
} else {
const err = await res.json();
alert(`Upload failed: ${err.error || 'Unknown error'}`);
alert(t('mediaLibrary.alertUploadFailed', { error: err.error || 'Unknown error' }));
}
} catch (error) {
console.error('Failed to upload', error);
alert('Failed to upload media file.');
alert(t('mediaLibrary.alertUploadGeneric'));
} finally {
setUploading(false);
e.target.value = ''; // Reset input
@@ -90,9 +92,9 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-cyan-glow" />
Media Library
{t('mediaLibrary.modalTitle')}
</h2>
<p className="text-sm text-slate-400 mt-1">Select or upload images and videos (.mp4, .webm) for your content.</p>
<p className="text-sm text-slate-400 mt-1">{t('mediaLibrary.modalSubtitle')}</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition-colors text-slate-400 hover:text-white">
<X className="w-6 h-6" />
@@ -103,14 +105,14 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
<div className="p-4 border-b border-white/10 bg-night-900/50 flex justify-between items-center">
<label className="btn-primary cursor-pointer inline-flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm">
{uploading ? (
<><Loader2 className="w-4 h-4 animate-spin" /> Uploading...</>
<><Loader2 className="w-4 h-4 animate-spin" /> {t('mediaLibrary.btnUploading')}</>
) : (
<><Upload className="w-4 h-4" /> Upload New File</>
<><Upload className="w-4 h-4" /> {t('mediaLibrary.btnUploadNewFile')}</>
)}
<input type="file" className="hidden" accept="image/*,video/*" onChange={handleUpload} disabled={uploading} />
</label>
<div className="text-sm text-slate-500 font-mono">
{media.length} items found
{t('mediaLibrary.itemsFound', { count: media.length })}
</div>
</div>
@@ -119,7 +121,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
{loading ? (
<div className="flex flex-col justify-center items-center h-full text-slate-500 space-y-4">
<Loader2 className="w-10 h-10 animate-spin text-cyan-glow" />
<p className="font-mono text-sm tracking-widest uppercase">Loading Media...</p>
<p className="font-mono text-sm tracking-widest uppercase">{t('mediaLibrary.loading')}</p>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6">
@@ -165,8 +167,8 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
{media.length === 0 && !loading && (
<div className="col-span-full flex flex-col items-center justify-center py-20 text-slate-500">
<ImageIcon className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg font-bold">No media found</p>
<p className="text-sm mt-2">Upload your first image or video to get started.</p>
<p className="text-lg font-bold">{t('mediaLibrary.noMediaFound')}</p>
<p className="text-sm mt-2">{t('mediaLibrary.noMediaSub')}</p>
</div>
)}
</div>
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
X,
Search,
@@ -28,17 +29,18 @@ interface BlockDefinition {
renderPreview: () => React.ReactNode;
}
const CATEGORIES = [
{ id: 'all', name: 'All Blocks' },
{ id: 'hero', name: 'Banner & Hero' },
{ id: 'products', name: 'Products & Shop' },
{ id: 'editorial', name: 'Story & Editorial' },
{ id: 'articles', name: 'Article Elements' },
{ id: 'forms', name: 'Forms & Info' },
{ id: 'custom', name: 'Product & Custom' }
const CATEGORY_KEYS = [
{ id: 'all', key: 'pageBuilderGallery.categories.all' },
{ id: 'hero', key: 'pageBuilderGallery.categories.hero' },
{ id: 'products', key: 'pageBuilderGallery.categories.products' },
{ id: 'editorial', key: 'pageBuilderGallery.categories.editorial' },
{ id: 'articles', key: 'pageBuilderGallery.categories.articles' },
{ id: 'forms', key: 'pageBuilderGallery.categories.forms' },
{ id: 'custom', key: 'pageBuilderGallery.categories.custom' }
];
export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddSectionModalProps) {
const { t } = useTranslation();
const [activeCategory, setActiveCategory] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
@@ -605,10 +607,10 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
<div>
<h3 className="text-xl font-extrabold flex items-center gap-3 text-white">
<Sparkles className="w-6 h-6 text-cyan-glow animate-pulse" />
Add Block Section
{t('pageBuilderGallery.galleryTitle')}
</h3>
<p className="text-xs text-slate-400 mt-1">
Select a visual block component to insert into your page layout.
{t('pageBuilderGallery.gallerySubtitle')}
</p>
</div>
@@ -618,7 +620,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Search blocks..."
placeholder={t('pageBuilderGallery.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-900/80 border border-white/10 text-white rounded-xl pl-9 pr-4 py-2 text-xs focus:outline-none focus:border-cyan-glow/50 transition-all"
@@ -644,7 +646,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
{/* Category Filter Tabs */}
<div className="px-6 py-3 border-b border-white/5 bg-night-950/40 flex gap-2 overflow-x-auto hide-scrollbar shrink-0">
{CATEGORIES.map((cat) => (
{CATEGORY_KEYS.map((cat) => (
<button
key={cat.id}
type="button"
@@ -655,7 +657,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
: 'text-slate-400 hover:text-white hover:bg-white/5'
}`}
>
{cat.name}
{t(cat.key)}
</button>
))}
</div>
@@ -665,7 +667,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
{filteredBlocks.length === 0 ? (
<div className="col-span-full py-20 text-center text-slate-500 space-y-3">
<Layers className="w-12 h-12 mx-auto text-slate-700" />
<p className="text-sm font-bold">No blocks found matching "{searchQuery}".</p>
<p className="text-sm font-bold">{t('pageBuilderGallery.noBlocksFound', { search: searchQuery })}</p>
</div>
) : (
filteredBlocks.map((block) => (
@@ -708,14 +710,14 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
{/* Hover Selection Footer */}
<div className="mt-4 pt-3 border-t border-white/5 flex items-center justify-between">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500 group-hover:text-slate-300">
Category: {block.category}
{t('pageBuilderGallery.categoryLabel', { cat: block.category })}
</span>
<button
type="button"
className="px-3 py-1 bg-cyan-glow/10 group-hover:bg-cyan-glow text-cyan-glow group-hover:text-night-900 text-xs font-bold rounded-lg transition-all flex items-center gap-1"
>
<Plus className="w-3.5 h-3.5" />
Select Block
{t('pageBuilderGallery.btnSelectBlock')}
</button>
</div>
</div>
+104
View File
@@ -1199,6 +1199,110 @@
"thUnits": "Units",
"thRevenueCol": "Revenue",
"thPercentTotal": "% Total"
},
"pageBuilder": {
"title": "Page Builder (CMS)",
"createPageBtn": "Create Page",
"orderSavedSuccess": "Homepage section ordering updated successfully.",
"orderSaveError": "Error saving layout order. Please try again.",
"pageTitleLabel": "Page Title",
"pageTitlePlaceholder": "Enter page title...",
"pageUrlLabel": "Page URL",
"copyUrlSuccess": "URL Copied",
"copyUrlTooltip": "Copy page URL",
"publishToWebsite": "Publish to Website",
"badgePublished": "Published",
"badgeDraft": "Draft",
"btnSaveSettings": "Save Settings",
"btnPreviewLive": "Preview Live",
"btnDeletePage": "Delete this page",
"confirmDeletePage": "Are you sure you want to delete the page \"{{title}}\"? This will delete all of its section blocks permanently!",
"sectionsTitle": "Page Sections",
"sectionsSubtitle": "Manage the content blocks for this page.",
"btnReorderLayout": "Reorder Layout",
"btnAddSection": "Add Section",
"selectBlockLabel": "Select Block to Edit",
"chooseBlockPlaceholder": "-- Choose a block --",
"hiddenBadge": "(Hidden)",
"editBlockTitle": "Edit Block Settings",
"createBlockTitle": "Create Layout Block",
"badgeType": "Type:",
"badgeBlockId": "Block ID:",
"badgePage": "Page:",
"badgePageId": "Page ID:",
"blockTypeLabel": "Block Type",
"browseVisualGallery": "Browse Visual Gallery",
"btnSaveBlock": "Save Block Settings",
"btnCreateBlock": "Create Block",
"btnCancel": "Cancel",
"emptyBlockPrompt": "Select a block from the left to edit its content",
"reorderModalTitle": "Reorder Layout",
"noSectionsInPage": "No content sections in this page yet.",
"hiddenTag": "Hidden",
"tooltipHideSection": "Hide Section",
"tooltipShowSection": "Show Section",
"tooltipEditBlock": "Edit Block Content",
"tooltipDeleteBlock": "Delete Block",
"confirmDeleteSection": "Are you sure you want to delete this section?",
"btnSaveOrder": "Save Order",
"btnClose": "Close",
"createPageModalTitle": "Create New Page",
"pageSlugField": "Page Slug (URL Path)",
"pageSlugPlaceholder": "e.g. about",
"slugHint": "Will be converted to lowercase, slugified format (e.g. /pages/about-us).",
"btnCreating": "Creating...",
"btnCreateSubmit": "Create Page",
"errCreatePage": "Failed to create page",
"errSaveSettings": "Failed to save page settings",
"errDeletePage": "Failed to delete page",
"errSaveBlock": "Failed to save: {{error}}"
},
"pageBuilderGallery": {
"galleryTitle": "Add Block Section",
"gallerySubtitle": "Select a visual block component to insert into your page layout.",
"searchPlaceholder": "Search blocks...",
"categoryLabel": "Category: {{cat}}",
"btnSelectBlock": "Select Block",
"noBlocksFound": "No blocks found matching \"{{search}}\".",
"categories": {
"all": "All Blocks",
"hero": "Banner & Hero",
"products": "Products & Shop",
"editorial": "Story & Editorial",
"articles": "Article Elements",
"forms": "Forms & Info",
"custom": "Product & Custom"
}
},
"mediaLibrary": {
"title": "Media Library",
"subtitle": "Manage your uploaded images",
"btnUploading": "Uploading...",
"btnUploadNew": "Upload New Image",
"btnUploadNewFile": "Upload New File",
"pasteHintPrefix": "Paste",
"pasteHint": "an image anywhere on this page (Ctrl+V / ⌘+V) to upload instantly",
"loading": "Loading Media...",
"clickToCopy": "Click to copy URL",
"copyUrl": "Copy URL",
"deleteTooltip": "Delete Image",
"noImages": "No images found",
"noImagesSub": "Upload your first image to get started.",
"noMediaFound": "No media found",
"noMediaSub": "Upload your first image or video to get started.",
"itemsFound": "{{count}} items found",
"modalTitle": "Media Library",
"modalSubtitle": "Select or upload images and videos (.mp4, .webm) for your content.",
"toastUploaded": "✅ Uploaded: {{name}}",
"toastUploadFailed": "❌ Upload failed",
"toastUploadError": "❌ Upload error",
"toastPasting": "📋 Pasting image...",
"confirmDelete": "Are you sure you want to delete this image?",
"alertDeleteFailed": "Failed to delete image",
"alertDeleteError": "An error occurred while deleting",
"alertUrlCopied": "Image URL copied to clipboard!",
"alertUploadFailed": "Upload failed: {{error}}",
"alertUploadGeneric": "Failed to upload media file."
}
}
+104
View File
@@ -1198,6 +1198,110 @@
"thUnits": "数量",
"thRevenueCol": "売上高",
"thPercentTotal": "全体比 %"
},
"pageBuilder": {
"title": "ページビルダー (CMS)",
"createPageBtn": "新規ページ作成",
"orderSavedSuccess": "ページのセクション順序を正常に保存しました。",
"orderSaveError": "レイアウト順序の保存中にエラーが発生しました。もう一度お試しください。",
"pageTitleLabel": "ページタイトル",
"pageTitlePlaceholder": "ページタイトルを入力...",
"pageUrlLabel": "ページ URL",
"copyUrlSuccess": "URLをコピーしました",
"copyUrlTooltip": "ページURLをコピー",
"publishToWebsite": "Webサイトに公開",
"badgePublished": "公開中",
"badgeDraft": "下書き",
"btnSaveSettings": "設定を保存",
"btnPreviewLive": "ライブプレビュー",
"btnDeletePage": "このページを削除",
"confirmDeletePage": "ページ \"{{title}}\" を削除してもよろしいですか?すべてのセクションブロックが完全に削除されます!",
"sectionsTitle": "ページセクション",
"sectionsSubtitle": "このページのコンテンツブロックを管理します。",
"btnReorderLayout": "並び替え",
"btnAddSection": "セクション追加",
"selectBlockLabel": "編集するブロックを選択",
"chooseBlockPlaceholder": "-- ブロックを選択 --",
"hiddenBadge": "(非表示)",
"editBlockTitle": "ブロック設定を編集",
"createBlockTitle": "新規ブロック作成",
"badgeType": "タイプ:",
"badgeBlockId": "ブロックID:",
"badgePage": "ページ:",
"badgePageId": "ページID:",
"blockTypeLabel": "ブロックタイプ",
"browseVisualGallery": "ビジュアルギャラリーを開く",
"btnSaveBlock": "ブロック設定を保存",
"btnCreateBlock": "ブロックを作成",
"btnCancel": "キャンセル",
"emptyBlockPrompt": "左側からブロックを選択してコンテンツを編集してください",
"reorderModalTitle": "レイアウトの並び替え",
"noSectionsInPage": "このページにはまだセクションがありません。",
"hiddenTag": "非表示",
"tooltipHideSection": "セクションを非表示",
"tooltipShowSection": "セクションを表示",
"tooltipEditBlock": "ブロック内容を編集",
"tooltipDeleteBlock": "ブロックを削除",
"confirmDeleteSection": "このセクションを削除してもよろしいですか?",
"btnSaveOrder": "順序を保存",
"btnClose": "閉じる",
"createPageModalTitle": "新規ページを作成",
"pageSlugField": "ページスラッグ (URLパス)",
"pageSlugPlaceholder": "例: about",
"slugHint": "小文字のスラッグ形式に自動変換されます (例: /pages/about-us)",
"btnCreating": "作成中...",
"btnCreateSubmit": "ページを作成",
"errCreatePage": "ページの作成に失敗しました",
"errSaveSettings": "ページ設定の保存に失敗しました",
"errDeletePage": "ページの削除に失敗しました",
"errSaveBlock": "保存に失敗しました: {{error}}"
},
"pageBuilderGallery": {
"galleryTitle": "ブロックセクションの追加",
"gallerySubtitle": "ページレイアウトに挿入するビジュアルブロックコンポーネントを選択してください。",
"searchPlaceholder": "ブロックを検索...",
"categoryLabel": "カテゴリー: {{cat}}",
"btnSelectBlock": "このブロックを選択",
"noBlocksFound": "\"{{search}}\" に一致するブロックは見つかりませんでした。",
"categories": {
"all": "すべてのブロック",
"hero": "バナー & ヒーロー",
"products": "商品 & ショップ",
"editorial": "ストーリー & エディトリアル",
"articles": "記事コンポーネント",
"forms": "フォーム & お問い合わせ",
"custom": "商品 & カスタム"
}
},
"mediaLibrary": {
"title": "メディアライブラリ",
"subtitle": "アップロードした画像とメディアファイルを管理します",
"btnUploading": "アップロード中...",
"btnUploadNew": "新しい画像をアップロード",
"btnUploadNewFile": "新しいファイルをアップロード",
"pasteHintPrefix": "貼り付け",
"pasteHint": "このページの任意の場所に画像を貼り付け(Ctrl+V / ⌘+V)して即時アップロード",
"loading": "メディアを読み込み中...",
"clickToCopy": "クリックしてURLをコピー",
"copyUrl": "URLをコピー",
"deleteTooltip": "画像を削除",
"noImages": "画像が見つかりません",
"noImagesSub": "最初の画像をアップロードして開始してください。",
"noMediaFound": "メディアが見つかりません",
"noMediaSub": "最初の画像または動画をアップロードして開始してください。",
"itemsFound": "{{count}} 件見つかりました",
"modalTitle": "メディアライブラリ",
"modalSubtitle": "コンテンツ用の画像や動画(.mp4、.webm)を選択またはアップロードします。",
"toastUploaded": "✅ アップロード完了: {{name}}",
"toastUploadFailed": "❌ アップロードに失敗しました",
"toastUploadError": "❌ アップロードエラーが発生しました",
"toastPasting": "📋 画像を貼り付け中...",
"confirmDelete": "この画像を削除してもよろしいですか?",
"alertDeleteFailed": "画像の削除に失敗しました",
"alertDeleteError": "削除中にエラーが発生しました",
"alertUrlCopied": "画像URLをクリップボードにコピーしました!",
"alertUploadFailed": "アップロードに失敗しました: {{error}}",
"alertUploadGeneric": "メディアファイルのアップロードに失敗しました。"
}
}
+104
View File
@@ -1198,6 +1198,110 @@
"thUnits": "ชิ้น",
"thRevenueCol": "ยอดรายได้",
"thPercentTotal": "% รวม"
},
"pageBuilder": {
"title": "ระบบสร้างหน้าเว็บ (Page Builder)",
"createPageBtn": "สร้างหน้าใหม่",
"orderSavedSuccess": "บันทึกลำดับ Section หน้าเว็บเรียบร้อยแล้ว",
"orderSaveError": "เกิดข้อผิดพลาดในการบันทึกลำดับ Layout กรุณาลองใหม่อีกครั้ง",
"pageTitleLabel": "ชื่อหน้า (Page Title)",
"pageTitlePlaceholder": "กรอกชื่อหน้า...",
"pageUrlLabel": "URL หน้าเว็บ",
"copyUrlSuccess": "คัดลอก URL เรียบร้อย",
"copyUrlTooltip": "คัดลอก URL ของหน้า",
"publishToWebsite": "เผยแพร่ขึ้นหน้าเว็บไซต์",
"badgePublished": "เผยแพร่แล้ว",
"badgeDraft": "ฉบับร่าง",
"btnSaveSettings": "บันทึกการตั้งค่า",
"btnPreviewLive": "เปิดดูหน้าจริง",
"btnDeletePage": "ลบหน้านี้",
"confirmDeletePage": "คุณแน่ใจหรือไม่ว่าต้องการลบหน้า \"{{title}}\"? การกระทำนี้จะลบบล็อกและเนื้อหาทั้งหมดอย่างถาวร!",
"sectionsTitle": "Sections ของหน้า",
"sectionsSubtitle": "จัดการบล็อกเนื้อหาทั้งหมดสำหรับหน้านี้",
"btnReorderLayout": "จัดเรียงลำดับบล็อก",
"btnAddSection": "เพิ่ม Section",
"selectBlockLabel": "เลือกบล็อกที่ต้องการแก้ไข",
"chooseBlockPlaceholder": "-- เลือกบล็อก --",
"hiddenBadge": "(ซ่อนอยู่)",
"editBlockTitle": "แก้ไขการตั้งค่าบล็อก",
"createBlockTitle": "สร้างบล็อกใหม่",
"badgeType": "ประเภท:",
"badgeBlockId": "Block ID:",
"badgePage": "หน้า:",
"badgePageId": "Page ID:",
"blockTypeLabel": "ประเภทบล็อก",
"browseVisualGallery": "เปิดดูแกลเลอรีบล็อก",
"btnSaveBlock": "บันทึกการตั้งค่าบล็อก",
"btnCreateBlock": "สร้างบล็อก",
"btnCancel": "ยกเลิก",
"emptyBlockPrompt": "เลือกบล็อกทางด้านซ้ายเพื่อแก้ไขเนื้อหา",
"reorderModalTitle": "จัดเรียงลำดับบล็อก",
"noSectionsInPage": "ยังไม่มี Section เนื้อหาในหน้านี้",
"hiddenTag": "ซ่อนอยู่",
"tooltipHideSection": "ซ่อน Section",
"tooltipShowSection": "แสดง Section",
"tooltipEditBlock": "แก้ไขเนื้อหาบล็อก",
"tooltipDeleteBlock": "ลบบล็อก",
"confirmDeleteSection": "คุณแน่ใจหรือไม่ว่าต้องการลบ Section นี้?",
"btnSaveOrder": "บันทึกลำดับ",
"btnClose": "ปิด",
"createPageModalTitle": "สร้างหน้าเว็บใหม่",
"pageSlugField": "Page Slug (เส้นทาง URL)",
"pageSlugPlaceholder": "เช่น about",
"slugHint": "ระบบจะแปลงเป็นตัวพิมพ์เล็กและคั่นด้วยขีด เช่น /pages/about-us",
"btnCreating": "กำลังสร้าง...",
"btnCreateSubmit": "สร้างหน้าใหม่",
"errCreatePage": "ไม่สามารถสร้างหน้าเว็บได้",
"errSaveSettings": "ไม่สามารถบันทึกการตั้งค่าหน้าได้",
"errDeletePage": "ไม่สามารถลบหน้าได้",
"errSaveBlock": "บันทึกไม่สำเร็จ: {{error}}"
},
"pageBuilderGallery": {
"galleryTitle": "เพิ่มบล็อกเนื้อหา (Block Section)",
"gallerySubtitle": "เลือกคอมโพเนนต์บล็อกเพื่อแทรกลงในโครงสร้างหน้าเว็บของคุณ",
"searchPlaceholder": "ค้นหาประเภทบล็อก...",
"categoryLabel": "หมวดหมู่: {{cat}}",
"btnSelectBlock": "เลือกบล็อกนี้",
"noBlocksFound": "ไม่พบบล็อกที่ตรงกับ \"{{search}}\"",
"categories": {
"all": "ทุกบล็อก",
"hero": "แบนเนอร์ & Hero",
"products": "สินค้า & ร้านค้า",
"editorial": "เรื่องราว & บทความ",
"articles": "องค์ประกอบบทความ",
"forms": "ฟอร์ม & ข้อมูลติดต่อ",
"custom": "สินค้า & กำหนดเอง"
}
},
"mediaLibrary": {
"title": "คลังสื่อมีเดีย",
"subtitle": "จัดการรูปภาพและไฟล์สื่อที่อัปโหลดของคุณ",
"btnUploading": "กำลังอัปโหลด...",
"btnUploadNew": "อัปโหลดรูปภาพใหม่",
"btnUploadNewFile": "อัปโหลดไฟล์ใหม่",
"pasteHintPrefix": "วางรูปภาพ (Paste)",
"pasteHint": "ที่ใดก็ได้ในหน้านี้ (Ctrl+V / ⌘+V) เพื่ออัปโหลดทันที",
"loading": "กำลังโหลดไฟล์สื่อ...",
"clickToCopy": "คลิกเพื่อคัดลอก URL",
"copyUrl": "คัดลอก URL",
"deleteTooltip": "ลบรูปภาพ",
"noImages": "ไม่พบรูปภาพ",
"noImagesSub": "อัปโหลดรูปภาพแรกของคุณเพื่อเริ่มต้นใช้งาน",
"noMediaFound": "ไม่พบไฟล์สื่อ",
"noMediaSub": "อัปโหลดรูปภาพหรือวิดีโอแรกของคุณเพื่อเริ่มต้นใช้งาน",
"itemsFound": "พบ {{count}} รายการ",
"modalTitle": "คลังสื่อมีเดีย",
"modalSubtitle": "เลือกหรืออัปโหลดรูปภาพและวิดีโอ (.mp4, .webm) สำหรับเนื้อหาของคุณ",
"toastUploaded": "✅ อัปโหลดสำเร็จ: {{name}}",
"toastUploadFailed": "❌ อัปโหลดไม่สำเร็จ",
"toastUploadError": "❌ เกิดข้อผิดพลาดในการอัปโหลด",
"toastPasting": "📋 กำลังวางรูปภาพ...",
"confirmDelete": "คุณแน่ใจหรือไม่ว่าต้องการลบรูปภาพนี้?",
"alertDeleteFailed": "ลบรูปภาพไม่สำเร็จ",
"alertDeleteError": "เกิดข้อผิดพลาดขณะลบรูปภาพ",
"alertUrlCopied": "คัดลอก URL รูปภาพลงคลิปบอร์ดแล้ว!",
"alertUploadFailed": "อัปโหลดไม่สำเร็จ: {{error}}",
"alertUploadGeneric": "ไม่สามารถอัปโหลดไฟล์สื่อได้"
}
}
+104
View File
@@ -1199,6 +1199,110 @@
"thUnits": "销量(件)",
"thRevenueCol": "销售额",
"thPercentTotal": "占总销售额 %"
},
"pageBuilder": {
"title": "页面构建器 (CMS)",
"createPageBtn": "创建新页面",
"orderSavedSuccess": "页面模块布局顺序保存成功。",
"orderSaveError": "保存布局顺序时出错,请重试。",
"pageTitleLabel": "页面标题",
"pageTitlePlaceholder": "输入页面标题...",
"pageUrlLabel": "页面 URL",
"copyUrlSuccess": "已复制 URL",
"copyUrlTooltip": "复制页面 URL",
"publishToWebsite": "发布到网站",
"badgePublished": "已发布",
"badgeDraft": "草稿",
"btnSaveSettings": "保存设置",
"btnPreviewLive": "实时预览",
"btnDeletePage": "删除此页面",
"confirmDeletePage": "确认删除页面 \"{{title}}\" 吗?此页面的所有内容块都将被永久删除!",
"sectionsTitle": "页面内容模块 (Sections)",
"sectionsSubtitle": "管理此页面的所有内容组件与模块。",
"btnReorderLayout": "调整模块顺序",
"btnAddSection": "添加 Section",
"selectBlockLabel": "选择要编辑的模块",
"chooseBlockPlaceholder": "-- 请选择模块 --",
"hiddenBadge": "(已隐藏)",
"editBlockTitle": "编辑模块设置",
"createBlockTitle": "创建新模块",
"badgeType": "类型:",
"badgeBlockId": "模块ID:",
"badgePage": "所属页面:",
"badgePageId": "页面ID:",
"blockTypeLabel": "模块类型",
"browseVisualGallery": "打开可视化画廊",
"btnSaveBlock": "保存模块设置",
"btnCreateBlock": "创建模块",
"btnCancel": "取消",
"emptyBlockPrompt": "请从左侧选择一个模块以编辑其内容",
"reorderModalTitle": "调整模块布局顺序",
"noSectionsInPage": "此页面暂无任何内容模块。",
"hiddenTag": "已隐藏",
"tooltipHideSection": "隐藏模块",
"tooltipShowSection": "显示模块",
"tooltipEditBlock": "编辑模块内容",
"tooltipDeleteBlock": "删除模块",
"confirmDeleteSection": "确认删除此模块吗?",
"btnSaveOrder": "保存顺序",
"btnClose": "关闭",
"createPageModalTitle": "创建新页面",
"pageSlugField": "页面 Slug (URL 路径)",
"pageSlugPlaceholder": "例如: about",
"slugHint": "将自动转换为小写 slug 格式 (例如: /pages/about-us)",
"btnCreating": "正在创建...",
"btnCreateSubmit": "创建页面",
"errCreatePage": "创建页面失败",
"errSaveSettings": "保存页面设置失败",
"errDeletePage": "删除页面失败",
"errSaveBlock": "保存失败: {{error}}"
},
"pageBuilderGallery": {
"galleryTitle": "添加内容模块 (Block Section)",
"gallerySubtitle": "选择要插入页面布局的可视化组件模块。",
"searchPlaceholder": "搜索模块类型...",
"categoryLabel": "分类: {{cat}}",
"btnSelectBlock": "选择此模块",
"noBlocksFound": "未找到与 \"{{search}}\" 匹配的模块。",
"categories": {
"all": "全部模块",
"hero": "横幅 & 首屏 (Hero)",
"products": "商品 & 商店",
"editorial": "品牌故事 & 专栏",
"articles": "文章元素",
"forms": "表单 & 联系信息",
"custom": "商品 & 自定义"
}
},
"mediaLibrary": {
"title": "媒体库",
"subtitle": "管理您上传的图片与媒体文件",
"btnUploading": "上传中...",
"btnUploadNew": "上传新图片",
"btnUploadNewFile": "上传新文件",
"pasteHintPrefix": "粘贴",
"pasteHint": "在此页面任意位置粘贴图片(Ctrl+V / ⌘+V)即可快速上传",
"loading": "正在加载媒体文件...",
"clickToCopy": "点击复制 URL",
"copyUrl": "复制 URL",
"deleteTooltip": "删除图片",
"noImages": "未找到图片",
"noImagesSub": "上传您的第一张图片以开始使用。",
"noMediaFound": "未找到媒体文件",
"noMediaSub": "上传您的第一张图片或视频以开始使用。",
"itemsFound": "共找到 {{count}} 项",
"modalTitle": "媒体库",
"modalSubtitle": "为您的内容选择或上传图片和视频(.mp4、.webm)。",
"toastUploaded": "✅ 上传成功:{{name}}",
"toastUploadFailed": "❌ 上传失败",
"toastUploadError": "❌ 上传发生错误",
"toastPasting": "📋 正在粘贴图片...",
"confirmDelete": "您确定要删除此图片吗?",
"alertDeleteFailed": "删除图片失败",
"alertDeleteError": "删除过程中发生错误",
"alertUrlCopied": "图片 URL 已复制到剪贴板!",
"alertUploadFailed": "上传失败:{{error}}",
"alertUploadGeneric": "上传媒体文件失败。"
}
}
+23 -21
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types';
import { Upload, Loader2, Image as ImageIcon, Trash2, ClipboardPaste } from 'lucide-react';
@@ -9,6 +10,7 @@ interface MediaItem {
}
export default function MediaLibraryPage() {
const { t } = useTranslation();
const [media, setMedia] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
@@ -52,17 +54,17 @@ export default function MediaLibraryPage() {
});
if (res.ok) {
await fetchMedia();
showToast(`✅ Uploaded: ${file.name || 'image'}`);
showToast(t('mediaLibrary.toastUploaded', { name: file.name || 'image' }));
} else {
showToast('❌ Upload failed');
showToast(t('mediaLibrary.toastUploadFailed'));
}
} catch (error) {
console.error('Failed to upload', error);
showToast('❌ Upload error');
showToast(t('mediaLibrary.toastUploadError'));
} finally {
setUploading(false);
}
}, []);
}, [t]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
@@ -83,13 +85,13 @@ export default function MediaLibraryPage() {
// Give the pasted file a sensible name
const ext = item.type.split('/')[1] || 'png';
const namedFile = new File([file], `paste-${Date.now()}.${ext}`, { type: item.type });
showToast('📋 Pasting image...');
showToast(t('mediaLibrary.toastPasting'));
await uploadFile(namedFile);
break;
}
}
},
[uploading, uploadFile]
[uploading, uploadFile, t]
);
useEffect(() => {
@@ -105,7 +107,7 @@ export default function MediaLibraryPage() {
e.stopPropagation();
if (!id) return;
if (!window.confirm('Are you sure you want to delete this image?')) return;
if (!window.confirm(t('mediaLibrary.confirmDelete'))) return;
try {
setDeletingId(id);
@@ -115,11 +117,11 @@ export default function MediaLibraryPage() {
if (res.ok) {
await fetchMedia();
} else {
alert('Failed to delete image');
alert(t('mediaLibrary.alertDeleteFailed'));
}
} catch (error) {
console.error('Failed to delete', error);
alert('An error occurred while deleting');
alert(t('mediaLibrary.alertDeleteError'));
} finally {
setDeletingId(null);
}
@@ -128,7 +130,7 @@ export default function MediaLibraryPage() {
const copyToClipboard = (url: string, e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(url);
alert('Image URL copied to clipboard!');
alert(t('mediaLibrary.alertUrlCopied'));
};
return (
@@ -160,16 +162,16 @@ export default function MediaLibraryPage() {
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
<ImageIcon className="w-8 h-8 text-cyan-glow" />
Media Library
{t('mediaLibrary.title')}
</h1>
<p className="text-slate-400 mt-2">Manage your uploaded images</p>
<p className="text-slate-400 mt-2">{t('mediaLibrary.subtitle')}</p>
</div>
<label className="btn-primary cursor-pointer inline-flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm">
{uploading ? (
<><Loader2 className="w-4 h-4 animate-spin" /> Uploading...</>
<><Loader2 className="w-4 h-4 animate-spin" /> {t('mediaLibrary.btnUploading')}</>
) : (
<><Upload className="w-4 h-4" /> Upload New Image</>
<><Upload className="w-4 h-4" /> {t('mediaLibrary.btnUploadNew')}</>
)}
<input type="file" className="hidden" accept="image/*" onChange={handleUpload} disabled={uploading} />
</label>
@@ -193,7 +195,7 @@ export default function MediaLibraryPage() {
>
<ClipboardPaste style={{ width: '1rem', height: '1rem', color: 'rgb(34,211,238)', flexShrink: 0 }} />
<span>
<span style={{ color: 'rgb(34,211,238)', fontWeight: 700 }}>Paste</span> an image anywhere on this page (Ctrl+V / ⌘+V) to upload instantly
<span style={{ color: 'rgb(34,211,238)', fontWeight: 700 }}>{t('mediaLibrary.pasteHintPrefix')}</span> {t('mediaLibrary.pasteHint')}
</span>
</div>
@@ -201,7 +203,7 @@ export default function MediaLibraryPage() {
{loading ? (
<div className="flex flex-col justify-center items-center h-full text-slate-500 space-y-4">
<Loader2 className="w-10 h-10 animate-spin text-cyan-glow" />
<p className="font-mono text-sm tracking-widest uppercase">Loading Media...</p>
<p className="font-mono text-sm tracking-widest uppercase">{t('mediaLibrary.loading')}</p>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
@@ -211,7 +213,7 @@ export default function MediaLibraryPage() {
className="group relative border-2 border-white/5 bg-night-950 rounded-xl overflow-hidden hover:border-cyan-glow transition-all hover:shadow-[0_0_15px_rgba(34,211,238,0.2)] aspect-square flex flex-col"
onClick={(e) => copyToClipboard(item.url, e)}
>
<div className="flex-1 overflow-hidden relative bg-black/50 cursor-pointer" title="Click to copy URL">
<div className="flex-1 overflow-hidden relative bg-black/50 cursor-pointer" title={t('mediaLibrary.clickToCopy')}>
<img
src={item.url}
alt={item.filename || 'media'}
@@ -219,7 +221,7 @@ export default function MediaLibraryPage() {
/>
{/* Hover Overlay */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
<span className="text-white text-xs font-bold uppercase tracking-wider">Copy URL</span>
<span className="text-white text-xs font-bold uppercase tracking-wider">{t('mediaLibrary.copyUrl')}</span>
</div>
</div>
{item.filename && (
@@ -230,7 +232,7 @@ export default function MediaLibraryPage() {
{item.id && (
<button
className="absolute top-2 right-2 p-1.5 bg-rose-500/80 text-white rounded hover:bg-rose-600 transition-colors opacity-0 group-hover:opacity-100"
title="Delete Image"
title={t('mediaLibrary.deleteTooltip')}
onClick={(e) => handleDelete(item.id, e)}
disabled={deletingId === item.id}
>
@@ -246,8 +248,8 @@ export default function MediaLibraryPage() {
{media.length === 0 && !loading && (
<div className="col-span-full flex flex-col items-center justify-center py-20 text-slate-500">
<ImageIcon className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg font-bold">No images found</p>
<p className="text-sm mt-2">Upload your first image to get started.</p>
<p className="text-lg font-bold">{t('mediaLibrary.noImages')}</p>
<p className="text-sm mt-2">{t('mediaLibrary.noImagesSub')}</p>
</div>
)}
</div>
+56 -54
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Layers,
Plus,
@@ -72,6 +73,7 @@ interface Brand {
}
export default function PageBuilderPage() {
const { t } = useTranslation();
const frontendBase = (
import.meta.env.DEV
? window.location.origin
@@ -388,11 +390,11 @@ export default function PageBuilderPage() {
await fetchPagesList();
setSelectedPageId(data.id);
} else {
alert(data.error || 'Failed to create page');
alert(data.error || t('pageBuilder.errCreatePage', 'Failed to create page'));
}
} catch (err) {
console.error('Create page error:', err);
alert('Failed to create page due to network error');
alert(t('pageBuilder.errCreatePage', 'Failed to create page'));
} finally {
setIsSavingPage(false);
}
@@ -418,11 +420,11 @@ export default function PageBuilderPage() {
setPage((prev) => (prev ? { ...prev, isPublished: data.isPublished ?? prev.isPublished } : prev));
await fetchPagesList();
} else {
alert(data.error || 'Failed to save page settings');
alert(data.error || t('pageBuilder.errSaveSettings', 'Failed to save page settings'));
}
} catch (err) {
console.error('Save page settings error:', err);
alert('Failed to save page settings due to network error');
alert(t('pageBuilder.errSaveSettings', 'Failed to save page settings'));
} finally {
setIsSavingPageSettings(false);
}
@@ -430,7 +432,7 @@ export default function PageBuilderPage() {
const handleDeletePage = async () => {
if (!page || page.slug === 'home') return;
if (!confirm(`Are you sure you want to delete the page "${page.title}"? This will delete all of its section blocks permanently!`)) return;
if (!confirm(t('pageBuilder.confirmDeletePage', { title: page.title, defaultValue: `Are you sure you want to delete the page "${page.title}"? This will delete all of its section blocks permanently!` }))) return;
try {
const res = await fetch(`${API_BASE}/pages/${page.id}`, {
@@ -443,7 +445,7 @@ export default function PageBuilderPage() {
setSections([]);
await fetchPagesList();
} else {
alert('Failed to delete page');
alert(t('pageBuilder.errDeletePage', 'Failed to delete page'));
}
} catch (err) {
console.error('Delete page error:', err);
@@ -518,7 +520,7 @@ export default function PageBuilderPage() {
};
const handleDeleteSection = async (id: number) => {
if (!confirm('Are you sure you want to delete this section?')) return;
if (!confirm(t('pageBuilder.confirmDeleteSection', 'Are you sure you want to delete this section?'))) return;
try {
const res = await fetch(`${API_BASE}/pages/sections/${id}`, {
method: 'DELETE'
@@ -929,7 +931,7 @@ export default function PageBuilderPage() {
if (selectedPageId) fetchPageDetails(selectedPageId);
} else {
const errData = await res.json();
alert(`Failed to save: ${errData.error || 'Unknown error'}`);
alert(t('pageBuilder.errSaveBlock', { error: errData.error || 'Unknown error', defaultValue: `Failed to save: ${errData.error || 'Unknown error'}` }));
}
} catch (err) {
console.error('Save section error:', err);
@@ -941,7 +943,7 @@ export default function PageBuilderPage() {
{/* Global Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Page Builder (CMS)</h1>
<h1 className="text-3xl font-bold">{t('pageBuilder.title', 'Page Builder (CMS)')}</h1>
<div className="h-6 w-px bg-white/10 hidden md:block"></div>
<div className="flex items-center gap-3">
<select
@@ -959,7 +961,7 @@ export default function PageBuilderPage() {
onClick={() => setIsPageModalOpen(true)}
className="px-4 py-2 bg-transparent border border-dashed border-white/20 hover:border-white/50 text-sm font-bold rounded-xl text-slate-300 hover:text-white transition-all uppercase tracking-wider cursor-pointer flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Create Page
<Plus className="w-4 h-4" /> {t('pageBuilder.createPageBtn', 'Create Page')}
</button>
</div>
</div>
@@ -969,13 +971,13 @@ export default function PageBuilderPage() {
{saveStatus === 'saved' && (
<div className="p-4 bg-cyan-glow/10 border border-cyan-glow/30 text-cyan-glow rounded-xl flex items-center gap-2">
<CheckCircle2 className="w-5 h-5" />
Homepage section ordering updated successfully.
{t('pageBuilder.orderSavedSuccess', 'Homepage section ordering updated successfully.')}
</div>
)}
{saveStatus === 'error' && (
<div className="p-4 bg-rose-500/10 border border-rose-500/30 text-rose-500 rounded-xl">
Error saving layout order. Please try again.
{t('pageBuilder.orderSaveError', 'Error saving layout order. Please try again.')}
</div>
)}
@@ -995,20 +997,20 @@ export default function PageBuilderPage() {
<div className="space-y-4">
<div className="flex flex-col gap-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Page Title
{t('pageBuilder.pageTitleLabel', 'Page Title')}
</label>
<input
type="text"
value={page.title || ''}
onChange={(e) => setPage({ ...page, title: e.target.value })}
className="bg-slate-900/50 border border-white/10 text-white rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-cyan-glow/50 transition-colors w-full"
placeholder="Enter page title..."
placeholder={t('pageBuilder.pageTitlePlaceholder', 'Enter page title...')}
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Page URL
{t('pageBuilder.pageUrlLabel', 'Page URL')}
</label>
<div className="flex items-center gap-2">
<input
@@ -1028,7 +1030,7 @@ export default function PageBuilderPage() {
className={`p-2.5 rounded-xl border border-white/10 text-white transition-all flex items-center justify-center ${
isCopied ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10'
}`}
title="Copy page URL"
title={t('pageBuilder.copyUrlTooltip', 'Copy page URL')}
>
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
@@ -1044,7 +1046,7 @@ export default function PageBuilderPage() {
className="h-4 w-4 rounded border-white/20 bg-slate-900 text-cyan-glow focus:ring-cyan-glow focus:ring-offset-slate-900"
/>
<span className="text-sm font-bold text-slate-200">
Publish to Website
{t('pageBuilder.publishToWebsite', 'Publish to Website')}
</span>
</label>
<span
@@ -1052,7 +1054,7 @@ export default function PageBuilderPage() {
page.isPublished ? 'bg-emerald-500/10 text-emerald-400' : 'bg-amber-500/10 text-amber-400'
}`}
>
{page.isPublished ? 'Published' : 'Draft'}
{page.isPublished ? t('pageBuilder.badgePublished', 'Published') : t('pageBuilder.badgeDraft', 'Draft')}
</span>
</div>
</div>
@@ -1069,7 +1071,7 @@ export default function PageBuilderPage() {
) : (
<Save className="w-4 h-4" />
)}
Save Settings
{t('pageBuilder.btnSaveSettings', 'Save Settings')}
</button>
<a
href={getPageUrl(page.slug)}
@@ -1079,7 +1081,7 @@ export default function PageBuilderPage() {
title={`Open ${page.title} live preview`}
>
<ExternalLink className="w-4 h-4" />
Preview Live
{t('pageBuilder.btnPreviewLive', 'Preview Live')}
</a>
</div>
{page.slug !== 'home' && (
@@ -1087,7 +1089,7 @@ export default function PageBuilderPage() {
onClick={handleDeletePage}
className="text-xs font-bold text-rose-500 hover:text-rose-400 hover:underline uppercase tracking-wider transition-all mt-2 w-fit mx-auto"
>
Delete this page
{t('pageBuilder.btnDeletePage', 'Delete this page')}
</button>
)}
</div>
@@ -1097,8 +1099,8 @@ export default function PageBuilderPage() {
{/* Section Management Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-night-950/40 border border-white/5 p-4 rounded-2xl">
<div>
<h2 className="text-lg font-bold">Page Sections</h2>
<p className="text-xs text-slate-400 mt-1">Manage the content blocks for this page.</p>
<h2 className="text-lg font-bold">{t('pageBuilder.sectionsTitle', 'Page Sections')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('pageBuilder.sectionsSubtitle', 'Manage the content blocks for this page.')}</p>
</div>
<div className="flex gap-3">
<button
@@ -1106,14 +1108,14 @@ export default function PageBuilderPage() {
className="flex items-center gap-2 px-5 py-2.5 bg-white/10 hover:bg-white/20 border border-white/5 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
>
<Layers className="w-4 h-4" />
Reorder Layout
{t('pageBuilder.btnReorderLayout', 'Reorder Layout')}
</button>
<button
onClick={openAddModal}
className="flex items-center gap-2 px-6 py-2.5 bg-cyan-glow text-night-900 hover:bg-cyan-glow/90 shadow-lg shadow-cyan-glow/20 rounded-xl font-bold text-xs uppercase tracking-wider transition-all"
>
<Plus className="w-4 h-4" />
Add Section
{t('pageBuilder.btnAddSection', 'Add Section')}
</button>
</div>
</div>
@@ -1121,7 +1123,7 @@ export default function PageBuilderPage() {
{/* Block Selection Dropdown */}
<div className="bg-night-950/40 border border-white/5 p-4 rounded-2xl space-y-3">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Select Block to Edit
{t('pageBuilder.selectBlockLabel', 'Select Block to Edit')}
</label>
<select
value={editingSection?.id?.toString() || ''}
@@ -1136,14 +1138,14 @@ export default function PageBuilderPage() {
}}
className="bg-slate-900 border border-white/10 text-white rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-cyan-glow/50 focus:ring-1 focus:ring-cyan-glow transition-all w-full"
>
<option value="">-- Choose a block --</option>
<option value="">{t('pageBuilder.chooseBlockPlaceholder', '-- Choose a block --')}</option>
{sections.map((section, idx) => {
const title = getAdminDisplayString(
section.payload?.title || section.payload?.tagline || section.payload?.quote || (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
) || `Block #${section.id}`;
return (
<option key={section.id} value={section.id?.toString()}>
{idx + 1}. {section.type} - {title} {section.isActive ? '' : '(Hidden)'}
{idx + 1}. {section.type} - {title} {section.isActive ? '' : t('pageBuilder.hiddenBadge', '(Hidden)')}
</option>
);
})}
@@ -1158,22 +1160,22 @@ export default function PageBuilderPage() {
<div className="flex flex-col gap-1">
<h3 className="text-xl font-bold flex items-center gap-3">
<Settings2 className="w-5 h-5 text-cyan-glow" />
{editingSection.id ? 'Edit Block Settings' : 'Create Layout Block'}
{editingSection.id ? t('pageBuilder.editBlockTitle', 'Edit Block Settings') : t('pageBuilder.createBlockTitle', 'Create Layout Block')}
</h3>
{editingSection.id && (
<div className="flex items-center gap-2 mt-1 ml-8 flex-wrap">
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-cyan-glow/10 text-cyan-glow rounded border border-cyan-glow/20 uppercase tracking-wider">
Type: {editingSection.type}
{t('pageBuilder.badgeType', 'Type:')} {editingSection.type}
</span>
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-white/5 text-slate-400 rounded border border-white/10 tracking-wider">
Block ID: {editingSection.id}
{t('pageBuilder.badgeBlockId', 'Block ID:')} {editingSection.id}
</span>
<div className="w-px h-3 bg-white/10 mx-1"></div>
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-fuchsia-500/10 text-fuchsia-400 rounded border border-fuchsia-500/20 uppercase tracking-wider">
Page: {page?.slug || 'Unknown'}
{t('pageBuilder.badgePage', 'Page:')} {page?.slug || 'Unknown'}
</span>
<span className="px-2 py-0.5 text-[10px] font-bold font-mono bg-white/5 text-slate-400 rounded border border-white/10 tracking-wider">
Page ID: {page?.id || '-'}
{t('pageBuilder.badgePageId', 'Page ID:')} {page?.id || '-'}
</span>
</div>
)}
@@ -1212,14 +1214,14 @@ export default function PageBuilderPage() {
{!editingSection.id && (
<div className="space-y-2 bg-night-950/40 p-4 rounded-2xl border border-white/5">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Block Type</label>
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">{t('pageBuilder.blockTypeLabel', 'Block Type')}</label>
<button
type="button"
onClick={() => setIsAddBlockModalOpen(true)}
className="text-xs font-bold text-cyan-glow hover:underline flex items-center gap-1.5 cursor-pointer"
>
<Sparkles className="w-3.5 h-3.5 text-cyan-glow" />
Browse Visual Gallery
{t('pageBuilder.browseVisualGallery', 'Browse Visual Gallery')}
</button>
</div>
<select
@@ -1630,14 +1632,14 @@ export default function PageBuilderPage() {
type="submit"
className="flex-1 px-8 py-3 bg-cyan-glow text-night-900 hover:bg-cyan-glow/80 disabled:opacity-50 font-bold rounded-xl transition-all"
>
{editingSection.id ? 'Save Block Settings' : 'Create Block'}
{editingSection.id ? t('pageBuilder.btnSaveBlock', 'Save Block Settings') : t('pageBuilder.btnCreateBlock', 'Create Block')}
</button>
<button
type="button"
onClick={() => setEditingSection(null)}
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
>
Cancel
{t('pageBuilder.btnCancel', 'Cancel')}
</button>
</div>
</form>
@@ -1645,7 +1647,7 @@ export default function PageBuilderPage() {
) : (
<div className="h-full flex flex-col items-center justify-center p-16 text-center text-slate-500 bg-white/[0.02] border border-white/5 rounded-3xl min-h-[400px]">
<Settings2 className="w-12 h-12 mx-auto text-slate-700 mb-4" />
<p>Select a block from the left to edit its content</p>
<p>{t('pageBuilder.emptyBlockPrompt', 'Select a block from the left to edit its content')}</p>
</div>
)}
</div>
@@ -1659,7 +1661,7 @@ export default function PageBuilderPage() {
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<h3 className="text-xl font-bold flex items-center gap-2">
<Layers className="w-5 h-5 text-cyan-glow" />
Reorder Layout
{t('pageBuilder.reorderModalTitle', 'Reorder Layout')}
</h3>
<button
onClick={() => setIsReorderModalOpen(false)}
@@ -1673,7 +1675,7 @@ export default function PageBuilderPage() {
{sections.length === 0 ? (
<div className="p-16 text-center text-slate-500 space-y-4 bg-white/[0.02] border border-white/5 rounded-2xl">
<Layers className="w-12 h-12 mx-auto text-slate-700" />
<p>No content sections in this page yet.</p>
<p>{t('pageBuilder.noSectionsInPage', 'No content sections in this page yet.')}</p>
</div>
) : (
sections.map((section, idx) => (
@@ -1712,7 +1714,7 @@ export default function PageBuilderPage() {
</span>
{!section.isActive && (
<span className="text-[9px] uppercase font-bold tracking-widest text-slate-500 bg-slate-900 px-1.5 py-0.5 rounded border border-white/10 shrink-0">
Hidden
{t('pageBuilder.hiddenTag', 'Hidden')}
</span>
)}
<h3 className="text-xs font-bold text-slate-100 truncate">
@@ -1735,7 +1737,7 @@ export default function PageBuilderPage() {
? 'bg-cyan-glow/10 border-cyan-glow/20 text-cyan-glow hover:bg-cyan-glow/20'
: 'bg-white/5 border-white/5 text-slate-400 hover:bg-white/10 hover:text-white'
}`}
title={section.isActive ? 'Hide Section' : 'Show Section'}
title={section.isActive ? t('pageBuilder.tooltipHideSection', 'Hide Section') : t('pageBuilder.tooltipShowSection', 'Show Section')}
>
{section.isActive ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
</button>
@@ -1745,14 +1747,14 @@ export default function PageBuilderPage() {
openEditModal(section);
}}
className="p-1.5 bg-white/5 border border-white/5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all"
title="Edit Block Content"
title={t('pageBuilder.tooltipEditBlock', 'Edit Block Content')}
>
<Edit className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteSection(section.id)}
className="p-1.5 bg-rose-500/10 border border-rose-500/10 rounded-lg text-rose-500 hover:bg-rose-500 hover:text-white transition-all"
title="Delete Block"
title={t('pageBuilder.tooltipDeleteBlock', 'Delete Block')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
@@ -1774,7 +1776,7 @@ export default function PageBuilderPage() {
) : (
<Save className="w-4 h-4" />
)}
Save Order
{t('pageBuilder.btnSaveOrder', 'Save Order')}
</button>
)}
<button
@@ -1782,7 +1784,7 @@ export default function PageBuilderPage() {
onClick={() => setIsReorderModalOpen(false)}
className="px-6 py-2.5 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all text-sm uppercase tracking-wider"
>
Close
{t('pageBuilder.btnClose', 'Close')}
</button>
</div>
</div>
@@ -1796,7 +1798,7 @@ export default function PageBuilderPage() {
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<h3 className="text-xl font-bold flex items-center gap-2">
<Layers className="w-5 h-5 text-cyan-glow" />
Create New Page
{t('pageBuilder.createPageModalTitle', 'Create New Page')}
</h3>
<button
onClick={() => setIsPageModalOpen(false)}
@@ -1808,11 +1810,11 @@ export default function PageBuilderPage() {
<form onSubmit={handleCreatePage} className="p-6 space-y-4">
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Page Title</label>
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">{t('pageBuilder.pageTitleLabel', 'Page Title')}</label>
<input
type="text"
required
placeholder="e.g. About Us"
placeholder={t('pageBuilder.pageTitlePlaceholder', 'e.g. About Us')}
value={newPageData.title}
onChange={(e) => setNewPageData({ ...newPageData, title: e.target.value })}
className="admin-input w-full"
@@ -1820,16 +1822,16 @@ export default function PageBuilderPage() {
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Page Slug (URL Path)</label>
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">{t('pageBuilder.pageSlugField', 'Page Slug (URL Path)')}</label>
<input
type="text"
required
placeholder="e.g. about"
placeholder={t('pageBuilder.pageSlugPlaceholder', 'e.g. about')}
value={newPageData.slug}
onChange={(e) => setNewPageData({ ...newPageData, slug: e.target.value })}
className="admin-input w-full"
/>
<p className="text-[9px] text-slate-500 font-medium">Will be converted to lowercase, slugified format (e.g. /pages/about-us).</p>
<p className="text-[9px] text-slate-500 font-medium">{t('pageBuilder.slugHint', 'Will be converted to lowercase, slugified format (e.g. /pages/about-us).')}</p>
</div>
<div className="pt-4 border-t border-white/5 flex gap-3">
@@ -1838,14 +1840,14 @@ export default function PageBuilderPage() {
disabled={isSavingPage}
className="flex-1 px-6 py-2.5 bg-cyan-glow text-night-900 hover:bg-cyan-glow/80 disabled:opacity-50 font-bold rounded-xl transition-all text-sm cursor-pointer"
>
{isSavingPage ? 'Creating...' : 'Create Page'}
{isSavingPage ? t('pageBuilder.btnCreating', 'Creating...') : t('pageBuilder.btnCreateSubmit', 'Create Page')}
</button>
<button
type="button"
onClick={() => setIsPageModalOpen(false)}
className="px-6 py-2.5 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all text-sm cursor-pointer"
>
Cancel
{t('pageBuilder.btnCancel', 'Cancel')}
</button>
</div>
</form>