x
This commit is contained in:
1 parent
b7004b63a3
commit
fc059ea652
8 files changed
+525
-101
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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": "メディアファイルのアップロードに失敗しました。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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": "ไม่สามารถอัปโหลดไฟล์สื่อได้"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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": "上传媒体文件失败。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user