fix notification and add billing page and contract
This commit is contained in:
@@ -75,7 +75,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
"(function(){try{var theme=localStorage.getItem('public-site-theme');if(theme!=='light'&&theme!=='dark'){theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}document.documentElement.classList.remove('light','dark');document.documentElement.classList.add(theme);document.documentElement.style.colorScheme=theme}catch(e){}})();",
|
||||
"(function(){try{var m=document.cookie.match(/(?:^|; )rentaldrivego-theme=([^;]+)/);var theme=m?decodeURIComponent(m[1]):(localStorage.getItem('rentaldrivego-theme')||localStorage.getItem('public-site-theme'));if(theme!=='light'&&theme!=='medium'&&theme!=='dark'){theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}var rootTheme=theme==='light'?'light':'dark';document.documentElement.classList.remove('light','dark');document.documentElement.classList.add(rootTheme);document.documentElement.style.colorScheme=rootTheme;document.body&&document.body.setAttribute('data-theme',theme)}catch(e){}})();",
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PUBLIC_SITE_LANGUAGE_COOKIE, type PublicSiteLanguage } from '@/lib/i18n'
|
||||
import { SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, writeScopedPreference } from '@/lib/preferences'
|
||||
|
||||
const labels: Record<PublicSiteLanguage, string> = {
|
||||
en: 'Language',
|
||||
@@ -23,11 +24,12 @@ export default function PublicLanguageSwitcher({
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language
|
||||
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
|
||||
window.localStorage.setItem('public-site-language', language)
|
||||
writeScopedPreference(SHARED_LANGUAGE_KEY, language, ['public-site-language'])
|
||||
}, [language])
|
||||
|
||||
function handleChange(nextLanguage: PublicSiteLanguage) {
|
||||
setLanguage(nextLanguage)
|
||||
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
|
||||
document.cookie = `${PUBLIC_SITE_LANGUAGE_COOKIE}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { PublicSiteLanguage } from '@/lib/i18n'
|
||||
import { SHARED_THEME_KEY, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
type Theme = 'light' | 'medium' | 'dark'
|
||||
|
||||
const labels: Record<PublicSiteLanguage, { theme: string; light: string; dark: string }> = {
|
||||
en: { theme: 'Theme', light: 'Light', dark: 'Dark' },
|
||||
fr: { theme: 'Mode', light: 'Clair', dark: 'Sombre' },
|
||||
ar: { theme: 'الوضع', light: 'فاتح', dark: 'داكن' },
|
||||
const labels: Record<PublicSiteLanguage, { theme: string; light: string; medium: string; dark: string }> = {
|
||||
en: { theme: 'Theme', light: 'Light', medium: 'Medium', dark: 'Dark' },
|
||||
fr: { theme: 'Mode', light: 'Clair', medium: 'Moyen', dark: 'Sombre' },
|
||||
ar: { theme: 'الوضع', light: 'فاتح', medium: 'متوسط', dark: 'داكن' },
|
||||
}
|
||||
|
||||
export default function PublicThemeSwitcher({
|
||||
@@ -19,8 +20,8 @@ export default function PublicThemeSwitcher({
|
||||
const [theme, setTheme] = useState<Theme>('light')
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = window.localStorage.getItem('public-site-theme')
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') {
|
||||
const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['public-site-theme'])
|
||||
if (storedTheme === 'light' || storedTheme === 'medium' || storedTheme === 'dark') {
|
||||
setTheme(storedTheme)
|
||||
return
|
||||
}
|
||||
@@ -32,10 +33,10 @@ export default function PublicThemeSwitcher({
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
document.documentElement.style.colorScheme = theme
|
||||
document.documentElement.classList.add(theme === 'light' ? 'light' : 'dark')
|
||||
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark'
|
||||
document.body.dataset.theme = theme
|
||||
window.localStorage.setItem('public-site-theme', theme)
|
||||
writeScopedPreference(SHARED_THEME_KEY, theme, ['public-site-theme'])
|
||||
}, [theme])
|
||||
|
||||
const copy = labels[currentLanguage]
|
||||
@@ -45,7 +46,7 @@ export default function PublicThemeSwitcher({
|
||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||||
{copy.theme}
|
||||
</span>
|
||||
{(['light', 'dark'] as Theme[]).map((value) => {
|
||||
{(['light', 'medium', 'dark'] as Theme[]).map((value) => {
|
||||
const active = value === theme
|
||||
return (
|
||||
<button
|
||||
@@ -58,7 +59,7 @@ export default function PublicThemeSwitcher({
|
||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
||||
}`}
|
||||
>
|
||||
{value === 'light' ? copy.light : copy.dark}
|
||||
{value === 'light' ? copy.light : value === 'medium' ? copy.medium : copy.dark}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { cookies } from 'next/headers'
|
||||
import { PUBLIC_SITE_LANGUAGE_COOKIE, isPublicSiteLanguage, type PublicSiteLanguage } from './i18n'
|
||||
import { SHARED_LANGUAGE_COOKIE } from './preferences'
|
||||
|
||||
export function getPublicSiteLanguage(): PublicSiteLanguage {
|
||||
const cookieValue = cookies().get(PUBLIC_SITE_LANGUAGE_COOKIE)?.value
|
||||
const cookieStore = cookies()
|
||||
const cookieValue =
|
||||
cookieStore.get(SHARED_LANGUAGE_COOKIE)?.value ??
|
||||
cookieStore.get(PUBLIC_SITE_LANGUAGE_COOKIE)?.value
|
||||
return isPublicSiteLanguage(cookieValue) ? cookieValue : 'en'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
export const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language'
|
||||
export const SHARED_LANGUAGE_KEY = 'rentaldrivego-language'
|
||||
export const SHARED_THEME_KEY = 'rentaldrivego-theme'
|
||||
|
||||
function readEmployeeToken() {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const localToken = window.localStorage.getItem('employee_token')
|
||||
if (localToken) return localToken
|
||||
|
||||
return document.cookie
|
||||
.split(';')
|
||||
.map((chunk) => chunk.trim())
|
||||
.find((chunk) => chunk.startsWith('employee_token='))
|
||||
?.split('=')[1] ?? null
|
||||
}
|
||||
|
||||
function decodeEmployeeId(token: string | null) {
|
||||
if (!token) return null
|
||||
|
||||
try {
|
||||
const encoded = token.split('.')[1] ?? ''
|
||||
const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
|
||||
const payload = JSON.parse(atob(padded)) as { sub?: string }
|
||||
return typeof payload.sub === 'string' && payload.sub ? payload.sub : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getScopedPreferenceKey(baseKey: string) {
|
||||
const employeeId = decodeEmployeeId(readEmployeeToken())
|
||||
return employeeId ? `${baseKey}:${employeeId}` : baseKey
|
||||
}
|
||||
|
||||
export function getScopedPreferenceCookieName(baseKey: string) {
|
||||
const employeeId = decodeEmployeeId(readEmployeeToken())
|
||||
return employeeId ? `${baseKey}--${employeeId}` : baseKey
|
||||
}
|
||||
|
||||
function readCookie(name: string) {
|
||||
if (typeof document === 'undefined') return null
|
||||
|
||||
return document.cookie
|
||||
.split(';')
|
||||
.map((chunk) => chunk.trim())
|
||||
.find((chunk) => chunk.startsWith(`${name}=`))
|
||||
?.slice(name.length + 1) ?? null
|
||||
}
|
||||
|
||||
function writeCookie(name: string, value: string) {
|
||||
if (typeof document === 'undefined') return
|
||||
document.cookie = `${name}=${value}; path=/; max-age=31536000; samesite=lax`
|
||||
}
|
||||
|
||||
export function readCurrentUserScopedPreference(baseKey: string) {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const scopedCookie = readCookie(getScopedPreferenceCookieName(baseKey))
|
||||
if (scopedCookie) return scopedCookie
|
||||
|
||||
return window.localStorage.getItem(getScopedPreferenceKey(baseKey))
|
||||
}
|
||||
|
||||
export function readScopedPreference(baseKey: string, legacyKeys: string[] = []) {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const scopedCookie = readCookie(getScopedPreferenceCookieName(baseKey))
|
||||
if (scopedCookie) return scopedCookie
|
||||
|
||||
const sharedCookie = readCookie(baseKey)
|
||||
if (sharedCookie) return sharedCookie
|
||||
|
||||
const scopedKey = getScopedPreferenceKey(baseKey)
|
||||
const candidates = [scopedKey, baseKey, ...legacyKeys]
|
||||
|
||||
for (const key of candidates) {
|
||||
const value = window.localStorage.getItem(key)
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function writeScopedPreference(baseKey: string, value: string, legacyKeys: string[] = []) {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const scopedKey = getScopedPreferenceKey(baseKey)
|
||||
const scopedCookie = getScopedPreferenceCookieName(baseKey)
|
||||
|
||||
writeCookie(baseKey, value)
|
||||
if (scopedCookie !== baseKey) {
|
||||
writeCookie(scopedCookie, value)
|
||||
}
|
||||
|
||||
window.localStorage.setItem(scopedKey, value)
|
||||
window.localStorage.setItem(baseKey, value)
|
||||
|
||||
for (const key of legacyKeys) {
|
||||
window.localStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user