HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2import { useCallback, useEffect, useState } from 'react';34/**5 * Compare tray — the list of entity slugs the visitor is collecting for `/compare?ids=`.6 * Source of truth: `localStorage['aia-compare']` (mirrored across tabs through the `storage` event and a same-tab7 * custom event). The tray is type-homogeneous: the first item fixes the entity type; adding another type replaces8 * the tray (the API compares 2–6 entities of one type).9 */10export const COMPARE_KEY = 'aia-compare';11export const COMPARE_MAX = 6;12export const COMPARE_MIN = 2;13const EVENT = 'aia-compare-change';1415export type TrayItem = { slug: string; name: string; entity_type: string; organization?: string | null };1617function read(): TrayItem[] {18 if (typeof window === 'undefined') return [];19 try {20 const raw = window.localStorage.getItem(COMPARE_KEY);21 if (!raw) return [];22 const arr = JSON.parse(raw) as unknown;23 if (!Array.isArray(arr)) return [];24 return arr.filter((x): x is TrayItem => !!x && typeof x === 'object' && typeof (x as TrayItem).slug === 'string' && typeof (x as TrayItem).entity_type === 'string').slice(0, COMPARE_MAX);25 } catch {26 return [];27 }28}2930function write(items: TrayItem[]) {31 if (typeof window === 'undefined') return;32 try {33 window.localStorage.setItem(COMPARE_KEY, JSON.stringify(items.slice(0, COMPARE_MAX)));34 } catch {35 /* storage full / disabled */36 }37 window.dispatchEvent(new CustomEvent(EVENT));38}3940/** Normalises the API's type aliases so companies/labs/orgs compare together (the API accepts them under `company`). */41export function trayType(entityType: string): string {42 if (['company', 'organization', 'lab', 'university'].includes(entityType)) return 'company';43 if (['framework', 'library', 'runtime'].includes(entityType)) return 'framework';44 if (entityType === 'quantization') return 'model';45 return entityType;46}4748export function readTray(): TrayItem[] {49 return read();50}51export function setTray(items: TrayItem[]) {52 write(items);53}54export function clearTray() {55 write([]);56}5758/** Add one item. Returns the new tray; a type change replaces the tray. */59export function addToTray(item: TrayItem): TrayItem[] {60 const cur = read();61 const t = trayType(item.entity_type);62 const same = cur.filter((c) => trayType(c.entity_type) === t);63 if (same.some((c) => c.slug === item.slug)) return cur;64 const next = same.length === cur.length ? [...cur, item] : [item];65 const capped = next.slice(0, COMPARE_MAX);66 write(capped);67 return capped;68}69export function removeFromTray(slug: string): TrayItem[] {70 const next = read().filter((c) => c.slug !== slug);71 write(next);72 return next;73}74export function toggleTray(item: TrayItem): TrayItem[] {75 return read().some((c) => c.slug === item.slug) ? removeFromTray(item.slug) : addToTray(item);76}7778/** React binding: returns the tray and mutators, subscribed to storage changes. `ready` is false during SSR/hydration. */79export function useCompareTray() {80 const [items, setItems] = useState<TrayItem[]>([]);81 const [ready, setReady] = useState(false);82 useEffect(() => {83 const sync = () => setItems(read());84 sync();85 setReady(true);86 window.addEventListener(EVENT, sync);87 window.addEventListener('storage', sync);88 return () => {89 window.removeEventListener(EVENT, sync);90 window.removeEventListener('storage', sync);91 };92 }, []);93 const add = useCallback((item: TrayItem) => setItems(addToTray(item)), []);94 const remove = useCallback((slug: string) => setItems(removeFromTray(slug)), []);95 const toggle = useCallback((item: TrayItem) => setItems(toggleTray(item)), []);96 const clear = useCallback(() => {97 clearTray();98 setItems([]);99 }, []);100 const replace = useCallback((next: TrayItem[]) => {101 write(next);102 setItems(next.slice(0, COMPARE_MAX));103 }, []);104 const has = useCallback((slug: string) => items.some((c) => c.slug === slug), [items]);105 const type = items[0] ? trayType(items[0].entity_type) : null;106 return { items, ready, add, remove, toggle, clear, replace, has, type, full: items.length >= COMPARE_MAX, canCompare: items.length >= COMPARE_MIN };107}108109export function compareHref(items: { slug: string }[]): string {110 return items.length ? `/compare?ids=${items.map((i) => encodeURIComponent(i.slug)).join(',')}` : '/compare';111}112