x
This commit is contained in:
1 parent
2729f88f45
commit
406cad801b
9 files changed
+1103
-6
No files matched your search
@@ -8,6 +8,7 @@ import ProductCmsPage from './pages/ProductCmsPage'
|
||||
import CategoriesPage from './pages/CategoriesPage'
|
||||
import CustomersPage from './pages/CustomersPage'
|
||||
import PageBuilderPage from './pages/PageBuilderPage'
|
||||
import CmsMockupPage from './pages/CmsMockupPage'
|
||||
import RawMaterialsPage from './pages/RawMaterialsPage'
|
||||
import LoginPage from './pages/LoginPage'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
@@ -64,6 +65,7 @@ function App() {
|
||||
<Route path="/categories" element={<CategoriesPage />} />
|
||||
<Route path="/customers" element={<CustomersPage />} />
|
||||
<Route path="/page-builder" element={<PageBuilderPage />} />
|
||||
<Route path="/cms-mockup" element={<CmsMockupPage />} />
|
||||
<Route path="/orders" element={<OrdersPage />} />
|
||||
<Route path="/orders/:id" element={<OrderDetailPage />} />
|
||||
<Route path="/media" element={<MediaLibraryPage />} />
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FileText,
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
|
||||
const MAIN_MENU = [
|
||||
@@ -39,6 +40,7 @@ const MAIN_MENU = [
|
||||
{ icon: Ticket, label: 'Promotions', to: '/promotions' },
|
||||
{ icon: Ticket, label: 'Coupons', to: '/coupons' },
|
||||
{ icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' },
|
||||
{ icon: Sparkles, label: 'CMS Block Showcase', to: '/cms-mockup' },
|
||||
{ icon: ImageIcon, label: 'Media Library', to: '/media' },
|
||||
{ icon: BookOpen, label: 'Scent Guide', to: '/articles' },
|
||||
{ icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' },
|
||||
|
||||
@@ -78,6 +78,24 @@ export default function EditorialTileForm({
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Image Ratio (สัดส่วนรูปภาพ)</label>
|
||||
<select
|
||||
value={editingSection.payload?.imageAspectRatio || 'square'}
|
||||
onChange={(e) =>
|
||||
setEditingSection({
|
||||
...editingSection,
|
||||
payload: { ...(editingSection.payload || {}), imageAspectRatio: e.target.value }
|
||||
})
|
||||
}
|
||||
className="admin-input w-full bg-slate-900 border-white/10"
|
||||
>
|
||||
<option value="square">Square 1:1 (จัตุรัส)</option>
|
||||
<option value="portrait">Portrait 3:4 (แนวสูง 3:4)</option>
|
||||
<option value="tall">Tall Fashion 2:3 (แนวสูง 2:3)</option>
|
||||
<option value="auto">Natural / Original (รูปต้นฉบับ)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
|
||||
<input
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,9 @@ import {
|
||||
X,
|
||||
Settings2,
|
||||
CheckCircle2,
|
||||
ExternalLink
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check
|
||||
} from 'lucide-react';
|
||||
import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types';
|
||||
import { getAdminDisplayString } from '../utils/localization';
|
||||
@@ -84,6 +86,7 @@ export default function PageBuilderPage() {
|
||||
const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide' | 'gallery'; index?: number } | null>(null);
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||
const [isSavingPageSettings, setIsSavingPageSettings] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isReorderModalOpen, setIsReorderModalOpen] = useState(false);
|
||||
|
||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
|
||||
@@ -943,6 +946,35 @@ export default function PageBuilderPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||
Page URL
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={getPageUrl(page.slug)}
|
||||
className="bg-slate-900/50 border border-white/10 text-slate-300 rounded-xl px-4 py-2 text-xs focus:outline-none transition-colors flex-1 select-all font-mono"
|
||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(getPageUrl(page.slug));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
}}
|
||||
className={`p-2.5 rounded-xl border border-white/10 text-white transition-all flex items-center justify-center ${
|
||||
isCopied ? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400' : 'bg-white/5 hover:bg-white/10'
|
||||
}`}
|
||||
title="Copy page URL"
|
||||
>
|
||||
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="inline-flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
|
||||
@@ -155,11 +155,11 @@ const PAGE_BLOCK_METADATA: Record<string, BlockMetadata> = {
|
||||
EDITORIAL_TILE: {
|
||||
type: "EDITORIAL_TILE",
|
||||
requiredFields: ["title", "description", "imageUrl", "imagePosition"],
|
||||
optionalFields: ["buttonText", "buttonLink", "backgroundColor", "textColor"],
|
||||
renderedFields: ["title", "description", "imageUrl", "imagePosition", "buttonText", "buttonLink", "backgroundColor", "textColor"],
|
||||
optionalFields: ["buttonText", "buttonLink", "backgroundColor", "textColor", "imageAspectRatio"],
|
||||
renderedFields: ["title", "description", "imageUrl", "imagePosition", "imageAspectRatio", "buttonText", "buttonLink", "backgroundColor", "textColor"],
|
||||
storedOnlyFields: [],
|
||||
discoveryToolsNeeded: [],
|
||||
warnings: ["imagePosition must be left or right."],
|
||||
warnings: ["imagePosition must be left or right.", "imageAspectRatio can be square, portrait, tall, or auto."],
|
||||
minimalPayload: {
|
||||
title: localizedObjectExample,
|
||||
description: localizedObjectExample,
|
||||
@@ -970,6 +970,7 @@ const SECTION_TOOL_CONFIGS = [
|
||||
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
||||
imageUrl: { type: "string", description: "Plain string main image URL" },
|
||||
imagePosition: { type: "string", description: "Plain string image position ('left' or 'right')" },
|
||||
imageAspectRatio: { type: "string", description: "Plain string image aspect ratio ('square', 'portrait', 'tall', or 'auto')" },
|
||||
title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` },
|
||||
description: { ...localizedStringSchema, description: `Localized description, for example ${localizedStringExample}` },
|
||||
buttonText: { ...localizedStringSchema, description: `Localized button text, for example ${localizedStringExample}` },
|
||||
|
||||
@@ -13,12 +13,27 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
const {
|
||||
imageUrl,
|
||||
imagePosition = 'left',
|
||||
imageAspectRatio = 'square',
|
||||
buttonLink
|
||||
} = payload;
|
||||
const title = getLocalizedField(payload.title, lang);
|
||||
const description = getLocalizedField(payload.description, lang);
|
||||
const buttonText = getLocalizedField(payload.buttonText, lang);
|
||||
|
||||
const getAspectClass = (ratio?: string) => {
|
||||
switch (ratio) {
|
||||
case 'portrait':
|
||||
return 'aspect-[3/4]';
|
||||
case 'tall':
|
||||
return 'aspect-[2/3]';
|
||||
case 'auto':
|
||||
return 'aspect-auto';
|
||||
case 'square':
|
||||
default:
|
||||
return 'aspect-square';
|
||||
}
|
||||
};
|
||||
|
||||
const renderTitle = (text: string) => {
|
||||
if (!text) return null;
|
||||
return text.split('[').map((part, i) => {
|
||||
@@ -43,7 +58,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
|
||||
>
|
||||
<div className="ui-section-inner grid grid-cols-1 items-center gap-12 md:grid-cols-2 md:gap-20">
|
||||
<div className={`group relative aspect-square w-full overflow-hidden rounded-3xl shadow-xl ${
|
||||
<div className={`group relative ${getAspectClass(imageAspectRatio)} w-full overflow-hidden rounded-3xl shadow-xl ${
|
||||
imagePosition === 'left' ? 'md:order-1' : 'md:order-2'
|
||||
}`}>
|
||||
<img
|
||||
|
||||
@@ -893,14 +893,29 @@ function EditorialTileSection({ payload, lang }: { payload: Record<string, unkno
|
||||
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : ''
|
||||
const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : ''
|
||||
const imagePosition = payload.imagePosition === 'right' ? 'right' : 'left'
|
||||
const imageAspectRatio = typeof payload.imageAspectRatio === 'string' ? payload.imageAspectRatio : 'square'
|
||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
||||
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
||||
|
||||
const getAspectClass = (ratio: string) => {
|
||||
switch (ratio) {
|
||||
case 'portrait':
|
||||
return 'aspect-[3/4]'
|
||||
case 'tall':
|
||||
return 'aspect-[2/3]'
|
||||
case 'auto':
|
||||
return 'aspect-auto'
|
||||
case 'square':
|
||||
default:
|
||||
return 'aspect-square'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden bg-surface-page ui-section-band" style={{ backgroundColor, color: textColor }}>
|
||||
<div className="mx-auto grid max-w-7xl gap-12 md:grid-cols-2 md:items-center">
|
||||
{imageUrl && (
|
||||
<div className={`group relative aspect-square overflow-hidden rounded-3xl shadow-card ${imagePosition === 'left' ? 'md:order-1' : 'md:order-2'}`}>
|
||||
<div className={`group relative ${getAspectClass(imageAspectRatio)} overflow-hidden rounded-3xl shadow-card ${imagePosition === 'left' ? 'md:order-1' : 'md:order-2'}`}>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title || 'Editorial'}
|
||||
|
||||
Reference in new issue
Block a user