Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
96 lines
2.6 KiB
TypeScript
96 lines
2.6 KiB
TypeScript
import { SEO_ROUTES } from '@/constants/routes'
|
|
import { createClientId } from '@/lib/createClientId'
|
|
|
|
export type TrafficSource = 'instagram' | 'telegram'
|
|
|
|
const VISITOR_KEY = 'ghabilee:visitor-id'
|
|
const ATTRIBUTION_PREFIX = 'ghabilee:attribution:'
|
|
const ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
|
|
|
const SOURCES = new Set<TrafficSource>(['instagram', 'telegram'])
|
|
|
|
interface StoredAttribution {
|
|
source: TrafficSource
|
|
capturedAt: number
|
|
}
|
|
|
|
const isBrowser = () => typeof window !== 'undefined'
|
|
|
|
export const parseTrafficSource = (raw: string | null | undefined): TrafficSource | null => {
|
|
if (!raw) return null
|
|
const value = raw.trim().toLowerCase()
|
|
|
|
return SOURCES.has(value as TrafficSource) ? (value as TrafficSource) : null
|
|
}
|
|
|
|
export const getOrCreateVisitorId = (): string | null => {
|
|
if (!isBrowser()) return null
|
|
|
|
try {
|
|
const existing = window.localStorage.getItem(VISITOR_KEY)
|
|
|
|
if (existing && /^[0-9a-f-]{36}$/i.test(existing)) return existing
|
|
|
|
const created = createClientId()
|
|
|
|
window.localStorage.setItem(VISITOR_KEY, created)
|
|
|
|
return created
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const rememberEventAttribution = (eventId: string, source: TrafficSource): void => {
|
|
if (!isBrowser() || !eventId) return
|
|
|
|
try {
|
|
const key = `${ATTRIBUTION_PREFIX}${eventId}`
|
|
const existingRaw = window.localStorage.getItem(key)
|
|
|
|
if (existingRaw) {
|
|
const existing = JSON.parse(existingRaw) as StoredAttribution
|
|
|
|
if (existing?.source && Date.now() - existing.capturedAt < ATTRIBUTION_TTL_MS) {
|
|
return
|
|
}
|
|
}
|
|
const payload: StoredAttribution = { source, capturedAt: Date.now() }
|
|
|
|
window.localStorage.setItem(key, JSON.stringify(payload))
|
|
} catch {
|
|
// Privacy modes / full storage — attribution is best-effort.
|
|
}
|
|
}
|
|
|
|
export const getEventAttribution = (eventId: string): TrafficSource | null => {
|
|
if (!isBrowser() || !eventId) return null
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(`${ATTRIBUTION_PREFIX}${eventId}`)
|
|
|
|
if (!raw) return null
|
|
const stored = JSON.parse(raw) as StoredAttribution
|
|
|
|
if (!stored?.source || !SOURCES.has(stored.source)) return null
|
|
if (Date.now() - stored.capturedAt > ATTRIBUTION_TTL_MS) return null
|
|
|
|
return stored.source
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const buildEventShareUrl = (slug: string, source?: TrafficSource): string => {
|
|
const path = SEO_ROUTES.EVENT_LANDING(slug)
|
|
|
|
if (!isBrowser()) {
|
|
return source ? `${path}?src=${source}` : path
|
|
}
|
|
const url = new URL(path, window.location.origin)
|
|
|
|
if (source) url.searchParams.set('src', source)
|
|
|
|
return url.toString()
|
|
}
|