import { getCatalogueKeywords, getMachineById as lookupMachineById, getMachineBySlug as lookupMachineBySlug, getPartById as lookupPartById, getRelatedMachines as lookupRelatedMachines, machineRegistry, } from './registry'; import { MACHINE_CATEGORIES, MACHINE_CATEGORY_LABELS, MACHINE_DIFFICULTIES, MACHINE_DIFFICULTY_LABELS, } from './schema'; import type { CatalogueCategoryFilter, CatalogueDifficultyFilter, MachineCategory, MachineDefinition, MachineDifficulty, MachineId, MachinePart, MachineSortKey, } from './schema'; export interface CatalogueFilterState { search: string; searchTerm: string; query: string; category: CatalogueCategoryFilter; difficulty: CatalogueDifficultyFilter; sort: MachineSortKey; sortBy: MachineSortKey; favouriteIds: readonly string[]; favourites: readonly string[]; showFavouritesOnly: boolean; } export const DEFAULT_CATALOGUE_FILTERS: CatalogueFilterState = { search: '', searchTerm: '', query: '', category: 'all', difficulty: 'all', sort: 'name', sortBy: 'name', favouriteIds: [], favourites: [], showFavouritesOnly: false, }; export interface CatalogueFacet { id: string; label: string; count: number; } export interface CatalogueFacets { categories: readonly CatalogueFacet[]; difficulties: readonly CatalogueFacet[]; tags: readonly CatalogueFacet[]; } export const getMachineById = lookupMachineById; export const getMachineBySlug = lookupMachineBySlug; export const getPartById = lookupPartById; export const getRelatedMachines = lookupRelatedMachines; const normaliseFilters = (filters: Partial = {}): CatalogueFilterState => { const search = filters.search ?? filters.searchTerm ?? filters.query ?? ''; const sort = filters.sort ?? filters.sortBy ?? 'name'; const favouriteIds = filters.favouriteIds ?? filters.favourites ?? []; return { ...DEFAULT_CATALOGUE_FILTERS, ...filters, search, searchTerm: search, query: search, sort, sortBy: sort, favouriteIds, favourites: favouriteIds, showFavouritesOnly: filters.showFavouritesOnly ?? false, }; }; export const normaliseSearchTerm = (term: string): string => term.trim().toLocaleLowerCase(); export const machineMatchesSearch = (machine: MachineDefinition, search: string): boolean => { const term = normaliseSearchTerm(search); if (!term) { return true; } return getCatalogueKeywords(machine).join(' ').toLocaleLowerCase().includes(term); }; export const sortMachines = ( source: readonly MachineDefinition[], sortKey: MachineSortKey = 'name', ): readonly MachineDefinition[] => { const machines = [...source]; switch (sortKey) { case 'newest': return machines.sort((a, b) => { const dateDifference = new Date(b.dateAdded).getTime() - new Date(a.dateAdded).getTime(); return dateDifference === 0 ? a.releaseOrder - b.releaseOrder : dateDifference; }); case 'complexity': return machines.sort((a, b) => { const complexityDifference = b.complexity - a.complexity; return complexityDifference === 0 ? a.title.localeCompare(b.title) : complexityDifference; }); case 'name': default: return machines.sort((a, b) => a.title.localeCompare(b.title)); } }; export function filterMachines(filters?: Partial): readonly MachineDefinition[]; export function filterMachines( source: readonly MachineDefinition[], filters?: Partial, ): readonly MachineDefinition[]; export function filterMachines( first: readonly MachineDefinition[] | Partial = machineRegistry, second: Partial = {}, ): readonly MachineDefinition[] { const source = Array.isArray(first) ? first : machineRegistry; const filters = normaliseFilters(Array.isArray(first) ? second : first); const favouriteSet = new Set(filters.favouriteIds); const filtered = source.filter((machine) => { if (filters.category !== 'all' && machine.category !== filters.category) { return false; } if (filters.difficulty !== 'all' && machine.difficulty !== filters.difficulty) { return false; } if (filters.showFavouritesOnly && !favouriteSet.has(machine.id)) { return false; } return machineMatchesSearch(machine, filters.search); }); return sortMachines(filtered, filters.sort); } export const searchMachines = (search: string, source: readonly MachineDefinition[] = machineRegistry) => filterMachines(source, { search }); export const getMachinesByIds = (ids: readonly MachineId[]): readonly MachineDefinition[] => ids.map((id) => lookupMachineById(id)).filter((machine): machine is MachineDefinition => Boolean(machine)); export const getMachineOptions = (source: readonly MachineDefinition[] = machineRegistry) => source.map((machine) => ({ value: machine.id, label: machine.title, category: machine.category, difficulty: machine.difficulty, })); export const selectMachineOptions = getMachineOptions; export const getCatalogueFacets = (source: readonly MachineDefinition[] = machineRegistry): CatalogueFacets => { const categories = MACHINE_CATEGORIES.map((category) => ({ id: category, label: MACHINE_CATEGORY_LABELS[category], count: source.filter((machine) => machine.category === category).length, })); const difficulties = MACHINE_DIFFICULTIES.map((difficulty) => ({ id: difficulty, label: MACHINE_DIFFICULTY_LABELS[difficulty], count: source.filter((machine) => machine.difficulty === difficulty).length, })); const tagCounts = source.reduce>((accumulator, machine) => { machine.tags.forEach((tag) => { accumulator.set(tag, (accumulator.get(tag) ?? 0) + 1); }); return accumulator; }, new Map()); const tags = [...tagCounts.entries()] .map(([id, count]) => ({ id, label: id .split(' ') .map((word) => `${word.charAt(0).toLocaleUpperCase()}${word.slice(1)}`) .join(' '), count, })) .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)); return { categories, difficulties, tags, }; }; export const getMachinesByCategory = ( category: MachineCategory | 'all', source: readonly MachineDefinition[] = machineRegistry, ): readonly MachineDefinition[] => category === 'all' ? source : source.filter((machine) => machine.category === category); export const getMachinesByDifficulty = ( difficulty: MachineDifficulty | 'all', source: readonly MachineDefinition[] = machineRegistry, ): readonly MachineDefinition[] => difficulty === 'all' ? source : source.filter((machine) => machine.difficulty === difficulty); export const getPartsForMachine = (machineId: string | null | undefined): readonly MachinePart[] => lookupMachineById(machineId)?.parts ?? []; export const getPartCount = (machineId: string | null | undefined): number => getPartsForMachine(machineId).length; export const getMachineLearningPath = (machineId: string | null | undefined): readonly MachineDefinition[] => { const machine = lookupMachineById(machineId); if (!machine) { return []; } const related = lookupRelatedMachines(machine, 3); return [machine, ...related]; };