feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
+29
View File
@@ -0,0 +1,29 @@
export function sendBeacon(endpoint: string, payload: unknown): boolean {
if (typeof window === 'undefined') return false;
const body = JSON.stringify(payload);
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
try {
const blob = new Blob([body], { type: 'application/json' });
if (navigator.sendBeacon(endpoint, blob)) return true;
} catch {
// fall through to fetch
}
}
if (typeof fetch === 'function') {
fetch(endpoint, {
method: 'POST',
body,
headers: { 'Content-Type': 'application/json' },
keepalive: true,
credentials: 'same-origin'
}).catch(() => {
// analytics must never throw
});
return true;
}
return false;
}
+127
View File
@@ -0,0 +1,127 @@
import { CLIENT_EVENTS, type ClientEventType } from '../events/taxonomy';
import { getSessionId, isDoNotTrack } from './session';
import { sendBeacon } from './beacon';
type TrackProps = Record<string, string | number | boolean | null | undefined>;
type QueuedEvent = {
eventType: ClientEventType;
occurredAt: string;
orgId?: string;
courseId?: string;
path?: string;
referrerHost?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
locale?: string;
props?: TrackProps;
};
type AnalyticsConfig = {
endpoint: string;
orgId?: string;
batchSize?: number;
flushIntervalMs?: number;
};
const DEFAULT_BATCH = 10;
const DEFAULT_INTERVAL = 2000;
let config: AnalyticsConfig | null = null;
let queue: QueuedEvent[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
let unloadBound = false;
function pickReferrerHost(): string | undefined {
if (typeof document === 'undefined' || !document.referrer) return undefined;
try {
return new URL(document.referrer).host || undefined;
} catch {
return undefined;
}
}
function pickUtm(): Pick<QueuedEvent, 'utmSource' | 'utmMedium' | 'utmCampaign'> {
if (typeof window === 'undefined') return {};
const params = new URL(window.location.href).searchParams;
return {
utmSource: params.get('utm_source') ?? undefined,
utmMedium: params.get('utm_medium') ?? undefined,
utmCampaign: params.get('utm_campaign') ?? undefined
};
}
function flush() {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (!config || queue.length === 0) return;
const sessionId = getSessionId();
if (!sessionId) {
// Session cookie not yet set by server hook; drop silently.
queue = [];
return;
}
const batch = queue;
queue = [];
sendBeacon(config.endpoint, { sessionId, events: batch });
}
function scheduleFlush() {
if (!config || timer) return;
timer = setTimeout(flush, config.flushIntervalMs ?? DEFAULT_INTERVAL);
}
function bindUnload() {
if (unloadBound || typeof window === 'undefined') return;
unloadBound = true;
window.addEventListener('pagehide', flush);
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
}
export function init(opts: AnalyticsConfig): void {
if (isDoNotTrack()) return;
config = { batchSize: DEFAULT_BATCH, flushIntervalMs: DEFAULT_INTERVAL, ...opts };
bindUnload();
}
export function track(eventType: ClientEventType, fields: Partial<QueuedEvent> = {}): void {
if (!config || isDoNotTrack()) return;
const event: QueuedEvent = {
eventType,
occurredAt: new Date().toISOString(),
orgId: fields.orgId ?? config.orgId,
courseId: fields.courseId,
path: fields.path ?? (typeof window !== 'undefined' ? window.location.pathname : undefined),
referrerHost: fields.referrerHost ?? pickReferrerHost(),
locale: fields.locale ?? (typeof navigator !== 'undefined' ? navigator.language : undefined),
props: fields.props,
...pickUtm()
};
queue.push(event);
if (queue.length >= (config.batchSize ?? DEFAULT_BATCH)) {
flush();
} else {
scheduleFlush();
}
}
export function pageView(fields: Partial<QueuedEvent> & { kind?: 'landing' | 'course' } = {}): void {
const eventType: ClientEventType =
fields.kind === 'course' ? CLIENT_EVENTS.COURSE_PAGE_VIEW : CLIENT_EVENTS.LANDING_VIEW;
track(eventType, fields);
}
export function flushNow(): void {
flush();
}
export const analytics = { init, track, pageView, flushNow };
+19
View File
@@ -0,0 +1,19 @@
const SESSION_COOKIE = 'cio_aid';
function readCookie(name: string): string | null {
if (typeof document === 'undefined') return null;
const match = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`));
return match ? decodeURIComponent(match.split('=').slice(1).join('=')) : null;
}
export function getSessionId(): string | null {
return readCookie(SESSION_COOKIE);
}
export function isDoNotTrack(): boolean {
if (typeof navigator === 'undefined') return false;
const dnt =
(navigator as Navigator & { msDoNotTrack?: string }).doNotTrack ??
(navigator as Navigator & { msDoNotTrack?: string }).msDoNotTrack;
return dnt === '1' || dnt === 'yes';
}