diff --git a/apps/admin/src/components/page-builder/FocalPointPicker.tsx b/apps/admin/src/components/page-builder/FocalPointPicker.tsx new file mode 100644 index 0000000..6beee30 --- /dev/null +++ b/apps/admin/src/components/page-builder/FocalPointPicker.tsx @@ -0,0 +1,52 @@ +import React, { useRef } from 'react'; + +interface FocalPointPickerProps { + imageUrl: string; + focalPoint?: { x: string; y: string }; + onChange: (focalPoint: { x: string; y: string }) => void; +} + +export default function FocalPointPicker({ imageUrl, focalPoint, onChange }: FocalPointPickerProps) { + const containerRef = useRef(null); + + const handleClick = (e: React.MouseEvent) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + onChange({ x: `${x.toFixed(2)}%`, y: `${y.toFixed(2)}%` }); + }; + + const currentX = focalPoint?.x || '50%'; + const currentY = focalPoint?.y || '50%'; + + return ( +
+ +
+ Focal point +
+
+
+
+
+ X: {currentX}, Y: {currentY} +
+
+ ); +} diff --git a/apps/admin/src/components/page-builder/HeroForm.tsx b/apps/admin/src/components/page-builder/HeroForm.tsx index 626ec6d..85cc5e9 100644 --- a/apps/admin/src/components/page-builder/HeroForm.tsx +++ b/apps/admin/src/components/page-builder/HeroForm.tsx @@ -1,12 +1,13 @@ import { UploadCloud, Image as ImageIcon } from 'lucide-react'; import { type PageSection } from '@homaroma/types'; +import FocalPointPicker from './FocalPointPicker'; interface HeroFormProps { editingSection: Partial; editingLang: string; getLocalizedValue: (payload: any, field: string) => string; updateLocalizedValue: (field: string, value: string) => void; - updatePayloadValue: (field: string, value: string | number) => void; + updatePayloadValue: (field: string, value: any) => void; setUploadTarget: (target: { type: 'main' | 'slide'; index?: number } | null) => void; setIsMediaModalOpen: (open: boolean) => void; } @@ -47,6 +48,16 @@ export default function HeroForm({
+ {editingSection.payload?.imageUrl && ( +
+ updatePayloadValue('focalPoint', focalPoint)} + /> +
+ )} +
{ if (!text) return null; @@ -41,6 +42,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { src={imageUrl || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/hero.png`} className="w-full h-full object-cover" alt="Hero Background" + style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }} onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} diff --git a/apps/spa/src/components/DynamicHeroV2.tsx b/apps/spa/src/components/DynamicHeroV2.tsx index 9d5e69d..9b98bf8 100644 --- a/apps/spa/src/components/DynamicHeroV2.tsx +++ b/apps/spa/src/components/DynamicHeroV2.tsx @@ -23,6 +23,7 @@ export default function DynamicHeroV2({ payload }: DynamicHeroV2Props) { const imageUrl = typeof payload.imageUrl === 'string' ? payload.imageUrl : '/assets/hero.png'; const align = typeof payload.contentAlignment === 'string' ? payload.contentAlignment : 'left'; const showOrnament = typeof payload.showOrnament === 'boolean' ? payload.showOrnament : true; + const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined; const alignClass = align === 'center' ? 'text-center items-center mx-auto' : align === 'right' ? 'text-right items-end ml-auto' : 'text-left items-start'; @@ -51,6 +52,7 @@ export default function DynamicHeroV2({ payload }: DynamicHeroV2Props) { src={imageUrl} alt={title || 'Hero'} className="w-full h-full object-cover" + style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }} />
diff --git a/apps/spa/src/components/Navbar.tsx b/apps/spa/src/components/Navbar.tsx index e683840..494f340 100644 --- a/apps/spa/src/components/Navbar.tsx +++ b/apps/spa/src/components/Navbar.tsx @@ -15,9 +15,10 @@ import { useMenuStore } from '../store/menuStore' interface NavbarProps { categories?: Category[] topOffsetClassName?: string + isTransparent?: boolean } -export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) { +export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = false }: NavbarProps) { const isDesktop = useMediaQuery('(min-width: 768px)') const [searchOpen, setSearchOpen] = useState(false) @@ -147,12 +148,18 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) { const logoUrl = settings?.logoUrl || '' + const shouldBeTransparent = isTransparent && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen + return ( <>
) : ( -
- {typeof { (e.target as HTMLImageElement).style.display = 'none' }} - /> -
-
-
-
+
+
+
{/* Breadcrumb */} -
- Home +
+ Home / - Shop + Shop / - {typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'} + {typeof category?.name === 'string' ? category.name : (category?.name as any)?.[localStorage.getItem('app_lang') || 'en'] || (category?.name as any)?.en || '...'}
-

+

{loading ? ( -
+
) : ( typeof category?.name === 'string' ? category.name : (category?.name as any)?.[i18n.language] || (category?.name as any)?.en || '' )}

{!loading && category?._count && ( -

+

{category._count.products} products available

)}
+ +
+ {typeof { (e.target as HTMLImageElement).style.display = 'none' }} + /> +
)} - {/* Products Grid */}
{/* Other Category Pills */} diff --git a/apps/spa/src/pages/HomePage.tsx b/apps/spa/src/pages/HomePage.tsx index cda7e5e..e0c4cc9 100644 --- a/apps/spa/src/pages/HomePage.tsx +++ b/apps/spa/src/pages/HomePage.tsx @@ -45,7 +45,7 @@ export default function HomePage() { return (
- + {loading ? (
diff --git a/apps/website/src/app/[lang]/category/[id]/page.tsx b/apps/website/src/app/[lang]/category/[id]/page.tsx index 1e3d842..d3a2a3d 100644 --- a/apps/website/src/app/[lang]/category/[id]/page.tsx +++ b/apps/website/src/app/[lang]/category/[id]/page.tsx @@ -84,25 +84,28 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s {heroSections.length > 0 ? ( ) : ( -
- {categoryName} -
-
-
- Home - / - Shop - / - {categoryName} +
+
+
+
+ Home + / + Shop + / + {categoryName} +
+

{categoryName}

+ {category._count &&

{category._count.products} products available

} +
+
+ {categoryName}
-

{categoryName}

- {category._count &&

{category._count.products} products available

}
)} diff --git a/apps/website/src/app/[lang]/shop/page.tsx b/apps/website/src/app/[lang]/shop/page.tsx index b630aed..87b4b3c 100644 --- a/apps/website/src/app/[lang]/shop/page.tsx +++ b/apps/website/src/app/[lang]/shop/page.tsx @@ -50,7 +50,7 @@ export default async function ShopPage({ searchParams, params }: { searchParams: {heroSections.length > 0 ? ( ) : ( -
+

All Products

The Collection

diff --git a/apps/website/src/components/Navbar.tsx b/apps/website/src/components/Navbar.tsx index 681f880..e49b1e4 100644 --- a/apps/website/src/components/Navbar.tsx +++ b/apps/website/src/components/Navbar.tsx @@ -150,15 +150,20 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0] const brandName = settings?.brandName || '' + const isHome = getPathWithoutLocale(pathname) === '/' + const isTransparent = isHome && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen + return ( <>