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 React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types'; import { API_BASE } from '@homaroma/types';
import { X, Upload, Loader2, Image as ImageIcon, Video } from 'lucide-react'; 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) { export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLibraryModalProps) {
const { t } = useTranslation();
const [media, setMedia] = useState<MediaItem[]>([]); const [media, setMedia] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@@ -63,11 +65,11 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
} }
} else { } else {
const err = await res.json(); const err = await res.json();
alert(`Upload failed: ${err.error || 'Unknown error'}`); alert(t('mediaLibrary.alertUploadFailed', { error: err.error || 'Unknown error' }));
} }
} catch (error) { } catch (error) {
console.error('Failed to upload', error); console.error('Failed to upload', error);
alert('Failed to upload media file.'); alert(t('mediaLibrary.alertUploadGeneric'));
} finally { } finally {
setUploading(false); setUploading(false);
e.target.value = ''; // Reset input e.target.value = ''; // Reset input
@@ -90,9 +92,9 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
<div> <div>
<h2 className="text-xl font-bold text-white flex items-center gap-2"> <h2 className="text-xl font-bold text-white flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-cyan-glow" /> <ImageIcon className="w-5 h-5 text-cyan-glow" />
Media Library {t('mediaLibrary.modalTitle')}
</h2> </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> </div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition-colors text-slate-400 hover:text-white"> <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" /> <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"> <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"> <label className="btn-primary cursor-pointer inline-flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm">
{uploading ? ( {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} /> <input type="file" className="hidden" accept="image/*,video/*" onChange={handleUpload} disabled={uploading} />
</label> </label>
<div className="text-sm text-slate-500 font-mono"> <div className="text-sm text-slate-500 font-mono">
{media.length} items found {t('mediaLibrary.itemsFound', { count: media.length })}
</div> </div>
</div> </div>
@@ -119,7 +121,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLi
{loading ? ( {loading ? (
<div className="flex flex-col justify-center items-center h-full text-slate-500 space-y-4"> <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" /> <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>
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6"> <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 && ( {media.length === 0 && !loading && (
<div className="col-span-full flex flex-col items-center justify-center py-20 text-slate-500"> <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" /> <ImageIcon className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg font-bold">No media found</p> <p className="text-lg font-bold">{t('mediaLibrary.noMediaFound')}</p>
<p className="text-sm mt-2">Upload your first image or video to get started.</p> <p className="text-sm mt-2">{t('mediaLibrary.noMediaSub')}</p>
</div> </div>
)} )}
</div> </div>
@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
X, X,
Search, Search,
@@ -28,17 +29,18 @@ interface BlockDefinition {
renderPreview: () => React.ReactNode; renderPreview: () => React.ReactNode;
} }
const CATEGORIES = [ const CATEGORY_KEYS = [
{ id: 'all', name: 'All Blocks' }, { id: 'all', key: 'pageBuilderGallery.categories.all' },
{ id: 'hero', name: 'Banner & Hero' }, { id: 'hero', key: 'pageBuilderGallery.categories.hero' },
{ id: 'products', name: 'Products & Shop' }, { id: 'products', key: 'pageBuilderGallery.categories.products' },
{ id: 'editorial', name: 'Story & Editorial' }, { id: 'editorial', key: 'pageBuilderGallery.categories.editorial' },
{ id: 'articles', name: 'Article Elements' }, { id: 'articles', key: 'pageBuilderGallery.categories.articles' },
{ id: 'forms', name: 'Forms & Info' }, { id: 'forms', key: 'pageBuilderGallery.categories.forms' },
{ id: 'custom', name: 'Product & Custom' } { id: 'custom', key: 'pageBuilderGallery.categories.custom' }
]; ];
export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddSectionModalProps) { export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddSectionModalProps) {
const { t } = useTranslation();
const [activeCategory, setActiveCategory] = useState('all'); const [activeCategory, setActiveCategory] = useState('all');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -605,10 +607,10 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
<div> <div>
<h3 className="text-xl font-extrabold flex items-center gap-3 text-white"> <h3 className="text-xl font-extrabold flex items-center gap-3 text-white">
<Sparkles className="w-6 h-6 text-cyan-glow animate-pulse" /> <Sparkles className="w-6 h-6 text-cyan-glow animate-pulse" />
Add Block Section {t('pageBuilderGallery.galleryTitle')}
</h3> </h3>
<p className="text-xs text-slate-400 mt-1"> <p className="text-xs text-slate-400 mt-1">
Select a visual block component to insert into your page layout. {t('pageBuilderGallery.gallerySubtitle')}
</p> </p>
</div> </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" /> <Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input <input
type="text" type="text"
placeholder="Search blocks..." placeholder={t('pageBuilderGallery.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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" 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 */} {/* 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"> <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 <button
key={cat.id} key={cat.id}
type="button" type="button"
@@ -655,7 +657,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
: '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)}
</button> </button>
))} ))}
</div> </div>
@@ -665,7 +667,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
{filteredBlocks.length === 0 ? ( {filteredBlocks.length === 0 ? (
<div className="col-span-full py-20 text-center text-slate-500 space-y-3"> <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" /> <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> </div>
) : ( ) : (
filteredBlocks.map((block) => ( filteredBlocks.map((block) => (
@@ -708,14 +710,14 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS
{/* Hover Selection Footer */} {/* Hover Selection Footer */}
<div className="mt-4 pt-3 border-t border-white/5 flex items-center justify-between"> <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"> <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> </span>
<button <button
type="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" 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" /> <Plus className="w-3.5 h-3.5" />
Select Block {t('pageBuilderGallery.btnSelectBlock')}
</button> </button>
</div> </div>
</div> </div>
+104
View File
@@ -1199,6 +1199,110 @@
"thUnits": "Units", "thUnits": "Units",
"thRevenueCol": "Revenue", "thRevenueCol": "Revenue",
"thPercentTotal": "% Total" "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": "数量", "thUnits": "数量",
"thRevenueCol": "売上高", "thRevenueCol": "売上高",
"thPercentTotal": "全体比 %" "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": "ชิ้น", "thUnits": "ชิ้น",
"thRevenueCol": "ยอดรายได้", "thRevenueCol": "ยอดรายได้",
"thPercentTotal": "% รวม" "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": "销量(件)", "thUnits": "销量(件)",
"thRevenueCol": "销售额", "thRevenueCol": "销售额",
"thPercentTotal": "占总销售额 %" "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 React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { API_BASE } from '@homaroma/types'; import { API_BASE } from '@homaroma/types';
import { Upload, Loader2, Image as ImageIcon, Trash2, ClipboardPaste } from 'lucide-react'; import { Upload, Loader2, Image as ImageIcon, Trash2, ClipboardPaste } from 'lucide-react';
@@ -9,6 +10,7 @@ interface MediaItem {
} }
export default function MediaLibraryPage() { export default function MediaLibraryPage() {
const { t } = useTranslation();
const [media, setMedia] = useState<MediaItem[]>([]); const [media, setMedia] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@@ -52,17 +54,17 @@ export default function MediaLibraryPage() {
}); });
if (res.ok) { if (res.ok) {
await fetchMedia(); await fetchMedia();
showToast(`✅ Uploaded: ${file.name || 'image'}`); showToast(t('mediaLibrary.toastUploaded', { name: file.name || 'image' }));
} else { } else {
showToast('❌ Upload failed'); showToast(t('mediaLibrary.toastUploadFailed'));
} }
} catch (error) { } catch (error) {
console.error('Failed to upload', error); console.error('Failed to upload', error);
showToast('❌ Upload error'); showToast(t('mediaLibrary.toastUploadError'));
} finally { } finally {
setUploading(false); setUploading(false);
} }
}, []); }, [t]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@@ -83,13 +85,13 @@ export default function MediaLibraryPage() {
// Give the pasted file a sensible name // Give the pasted file a sensible name
const ext = item.type.split('/')[1] || 'png'; const ext = item.type.split('/')[1] || 'png';
const namedFile = new File([file], `paste-${Date.now()}.${ext}`, { type: item.type }); const namedFile = new File([file], `paste-${Date.now()}.${ext}`, { type: item.type });
showToast('📋 Pasting image...'); showToast(t('mediaLibrary.toastPasting'));
await uploadFile(namedFile); await uploadFile(namedFile);
break; break;
} }
} }
}, },
[uploading, uploadFile] [uploading, uploadFile, t]
); );
useEffect(() => { useEffect(() => {
@@ -105,7 +107,7 @@ export default function MediaLibraryPage() {
e.stopPropagation(); e.stopPropagation();
if (!id) return; if (!id) return;
if (!window.confirm('Are you sure you want to delete this image?')) return; if (!window.confirm(t('mediaLibrary.confirmDelete'))) return;
try { try {
setDeletingId(id); setDeletingId(id);
@@ -115,11 +117,11 @@ export default function MediaLibraryPage() {
if (res.ok) { if (res.ok) {
await fetchMedia(); await fetchMedia();
} else { } else {
alert('Failed to delete image'); alert(t('mediaLibrary.alertDeleteFailed'));
} }
} catch (error) { } catch (error) {
console.error('Failed to delete', error); console.error('Failed to delete', error);
alert('An error occurred while deleting'); alert(t('mediaLibrary.alertDeleteError'));
} finally { } finally {
setDeletingId(null); setDeletingId(null);
} }
@@ -128,7 +130,7 @@ export default function MediaLibraryPage() {
const copyToClipboard = (url: string, e: React.MouseEvent) => { const copyToClipboard = (url: string, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
alert('Image URL copied to clipboard!'); alert(t('mediaLibrary.alertUrlCopied'));
}; };
return ( return (
@@ -160,16 +162,16 @@ export default function MediaLibraryPage() {
<div> <div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3"> <h1 className="text-3xl font-bold text-white flex items-center gap-3">
<ImageIcon className="w-8 h-8 text-cyan-glow" /> <ImageIcon className="w-8 h-8 text-cyan-glow" />
Media Library {t('mediaLibrary.title')}
</h1> </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> </div>
<label className="btn-primary cursor-pointer inline-flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm"> <label className="btn-primary cursor-pointer inline-flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm">
{uploading ? ( {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} /> <input type="file" className="hidden" accept="image/*" onChange={handleUpload} disabled={uploading} />
</label> </label>
@@ -193,7 +195,7 @@ export default function MediaLibraryPage() {
> >
<ClipboardPaste style={{ width: '1rem', height: '1rem', color: 'rgb(34,211,238)', flexShrink: 0 }} /> <ClipboardPaste style={{ width: '1rem', height: '1rem', color: 'rgb(34,211,238)', flexShrink: 0 }} />
<span> <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> </span>
</div> </div>
@@ -201,7 +203,7 @@ export default function MediaLibraryPage() {
{loading ? ( {loading ? (
<div className="flex flex-col justify-center items-center h-full text-slate-500 space-y-4"> <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" /> <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>
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6"> <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" 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)} 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 <img
src={item.url} src={item.url}
alt={item.filename || 'media'} alt={item.filename || 'media'}
@@ -219,7 +221,7 @@ export default function MediaLibraryPage() {
/> />
{/* Hover Overlay */} {/* 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"> <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>
</div> </div>
{item.filename && ( {item.filename && (
@@ -230,7 +232,7 @@ export default function MediaLibraryPage() {
{item.id && ( {item.id && (
<button <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" 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)} onClick={(e) => handleDelete(item.id, e)}
disabled={deletingId === item.id} disabled={deletingId === item.id}
> >
@@ -246,8 +248,8 @@ export default function MediaLibraryPage() {
{media.length === 0 && !loading && ( {media.length === 0 && !loading && (
<div className="col-span-full flex flex-col items-center justify-center py-20 text-slate-500"> <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" /> <ImageIcon className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg font-bold">No images found</p> <p className="text-lg font-bold">{t('mediaLibrary.noImages')}</p>
<p className="text-sm mt-2">Upload your first image to get started.</p> <p className="text-sm mt-2">{t('mediaLibrary.noImagesSub')}</p>
</div> </div>
)} )}
</div> </div>
+56 -54
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Layers, Layers,
Plus, Plus,
@@ -72,6 +73,7 @@ interface Brand {
} }
export default function PageBuilderPage() { export default function PageBuilderPage() {
const { t } = useTranslation();
const frontendBase = ( const frontendBase = (
import.meta.env.DEV import.meta.env.DEV
? window.location.origin ? window.location.origin
@@ -388,11 +390,11 @@ export default function PageBuilderPage() {
await fetchPagesList(); await fetchPagesList();
setSelectedPageId(data.id); setSelectedPageId(data.id);
} else { } else {
alert(data.error || 'Failed to create page'); alert(data.error || t('pageBuilder.errCreatePage', 'Failed to create page'));
} }
} catch (err) { } catch (err) {
console.error('Create page error:', 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 { } finally {
setIsSavingPage(false); setIsSavingPage(false);
} }
@@ -418,11 +420,11 @@ export default function PageBuilderPage() {
setPage((prev) => (prev ? { ...prev, isPublished: data.isPublished ?? prev.isPublished } : prev)); setPage((prev) => (prev ? { ...prev, isPublished: data.isPublished ?? prev.isPublished } : prev));
await fetchPagesList(); await fetchPagesList();
} else { } else {
alert(data.error || 'Failed to save page settings'); alert(data.error || t('pageBuilder.errSaveSettings', 'Failed to save page settings'));
} }
} catch (err) { } catch (err) {
console.error('Save page settings error:', 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 { } finally {
setIsSavingPageSettings(false); setIsSavingPageSettings(false);
} }
@@ -430,7 +432,7 @@ export default function PageBuilderPage() {
const handleDeletePage = async () => { const handleDeletePage = async () => {
if (!page || page.slug === 'home') return; 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 { try {
const res = await fetch(`${API_BASE}/pages/${page.id}`, { const res = await fetch(`${API_BASE}/pages/${page.id}`, {
@@ -443,7 +445,7 @@ export default function PageBuilderPage() {
setSections([]); setSections([]);
await fetchPagesList(); await fetchPagesList();
} else { } else {
alert('Failed to delete page'); alert(t('pageBuilder.errDeletePage', 'Failed to delete page'));
} }
} catch (err) { } catch (err) {
console.error('Delete page error:', err); console.error('Delete page error:', err);
@@ -518,7 +520,7 @@ export default function PageBuilderPage() {
}; };
const handleDeleteSection = async (id: number) => { 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 { try {
const res = await fetch(`${API_BASE}/pages/sections/${id}`, { const res = await fetch(`${API_BASE}/pages/sections/${id}`, {
method: 'DELETE' method: 'DELETE'
@@ -929,7 +931,7 @@ export default function PageBuilderPage() {
if (selectedPageId) fetchPageDetails(selectedPageId); if (selectedPageId) fetchPageDetails(selectedPageId);
} else { } else {
const errData = await res.json(); 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) { } catch (err) {
console.error('Save section error:', err); console.error('Save section error:', err);
@@ -941,7 +943,7 @@ export default function PageBuilderPage() {
{/* Global Header */} {/* Global Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center 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="h-6 w-px bg-white/10 hidden md:block"></div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<select <select
@@ -959,7 +961,7 @@ export default function PageBuilderPage() {
onClick={() => setIsPageModalOpen(true)} 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" 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> </button>
</div> </div>
</div> </div>
@@ -969,13 +971,13 @@ export default function PageBuilderPage() {
{saveStatus === 'saved' && ( {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"> <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" /> <CheckCircle2 className="w-5 h-5" />
Homepage section ordering updated successfully. {t('pageBuilder.orderSavedSuccess', 'Homepage section ordering updated successfully.')}
</div> </div>
)} )}
{saveStatus === 'error' && ( {saveStatus === 'error' && (
<div className="p-4 bg-rose-500/10 border border-rose-500/30 text-rose-500 rounded-xl"> <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> </div>
)} )}
@@ -995,20 +997,20 @@ export default function PageBuilderPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400"> <label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Page Title {t('pageBuilder.pageTitleLabel', 'Page Title')}
</label> </label>
<input <input
type="text" type="text"
value={page.title || ''} value={page.title || ''}
onChange={(e) => setPage({ ...page, title: e.target.value })} 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" 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>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400"> <label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Page URL {t('pageBuilder.pageUrlLabel', 'Page URL')}
</label> </label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <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 ${ 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' 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" />} {isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button> </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" 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"> <span className="text-sm font-bold text-slate-200">
Publish to Website {t('pageBuilder.publishToWebsite', 'Publish to Website')}
</span> </span>
</label> </label>
<span <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 ? '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> </span>
</div> </div>
</div> </div>
@@ -1069,7 +1071,7 @@ export default function PageBuilderPage() {
) : ( ) : (
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
)} )}
Save Settings {t('pageBuilder.btnSaveSettings', 'Save Settings')}
</button> </button>
<a <a
href={getPageUrl(page.slug)} href={getPageUrl(page.slug)}
@@ -1079,7 +1081,7 @@ export default function PageBuilderPage() {
title={`Open ${page.title} live preview`} title={`Open ${page.title} live preview`}
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
Preview Live {t('pageBuilder.btnPreviewLive', 'Preview Live')}
</a> </a>
</div> </div>
{page.slug !== 'home' && ( {page.slug !== 'home' && (
@@ -1087,7 +1089,7 @@ export default function PageBuilderPage() {
onClick={handleDeletePage} 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" 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> </button>
)} )}
</div> </div>
@@ -1097,8 +1099,8 @@ export default function PageBuilderPage() {
{/* Section Management Header */} {/* 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 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> <div>
<h2 className="text-lg font-bold">Page Sections</h2> <h2 className="text-lg font-bold">{t('pageBuilder.sectionsTitle', 'Page Sections')}</h2>
<p className="text-xs text-slate-400 mt-1">Manage the content blocks for this page.</p> <p className="text-xs text-slate-400 mt-1">{t('pageBuilder.sectionsSubtitle', 'Manage the content blocks for this page.')}</p>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <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" 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" /> <Layers className="w-4 h-4" />
Reorder Layout {t('pageBuilder.btnReorderLayout', 'Reorder Layout')}
</button> </button>
<button <button
onClick={openAddModal} 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" 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" /> <Plus className="w-4 h-4" />
Add Section {t('pageBuilder.btnAddSection', 'Add Section')}
</button> </button>
</div> </div>
</div> </div>
@@ -1121,7 +1123,7 @@ export default function PageBuilderPage() {
{/* Block Selection Dropdown */} {/* Block Selection Dropdown */}
<div className="bg-night-950/40 border border-white/5 p-4 rounded-2xl space-y-3"> <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"> <label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
Select Block to Edit {t('pageBuilder.selectBlockLabel', 'Select Block to Edit')}
</label> </label>
<select <select
value={editingSection?.id?.toString() || ''} 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" 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) => { {sections.map((section, idx) => {
const title = getAdminDisplayString( const title = getAdminDisplayString(
section.payload?.title || section.payload?.tagline || section.payload?.quote || (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined) section.payload?.title || section.payload?.tagline || section.payload?.quote || (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
) || `Block #${section.id}`; ) || `Block #${section.id}`;
return ( return (
<option key={section.id} value={section.id?.toString()}> <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> </option>
); );
})} })}
@@ -1158,22 +1160,22 @@ export default function PageBuilderPage() {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="text-xl font-bold flex items-center gap-3"> <h3 className="text-xl font-bold flex items-center gap-3">
<Settings2 className="w-5 h-5 text-cyan-glow" /> <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> </h3>
{editingSection.id && ( {editingSection.id && (
<div className="flex items-center gap-2 mt-1 ml-8 flex-wrap"> <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"> <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>
<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"> <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> </span>
<div className="w-px h-3 bg-white/10 mx-1"></div> <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"> <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>
<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"> <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> </span>
</div> </div>
)} )}
@@ -1212,14 +1214,14 @@ export default function PageBuilderPage() {
{!editingSection.id && ( {!editingSection.id && (
<div className="space-y-2 bg-night-950/40 p-4 rounded-2xl border border-white/5"> <div className="space-y-2 bg-night-950/40 p-4 rounded-2xl border border-white/5">
<div className="flex items-center justify-between"> <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 <button
type="button" type="button"
onClick={() => setIsAddBlockModalOpen(true)} onClick={() => setIsAddBlockModalOpen(true)}
className="text-xs font-bold text-cyan-glow hover:underline flex items-center gap-1.5 cursor-pointer" 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" /> <Sparkles className="w-3.5 h-3.5 text-cyan-glow" />
Browse Visual Gallery {t('pageBuilder.browseVisualGallery', 'Browse Visual Gallery')}
</button> </button>
</div> </div>
<select <select
@@ -1630,14 +1632,14 @@ export default function PageBuilderPage() {
type="submit" 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" 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>
<button <button
type="button" type="button"
onClick={() => setEditingSection(null)} onClick={() => setEditingSection(null)}
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all" 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> </button>
</div> </div>
</form> </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]"> <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" /> <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>
)} )}
</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]"> <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"> <h3 className="text-xl font-bold flex items-center gap-2">
<Layers className="w-5 h-5 text-cyan-glow" /> <Layers className="w-5 h-5 text-cyan-glow" />
Reorder Layout {t('pageBuilder.reorderModalTitle', 'Reorder Layout')}
</h3> </h3>
<button <button
onClick={() => setIsReorderModalOpen(false)} onClick={() => setIsReorderModalOpen(false)}
@@ -1673,7 +1675,7 @@ export default function PageBuilderPage() {
{sections.length === 0 ? ( {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"> <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" /> <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> </div>
) : ( ) : (
sections.map((section, idx) => ( sections.map((section, idx) => (
@@ -1712,7 +1714,7 @@ export default function PageBuilderPage() {
</span> </span>
{!section.isActive && ( {!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"> <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> </span>
)} )}
<h3 className="text-xs font-bold text-slate-100 truncate"> <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-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' : '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" />} {section.isActive ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
</button> </button>
@@ -1745,14 +1747,14 @@ export default function PageBuilderPage() {
openEditModal(section); 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" 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" /> <Edit className="w-3.5 h-3.5" />
</button> </button>
<button <button
onClick={() => handleDeleteSection(section.id)} 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" 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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
@@ -1774,7 +1776,7 @@ export default function PageBuilderPage() {
) : ( ) : (
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
)} )}
Save Order {t('pageBuilder.btnSaveOrder', 'Save Order')}
</button> </button>
)} )}
<button <button
@@ -1782,7 +1784,7 @@ export default function PageBuilderPage() {
onClick={() => setIsReorderModalOpen(false)} 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" 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> </button>
</div> </div>
</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]"> <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"> <h3 className="text-xl font-bold flex items-center gap-2">
<Layers className="w-5 h-5 text-cyan-glow" /> <Layers className="w-5 h-5 text-cyan-glow" />
Create New Page {t('pageBuilder.createPageModalTitle', 'Create New Page')}
</h3> </h3>
<button <button
onClick={() => setIsPageModalOpen(false)} onClick={() => setIsPageModalOpen(false)}
@@ -1808,11 +1810,11 @@ export default function PageBuilderPage() {
<form onSubmit={handleCreatePage} className="p-6 space-y-4"> <form onSubmit={handleCreatePage} className="p-6 space-y-4">
<div className="space-y-1"> <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 <input
type="text" type="text"
required required
placeholder="e.g. About Us" placeholder={t('pageBuilder.pageTitlePlaceholder', 'e.g. About Us')}
value={newPageData.title} value={newPageData.title}
onChange={(e) => setNewPageData({ ...newPageData, title: e.target.value })} onChange={(e) => setNewPageData({ ...newPageData, title: e.target.value })}
className="admin-input w-full" className="admin-input w-full"
@@ -1820,16 +1822,16 @@ export default function PageBuilderPage() {
</div> </div>
<div className="space-y-1"> <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 <input
type="text" type="text"
required required
placeholder="e.g. about" placeholder={t('pageBuilder.pageSlugPlaceholder', 'e.g. about')}
value={newPageData.slug} value={newPageData.slug}
onChange={(e) => setNewPageData({ ...newPageData, slug: e.target.value })} onChange={(e) => setNewPageData({ ...newPageData, slug: e.target.value })}
className="admin-input w-full" 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>
<div className="pt-4 border-t border-white/5 flex gap-3"> <div className="pt-4 border-t border-white/5 flex gap-3">
@@ -1838,14 +1840,14 @@ export default function PageBuilderPage() {
disabled={isSavingPage} 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" 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>
<button <button
type="button" type="button"
onClick={() => setIsPageModalOpen(false)} 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" 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> </button>
</div> </div>
</form> </form>