fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { calculateBookingTotalDays, normalizeOptionalText, resolveReturnLocation } from './BookingForm'
|
||||
|
||||
describe('BookingForm helpers', () => {
|
||||
it('calculates booking nights from date-only inputs', () => {
|
||||
expect(calculateBookingTotalDays('2026-06-09', '2026-06-10')).toBe(1)
|
||||
expect(calculateBookingTotalDays('2026-06-09', '2026-06-16')).toBe(7)
|
||||
})
|
||||
|
||||
it('never returns a negative total for reversed or incomplete dates', () => {
|
||||
expect(calculateBookingTotalDays('2026-06-16', '2026-06-09')).toBe(0)
|
||||
expect(calculateBookingTotalDays('', '2026-06-09')).toBe(0)
|
||||
expect(calculateBookingTotalDays('2026-06-09', '')).toBe(0)
|
||||
})
|
||||
|
||||
it('uses pickup location for same-location returns', () => {
|
||||
expect(resolveReturnLocation({ dropoffMode: 'same', pickupLocation: 'Marrakesh Airport', returnLocation: 'Casablanca' })).toBe('Marrakesh Airport')
|
||||
})
|
||||
|
||||
it('uses the explicit return location for different dropoffs and preserves an unset optional value', () => {
|
||||
expect(resolveReturnLocation({ dropoffMode: 'different', pickupLocation: 'Marrakesh', returnLocation: 'Casablanca' })).toBe('Casablanca')
|
||||
expect(resolveReturnLocation({ dropoffMode: 'different', pickupLocation: 'Marrakesh', returnLocation: '' })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('normalizes optional free-text fields without leaking empty strings into payloads', () => {
|
||||
expect(normalizeOptionalText(' flight lands at 9 ')).toBe('flight lands at 9')
|
||||
expect(normalizeOptionalText(' ')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -131,6 +131,30 @@ const copy = {
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
export function calculateBookingTotalDays(startDate: string, endDate: string): number {
|
||||
if (!startDate || !endDate) return 0
|
||||
return Math.max(0, Math.ceil((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86_400_000))
|
||||
}
|
||||
|
||||
export function resolveReturnLocation({
|
||||
dropoffMode,
|
||||
returnLocation,
|
||||
pickupLocation,
|
||||
}: {
|
||||
dropoffMode: 'same' | 'different'
|
||||
returnLocation: string
|
||||
pickupLocation: string
|
||||
}): string | undefined {
|
||||
if (dropoffMode === 'different') return returnLocation || undefined
|
||||
return pickupLocation || undefined
|
||||
}
|
||||
|
||||
export function normalizeOptionalText(value: string): string | undefined {
|
||||
const trimmed = value.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
|
||||
export default function BookingForm({
|
||||
vehicleId,
|
||||
companySlug,
|
||||
@@ -169,10 +193,7 @@ export default function BookingForm({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const totalDays =
|
||||
startDate && endDate
|
||||
? Math.max(0, Math.ceil((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86_400_000))
|
||||
: 0
|
||||
const totalDays = calculateBookingTotalDays(startDate, endDate)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
@@ -228,12 +249,12 @@ export default function BookingForm({
|
||||
licenseExpiry: new Date(licenseExpiry).toISOString(),
|
||||
licenseCountry: licenseCountry.trim(),
|
||||
licenseCategory: licenseCategory.trim(),
|
||||
internationalLicenseNumber: internationalLicenseNumber.trim() || undefined,
|
||||
internationalLicenseNumber: normalizeOptionalText(internationalLicenseNumber),
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
pickupLocation: pickupLocation || undefined,
|
||||
returnLocation: dropoffMode === 'different' ? (returnLocation || undefined) : (pickupLocation || undefined),
|
||||
notes: notes.trim() || undefined,
|
||||
returnLocation: resolveReturnLocation({ dropoffMode, returnLocation, pickupLocation }),
|
||||
notes: normalizeOptionalText(notes),
|
||||
language,
|
||||
})
|
||||
setSuccess(true)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { isValidElement } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const preferenceState = vi.hoisted(() => ({ language: 'en' as 'en' | 'fr' | 'ar' }))
|
||||
|
||||
vi.mock('@/components/MarketplaceShell', () => ({
|
||||
useMarketplacePreferences: () => ({ language: preferenceState.language, theme: 'light' }),
|
||||
}))
|
||||
|
||||
import FooterContentPage from './FooterContentPage'
|
||||
|
||||
function collectText(node: unknown): string[] {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') return []
|
||||
if (typeof node === 'string' || typeof node === 'number') return [String(node)]
|
||||
if (Array.isArray(node)) return node.flatMap(collectText)
|
||||
if (isValidElement<{ children?: React.ReactNode }>(node)) return collectText(node.props.children)
|
||||
return []
|
||||
}
|
||||
|
||||
function collectElements(node: unknown): React.ReactElement[] {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') return []
|
||||
if (Array.isArray(node)) return node.flatMap(collectElements)
|
||||
if (!isValidElement<{ children?: React.ReactNode }>(node)) return []
|
||||
return [node, ...collectElements(node.props.children)]
|
||||
}
|
||||
|
||||
describe('FooterContentPage', () => {
|
||||
beforeEach(() => {
|
||||
preferenceState.language = 'en'
|
||||
})
|
||||
|
||||
it('uses the current marketplace language when no forced language is provided', () => {
|
||||
preferenceState.language = 'fr'
|
||||
const page = FooterContentPage({ slug: 'contact-sales' })
|
||||
const text = collectText(page).join(' ')
|
||||
|
||||
expect(text).toContain('Informations du pied de page')
|
||||
expect(text).toContain('Besoin de plus de détails')
|
||||
})
|
||||
|
||||
it('lets static app policy pages force their own locale', () => {
|
||||
preferenceState.language = 'en'
|
||||
const page = FooterContentPage({ slug: 'terms-of-service', forcedLanguage: 'fr' })
|
||||
const text = collectText(page).join(' ')
|
||||
|
||||
expect(text).toContain('Informations du pied de page')
|
||||
expect(text).not.toContain('Footer information')
|
||||
})
|
||||
|
||||
it('marks Arabic content as right-aligned', () => {
|
||||
const page = FooterContentPage({ slug: 'privacy-policy', forcedLanguage: 'ar' })
|
||||
const elements = collectElements(page)
|
||||
|
||||
expect(elements.some((element) => String(element.props.className ?? '').includes('text-right'))).toBe(true)
|
||||
})
|
||||
|
||||
it('turns plain URLs embedded in policy paragraphs into safe anchors', () => {
|
||||
const page = FooterContentPage({ slug: 'privacy-policy', forcedLanguage: 'en' })
|
||||
const links = collectElements(page).filter((element) => element.type === 'a')
|
||||
|
||||
expect(links.some((link) => String(link.props.href).startsWith('https://'))).toBe(true)
|
||||
expect(links.every((link) => link.props.href === collectText(link).join(''))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildDashboardSignInHref,
|
||||
companyInitial,
|
||||
localeMenuPositionClass,
|
||||
ownerWorkspaceHref,
|
||||
} from './MarketplaceHeader'
|
||||
|
||||
describe('MarketplaceHeader helpers', () => {
|
||||
it('builds dashboard sign-in links with language and theme query parameters', () => {
|
||||
expect(buildDashboardSignInHref('https://app.example.com/dashboard', 'fr', 'dark')).toBe(
|
||||
'https://app.example.com/dashboard/sign-in?lang=fr&theme=dark'
|
||||
)
|
||||
expect(buildDashboardSignInHref('/dashboard', 'ar', 'light')).toBe('/dashboard/sign-in?lang=ar&theme=light')
|
||||
})
|
||||
|
||||
it('keeps the locale dropdown anchored to the readable side for RTL and LTR languages', () => {
|
||||
expect(localeMenuPositionClass('ar')).toBe('right-0 sm:right-0')
|
||||
expect(localeMenuPositionClass('en')).toBe('left-0 sm:left-auto sm:right-0')
|
||||
expect(localeMenuPositionClass('fr')).toBe('left-0 sm:left-auto sm:right-0')
|
||||
})
|
||||
|
||||
it('routes existing companies to their workspace and new owners to sign-up', () => {
|
||||
expect(ownerWorkspaceHref('https://dashboard.example.com', 'Atlas Cars')).toBe('https://dashboard.example.com')
|
||||
expect(ownerWorkspaceHref('https://dashboard.example.com', null)).toBe('https://dashboard.example.com/sign-up')
|
||||
})
|
||||
|
||||
it('normalizes company initials for the owner workspace pill', () => {
|
||||
expect(companyInitial('atlas cars')).toBe('A')
|
||||
expect(companyInitial(' زاكورة كار ')).toBe('ز')
|
||||
})
|
||||
})
|
||||
@@ -5,8 +5,8 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
type Language = 'en' | 'fr' | 'ar'
|
||||
export type Theme = 'light' | 'dark'
|
||||
export type Language = 'en' | 'fr' | 'ar'
|
||||
|
||||
type Dictionary = {
|
||||
marketplace: string
|
||||
@@ -24,6 +24,26 @@ type LanguageMeta = {
|
||||
shortLabel: string
|
||||
}
|
||||
|
||||
|
||||
export function buildDashboardSignInHref(dashboardUrl: string, language: Language, theme: Theme): string {
|
||||
const signInParams = new URLSearchParams()
|
||||
signInParams.set('lang', language)
|
||||
signInParams.set('theme', theme)
|
||||
return `${dashboardUrl}/sign-in?${signInParams.toString()}`
|
||||
}
|
||||
|
||||
export function localeMenuPositionClass(language: Language): string {
|
||||
return language === 'ar' ? 'right-0 sm:right-0' : 'left-0 sm:left-auto sm:right-0'
|
||||
}
|
||||
|
||||
export function ownerWorkspaceHref(dashboardUrl: string, companyName: string | null): string {
|
||||
return companyName ? dashboardUrl : `${dashboardUrl}/sign-up`
|
||||
}
|
||||
|
||||
export function companyInitial(companyName: string): string {
|
||||
return companyName.trim().charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
export default function MarketplaceHeader({
|
||||
dict,
|
||||
theme,
|
||||
@@ -52,10 +72,7 @@ export default function MarketplaceHeader({
|
||||
{ value: 'dark' as const, label: dict.dark },
|
||||
]
|
||||
const currentTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[0]
|
||||
const localeMenuPositionClass =
|
||||
currentLanguage.value === 'ar'
|
||||
? 'right-0 sm:right-0'
|
||||
: 'left-0 sm:left-auto sm:right-0'
|
||||
const menuPositionClass = localeMenuPositionClass(currentLanguage.value)
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: MouseEvent) {
|
||||
@@ -81,10 +98,7 @@ export default function MarketplaceHeader({
|
||||
setThemeMenuOpen((open) => !open)
|
||||
}
|
||||
|
||||
const signInParams = new URLSearchParams()
|
||||
signInParams.set('lang', currentLanguage.value)
|
||||
signInParams.set('theme', theme)
|
||||
const signInHref = `${dashboardUrl}/sign-in?${signInParams.toString()}`
|
||||
const signInHref = buildDashboardSignInHref(dashboardUrl, currentLanguage.value, theme)
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-blue-900 dark:bg-blue-950/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
|
||||
@@ -129,13 +143,13 @@ export default function MarketplaceHeader({
|
||||
className="ml-1 flex items-center gap-1.5 rounded-full bg-orange-600 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400 sm:ml-2 sm:gap-2 sm:px-4 sm:py-2 sm:text-sm"
|
||||
>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-[10px] font-black dark:bg-blue-950/20">
|
||||
{companyName.charAt(0).toUpperCase()}
|
||||
{companyInitial(companyName)}
|
||||
</span>
|
||||
{companyName}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={`${dashboardUrl}/sign-up`}
|
||||
href={ownerWorkspaceHref(dashboardUrl, companyName)}
|
||||
className="ml-1 rounded-full bg-orange-600 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400 sm:ml-2 sm:px-5 sm:py-2 sm:text-sm"
|
||||
>
|
||||
{dict.ownerSignIn}
|
||||
@@ -156,7 +170,7 @@ export default function MarketplaceHeader({
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${localeMenuOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{localeMenuOpen ? (
|
||||
<div className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] 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)] ${localeMenuPositionClass}`}>
|
||||
<div className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] 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)] ${menuPositionClass}`}>
|
||||
{localeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
@@ -189,7 +203,7 @@ export default function MarketplaceHeader({
|
||||
<ChevronDown className={`h-3.5 w-3.5 text-stone-500 transition-transform dark:text-stone-400 ${themeMenuOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{themeMenuOpen ? (
|
||||
<div className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] 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)] ${localeMenuPositionClass}`}>
|
||||
<div className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] 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)] ${menuPositionClass}`}>
|
||||
{themeOptions
|
||||
.filter((option) => option.value !== theme)
|
||||
.map((option) => (
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getFooterContent, localeOptions } from './MarketplaceShell'
|
||||
|
||||
describe('MarketplaceShell footer content registry', () => {
|
||||
it('exposes exactly the supported marketplace locales in selector order', () => {
|
||||
expect(localeOptions.map((option) => option.value)).toEqual(['en', 'ar', 'fr'])
|
||||
expect(localeOptions.every((option) => option.label.length > 0 && option.flag.length > 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns English footer links with app privacy pointing at the English mobile policy', () => {
|
||||
const content = getFooterContent('en')
|
||||
|
||||
expect(content.localeLabel).toBe('Global (English)')
|
||||
expect(content.rightsLabel).toBe('All rights reserved.')
|
||||
expect(content.primary).toContainEqual({ label: 'Privacy Policy', href: '/app-privacy-en' })
|
||||
expect(content.secondary).toContainEqual({ label: 'Contact Sales', href: '/footer/contact-sales' })
|
||||
})
|
||||
|
||||
it('returns French labels while preserving canonical footer slugs', () => {
|
||||
const content = getFooterContent('fr')
|
||||
|
||||
expect(content.localeLabel).toBe('Europe (French)')
|
||||
expect(content.primary).toContainEqual({ label: "Conditions d'utilisation", href: '/footer/terms-of-service' })
|
||||
expect(content.secondary).toContainEqual({ label: 'Conditions générales', href: '/footer/general-conditions' })
|
||||
})
|
||||
|
||||
it('returns Arabic labels and app privacy href without falling back to English copy', () => {
|
||||
const content = getFooterContent('ar')
|
||||
|
||||
expect(content.localeLabel).toBe('North Africa (Arabic)')
|
||||
expect(content.rightsLabel).toBe('جميع الحقوق محفوظة.')
|
||||
expect(content.primary).toContainEqual({ label: 'سياسة الخصوصية', href: '/app-privacy-ar' })
|
||||
expect(content.primary.map((item) => item.label)).not.toContain('Privacy Policy')
|
||||
})
|
||||
})
|
||||
@@ -201,20 +201,9 @@ export default function MarketplaceShell({
|
||||
}
|
||||
|
||||
async function syncCompanyBrand() {
|
||||
const token = document.cookie
|
||||
.split(';')
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith('employee_token='))
|
||||
?.split('=')[1]
|
||||
|
||||
if (!token) {
|
||||
setCompanyName(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/companies/me/brand`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { buildFrameUrl, FRAME_HEIGHT, getDefaultFramePath } from './WorkspaceFrame'
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function stubBrowser({ cookie = '', origin = 'https://market.example' }: { cookie?: string; origin?: string } = {}) {
|
||||
const parsed = new URL(origin)
|
||||
vi.stubGlobal('window', {
|
||||
location: { origin, hostname: parsed.hostname, protocol: parsed.protocol, replace: vi.fn() },
|
||||
})
|
||||
vi.stubGlobal('document', { cookie })
|
||||
}
|
||||
|
||||
describe('WorkspaceFrame helpers', () => {
|
||||
it('uses the dashboard sign-in path during server rendering', () => {
|
||||
vi.stubGlobal('window', undefined)
|
||||
|
||||
expect(getDefaultFramePath('sign-in')).toBe('/dashboard/sign-in')
|
||||
expect(buildFrameUrl('https://dashboard.example/dashboard', '/dashboard/sign-in')).toBe('/dashboard/sign-in')
|
||||
})
|
||||
|
||||
it('keeps users with an employee token on the embedded sign-in path until the dashboard confirms login', () => {
|
||||
stubBrowser({ cookie: 'other=1; employee_session=token_123' })
|
||||
|
||||
expect(getDefaultFramePath('sign-in')).toBe('/dashboard/sign-in')
|
||||
})
|
||||
|
||||
it('keeps unauthenticated visitors on the embedded sign-in path', () => {
|
||||
stubBrowser({ cookie: 'marketplace-language=fr' })
|
||||
|
||||
expect(getDefaultFramePath('sign-in')).toBe('/dashboard/sign-in')
|
||||
})
|
||||
|
||||
it('rewrites a configured app base to the requested embedded path and query', () => {
|
||||
stubBrowser({ origin: 'https://market.example' })
|
||||
|
||||
expect(buildFrameUrl('https://dashboard.example/dashboard', '/dashboard/reset-password?token=abc')).toBe(
|
||||
'https://market.example/dashboard/reset-password?token=abc',
|
||||
)
|
||||
})
|
||||
|
||||
it('documents the iframe height contract used by embedded dashboard surfaces', () => {
|
||||
expect(FRAME_HEIGHT).toBe('calc(100vh - 56px)')
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
||||
import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
||||
import { useMarketplacePreferences } from './MarketplaceShell'
|
||||
|
||||
type FrameId = 'sign-in'
|
||||
export type FrameId = 'sign-in'
|
||||
|
||||
const frameConfig: Record<FrameId, { label: string; appUrl: string; path: string }> = {
|
||||
'sign-in': {
|
||||
@@ -14,24 +14,15 @@ const frameConfig: Record<FrameId, { label: string; appUrl: string; path: string
|
||||
},
|
||||
}
|
||||
|
||||
function getDefaultFramePath(target: FrameId) {
|
||||
export function getDefaultFramePath(target: FrameId) {
|
||||
if (typeof window === 'undefined') {
|
||||
return frameConfig[target].path
|
||||
}
|
||||
|
||||
const employeeToken = document.cookie
|
||||
.split(';')
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith('employee_token='))
|
||||
|
||||
if (target === 'sign-in' && employeeToken) {
|
||||
return '/dashboard'
|
||||
}
|
||||
|
||||
return frameConfig[target].path
|
||||
}
|
||||
|
||||
function buildFrameUrl(appUrl: string, path: string) {
|
||||
export function buildFrameUrl(appUrl: string, path: string) {
|
||||
if (typeof window === 'undefined') return path
|
||||
|
||||
const resolvedAppUrl = resolveBrowserAppUrl(appUrl)
|
||||
@@ -43,7 +34,7 @@ function buildFrameUrl(appUrl: string, path: string) {
|
||||
return appBase.toString()
|
||||
}
|
||||
|
||||
const FRAME_HEIGHT = 'calc(100vh - 56px)'
|
||||
export const FRAME_HEIGHT = 'calc(100vh - 56px)'
|
||||
|
||||
export default function WorkspaceFrame({ target }: { target: FrameId }) {
|
||||
const { language, theme } = useMarketplacePreferences()
|
||||
@@ -60,19 +51,6 @@ export default function WorkspaceFrame({ target }: { target: FrameId }) {
|
||||
setFramePath(getDefaultFramePath(target))
|
||||
}, [target])
|
||||
|
||||
useEffect(() => {
|
||||
if (target !== 'sign-in') return
|
||||
|
||||
const employeeToken = document.cookie
|
||||
.split(';')
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith('employee_token='))
|
||||
|
||||
if (employeeToken) {
|
||||
window.location.replace('/dashboard')
|
||||
}
|
||||
}, [target])
|
||||
|
||||
useEffect(() => {
|
||||
function handleFrameMessage(event: MessageEvent) {
|
||||
if (!event.data || typeof event.data !== 'object') return
|
||||
|
||||
Reference in New Issue
Block a user