Files
carmanagement/storefront/src/components/StorefrontShell.tsx
T
root 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
refactor: rename marketplace to storefront across the entire monorepo
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
2026-06-28 01:14:46 -04:00

365 lines
12 KiB
TypeScript

'use client'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { appPrivacyHref, footerPageHref } from '@/lib/footerContent'
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE, isStorefrontLanguage, type StorefrontLanguage } from '@/lib/i18n'
import { SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
type Theme = 'light' | 'dark'
type Dictionary = {
storefront: string
explore: string
signIn: string
ownerSignIn: string
language: string
theme: string
light: string
dark: string
preferences: string
}
const dictionaries: Record<StorefrontLanguage, Dictionary> = {
en: {
storefront: 'Home',
explore: 'Storefront',
signIn: 'Sign in',
ownerSignIn: 'Create Agency Space',
language: 'Language',
theme: 'Theme',
light: 'Light',
dark: 'Dark',
preferences: 'Storefront preferences',
},
fr: {
storefront: 'Accueil',
explore: 'Storefront',
signIn: 'Connexion',
ownerSignIn: "Créer un espace d'agence",
language: 'Langue',
theme: 'Mode',
light: 'Clair',
dark: 'Sombre',
preferences: 'Préférences storefront',
},
ar: {
storefront: 'الرئيسية',
explore: 'السوق',
signIn: 'تسجيل الدخول',
ownerSignIn: 'إنشاء مساحة الوكالة',
language: 'اللغة',
theme: 'الوضع',
light: 'فاتح',
dark: 'داكن',
preferences: 'تفضيلات السوق',
},
}
const EMPLOYEE_PROFILE_KEY = 'employee_profile'
export const localeOptions: Array<{ value: StorefrontLanguage; label: string; flag: string }> = [
{ value: 'en', label: 'Global (English)', flag: '🇺🇸' },
{ value: 'ar', label: 'North Africa (Arabic)', flag: '🇲🇦' },
{ value: 'fr', label: 'Europe (French)', flag: '🇫🇷' },
]
export function getFooterContent(language: StorefrontLanguage): {
primary: Array<{ label: string; href?: string }>
secondary: Array<{ label: string; href?: string }>
localeLabel: string
rightsLabel: string
} {
switch (language) {
case 'fr':
return {
primary: [
{ label: 'À propos de nous', href: footerPageHref['about-us'] },
{ label: "Conditions d'utilisation", href: footerPageHref['terms-of-service'] },
{ label: 'Sécurité', href: footerPageHref.security },
{ label: 'Conformité', href: footerPageHref.compliance },
{ label: 'Politique de confidentialité', href: appPrivacyHref.fr },
{ label: 'Politique relative aux cookies', href: footerPageHref['cookie-policy'] },
],
secondary: [
{ label: 'Newsletter', href: footerPageHref.newsletter },
{ label: 'Contacter les ventes', href: footerPageHref['contact-sales'] },
{ label: 'Conditions générales', href: footerPageHref['general-conditions'] },
],
localeLabel: 'Europe (French)',
rightsLabel: 'Tous droits réservés.',
}
case 'ar':
return {
primary: [
{ label: 'من نحن', href: footerPageHref['about-us'] },
{ label: 'شروط الاستخدام', href: footerPageHref['terms-of-service'] },
{ label: 'الأمان', href: footerPageHref.security },
{ label: 'الامتثال', href: footerPageHref.compliance },
{ label: 'سياسة الخصوصية', href: appPrivacyHref.ar },
{ label: 'سياسة ملفات تعريف الارتباط', href: footerPageHref['cookie-policy'] },
],
secondary: [
{ label: 'النشرة الإخبارية', href: footerPageHref.newsletter },
{ label: 'تواصل مع المبيعات', href: footerPageHref['contact-sales'] },
{ label: 'الشروط العامة', href: footerPageHref['general-conditions'] },
],
localeLabel: 'North Africa (Arabic)',
rightsLabel: 'جميع الحقوق محفوظة.',
}
case 'en':
default:
return {
primary: [
{ label: 'About Us', href: footerPageHref['about-us'] },
{ label: 'Terms of Service', href: footerPageHref['terms-of-service'] },
{ label: 'Security', href: footerPageHref.security },
{ label: 'Compliance', href: footerPageHref.compliance },
{ label: 'Privacy Policy', href: appPrivacyHref.en },
{ label: 'Cookie Policy', href: footerPageHref['cookie-policy'] },
],
secondary: [
{ label: 'Newsletter', href: footerPageHref.newsletter },
{ label: 'Contact Sales', href: footerPageHref['contact-sales'] },
{ label: 'General Conditions', href: footerPageHref['general-conditions'] },
],
localeLabel: 'Global (English)',
rightsLabel: 'All rights reserved.',
}
}
}
export function hasCachedEmployeeProfile(): boolean {
if (typeof window === 'undefined') return false
try {
return Boolean(window.localStorage.getItem(EMPLOYEE_PROFILE_KEY))
} catch {
return false
}
}
export function clearCachedEmployeeProfile() {
if (typeof window === 'undefined') return
try {
window.localStorage.removeItem(EMPLOYEE_PROFILE_KEY)
} catch {}
}
function detectBrowserLanguage(): string | null {
if (typeof navigator === 'undefined') return null
const langs = Array.from(navigator.languages ?? [navigator.language])
for (const lang of langs) {
const code = lang.split('-')[0].toLowerCase()
if (code === 'fr' || code === 'ar' || code === 'en') return code
}
return null
}
type PreferencesContextValue = {
language: StorefrontLanguage
theme: Theme
dict: Dictionary
companyName: string | null
setLanguage: (language: StorefrontLanguage) => void
setTheme: (theme: Theme) => void
}
const PreferencesContext = createContext<PreferencesContextValue | null>(null)
export function useStorefrontPreferences() {
const context = useContext(PreferencesContext)
if (!context) throw new Error('useStorefrontPreferences must be used within StorefrontShell')
return context
}
export default function StorefrontShell({
children,
initialLanguage = 'en',
initialTheme = 'light',
}: {
children: React.ReactNode
initialLanguage?: StorefrontLanguage
initialTheme?: Theme
}) {
const router = useRouter()
const pathname = usePathname()
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
const [language, setLanguageState] = useState<StorefrontLanguage>(initialLanguage)
const [theme, setThemeState] = useState<Theme>(initialTheme)
const [hydrated, setHydrated] = useState(false)
const previousLanguage = useRef<StorefrontLanguage>(initialLanguage)
const [companyName, setCompanyName] = useState<string | null>(null)
function applyLanguage(nextLanguage: StorefrontLanguage) {
if (nextLanguage === language) return
if (typeof window !== 'undefined') {
document.documentElement.lang = nextLanguage
document.documentElement.dir = nextLanguage === 'ar' ? 'rtl' : 'ltr'
try {
sessionStorage.setItem('storefront-language', nextLanguage)
} catch {}
writeScopedPreference(SHARED_LANGUAGE_KEY, nextLanguage, ['storefront-language'])
}
setLanguageState(nextLanguage)
}
function applyTheme(nextTheme: Theme) {
if (nextTheme === theme) return
if (typeof window !== 'undefined') {
document.documentElement.classList.toggle('dark', nextTheme === 'dark')
document.documentElement.style.colorScheme = nextTheme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = nextTheme
writeScopedPreference(SHARED_THEME_KEY, nextTheme, ['storefront-theme'])
}
setThemeState(nextTheme)
}
async function syncCompanyBrand() {
if (!hasCachedEmployeeProfile()) {
setCompanyName(null)
return
}
try {
const response = await fetch(`${apiUrl}/companies/me/brand`, {
credentials: 'include',
})
if (!response.ok) {
if (response.status === 401) {
clearCachedEmployeeProfile()
}
setCompanyName(null)
return
}
const json = await response.json()
const name = json?.data?.displayName ?? json?.displayName ?? null
setCompanyName(name)
} catch {
setCompanyName(null)
}
}
function readSessionLanguage(): StorefrontLanguage | null {
try {
const val = sessionStorage.getItem('storefront-language')
return isStorefrontLanguage(val) ? val : null
} catch {
return null
}
}
function syncScopedPreferencesForSession() {
const sessionLang = readSessionLanguage()
if (sessionLang) {
if (sessionLang !== language) setLanguageState(sessionLang)
} else {
const scopedLanguage = readCurrentUserScopedPreference(SHARED_LANGUAGE_KEY)
if (isStorefrontLanguage(scopedLanguage) && scopedLanguage !== language) {
setLanguageState(scopedLanguage)
}
}
const scopedTheme = readCurrentUserScopedPreference(SHARED_THEME_KEY)
if ((scopedTheme === 'light' || scopedTheme === 'dark') && scopedTheme !== theme) {
setThemeState(scopedTheme)
} else {
writeScopedPreference(SHARED_THEME_KEY, theme, ['storefront-theme'])
}
}
useEffect(() => {
const sessionLang = readSessionLanguage()
if (sessionLang) {
setLanguageState(sessionLang)
writeScopedPreference(SHARED_LANGUAGE_KEY, sessionLang, ['storefront-language'])
} else {
const storedLanguage = readScopedPreference(SHARED_LANGUAGE_KEY, ['storefront-language'])
if (isStorefrontLanguage(storedLanguage)) {
setLanguageState(storedLanguage)
writeScopedPreference(SHARED_LANGUAGE_KEY, storedLanguage, ['storefront-language'])
} else {
const detected = detectBrowserLanguage()
if (isStorefrontLanguage(detected)) {
setLanguageState(detected)
writeScopedPreference(SHARED_LANGUAGE_KEY, detected, ['storefront-language'])
}
}
}
const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['storefront-theme']) as Theme | null
if (storedTheme === 'light' || storedTheme === 'dark') {
setThemeState(storedTheme)
}
setHydrated(true)
void syncCompanyBrand()
}, [])
useEffect(() => {
function handleWindowFocus() {
syncScopedPreferencesForSession()
void syncCompanyBrand()
}
function handleAuthMessage(event: MessageEvent) {
if (!event.data || typeof event.data !== 'object') return
const message = event.data as { type?: string }
if (message.type === 'rentaldrivego:employee-login' || message.type === 'rentaldrivego:employee-logout') {
syncScopedPreferencesForSession()
void syncCompanyBrand()
}
}
window.addEventListener('focus', handleWindowFocus)
window.addEventListener('message', handleAuthMessage)
document.addEventListener('visibilitychange', handleWindowFocus)
return () => {
window.removeEventListener('focus', handleWindowFocus)
window.removeEventListener('message', handleAuthMessage)
document.removeEventListener('visibilitychange', handleWindowFocus)
}
}, [language, theme])
useEffect(() => {
document.documentElement.lang = language
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
if (!hydrated) return
try { sessionStorage.setItem('storefront-language', language) } catch {}
writeScopedPreference(SHARED_LANGUAGE_KEY, language, ['storefront-language'])
if (previousLanguage.current !== language && pathname !== '/sign-in') {
router.refresh()
}
previousLanguage.current = language
}, [hydrated, language, pathname, router])
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark')
document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light'
document.body.dataset.theme = theme
if (hydrated) {
writeScopedPreference(SHARED_THEME_KEY, theme, ['storefront-theme'])
}
}, [theme, hydrated])
const value = useMemo(
() => ({ language, theme, dict: dictionaries[language], companyName, setLanguage: applyLanguage, setTheme: applyTheme }),
[language, theme, companyName],
)
return <PreferencesContext.Provider value={value}>{children}</PreferencesContext.Provider>
}