8aab968e09
Build & Deploy / Build & Push Docker Image (push) Successful in 1m2s
Test / Type Check (all packages) (push) Failing after 28s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Storefront Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
Build & Deploy / Deploy to VPS (push) Successful in 3s
Comprehensive rename of all marketplace references to storefront: - API module: apps/api/src/modules/marketplace/ → storefront/ - Components: MarketplaceHeader → StorefrontHeader, MarketplaceShell → StorefrontShell, MarketplaceFooter → StorefrontFooter - Types: marketplace-homepage.ts → storefront-homepage.ts - Test files: employee-marketplace-* → employee-storefront-* - All source code identifiers, imports, route paths, and strings - Documentation (docs/), CI config (.gitlab-ci.yml), scripts - Dashboard, admin, storefront workspace references - Prisma field names preserved (isListedOnMarketplace, marketplaceRating, marketplaceFunnelEvent) as they map to database schema Validation: - API type-check: 0 errors - Storefront type-check: 0 errors - Dashboard type-check: 0 errors - Full monorepo type-check: only pre-existing admin TS18046
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { ChevronDown } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
type LocaleOption = {
|
|
value: 'en' | 'fr' | 'ar'
|
|
label: string
|
|
flag: string
|
|
}
|
|
|
|
type FooterItem = {
|
|
label: string
|
|
href?: string
|
|
}
|
|
|
|
export default function StorefrontFooter({
|
|
primaryItems,
|
|
secondaryItems,
|
|
localeLabel,
|
|
rightsLabel,
|
|
localeOptions,
|
|
currentLocale,
|
|
onSelectLanguage,
|
|
}: {
|
|
primaryItems: FooterItem[]
|
|
secondaryItems: FooterItem[]
|
|
localeLabel: string
|
|
rightsLabel: string
|
|
localeOptions: LocaleOption[]
|
|
currentLocale: LocaleOption
|
|
onSelectLanguage: (language: LocaleOption['value']) => void
|
|
}) {
|
|
const localeMenuRef = useRef<HTMLDivElement | null>(null)
|
|
const [localeMenuOpen, setLocaleMenuOpen] = useState(false)
|
|
|
|
useEffect(() => {
|
|
function handlePointerDown(event: MouseEvent) {
|
|
if (!localeMenuRef.current?.contains(event.target as Node)) {
|
|
setLocaleMenuOpen(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handlePointerDown)
|
|
return () => document.removeEventListener('mousedown', handlePointerDown)
|
|
}, [])
|
|
|
|
return (
|
|
<footer className="border-t border-stone-200/80 bg-white/72 px-4 py-8 text-stone-600 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-blue-950/72 dark:text-stone-300">
|
|
<div className="shell flex flex-col items-center gap-5 text-center">
|
|
<nav className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
|
{primaryItems.map((item, index) => (
|
|
<div key={item.label} className="flex items-center">
|
|
<FooterNavItem item={item} />
|
|
{index < primaryItems.length - 1 ? (
|
|
<span className="px-2 text-stone-400 dark:text-stone-600" aria-hidden="true">|</span>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
|
{secondaryItems.map((item) => (
|
|
<div key={item.label} className="flex items-center">
|
|
<FooterNavItem item={item} />
|
|
<span className="px-2 text-stone-400 dark:text-stone-600" aria-hidden="true">|</span>
|
|
</div>
|
|
))}
|
|
<div ref={localeMenuRef} className="relative px-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setLocaleMenuOpen((open) => !open)}
|
|
className="inline-flex items-center gap-2 text-sky-600 transition hover:text-sky-700 dark:text-sky-400 dark:hover:text-sky-300"
|
|
aria-expanded={localeMenuOpen}
|
|
aria-haspopup="menu"
|
|
>
|
|
<span aria-hidden="true" className="text-base leading-none">{currentLocale.flag}</span>
|
|
<span>{localeLabel}</span>
|
|
<ChevronDown className={`h-4 w-4 transition-transform ${localeMenuOpen ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
{localeMenuOpen ? (
|
|
<div className="absolute left-1/2 top-full z-20 mt-3 w-56 -translate-x-1/2 overflow-hidden rounded-2xl border border-stone-200 bg-white/95 text-left shadow-[0_20px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:border-blue-900 dark:bg-blue-950/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
|
{localeOptions.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
onClick={() => {
|
|
onSelectLanguage(option.value)
|
|
setLocaleMenuOpen(false)
|
|
}}
|
|
className="flex w-full items-center gap-3 px-4 py-3 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-200 dark:hover:bg-blue-900/40 dark:hover:text-white"
|
|
>
|
|
<span aria-hidden="true" className="text-base leading-none">{option.flag}</span>
|
|
<span>{option.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-stone-500 dark:text-stone-400">
|
|
© {new Date().getFullYear()} RentalDriveGo. {rightsLabel}
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
)
|
|
}
|
|
|
|
function FooterNavItem({ item }: { item: FooterItem }) {
|
|
const className = 'px-3 text-stone-600 transition hover:text-blue-900 dark:text-stone-300 dark:hover:text-white'
|
|
|
|
if (item.href) {
|
|
return (
|
|
<Link href={item.href} className={className}>
|
|
{item.label}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
return <span className={className}>{item.label}</span>
|
|
}
|