aidxnCC/lib/services/device.service.ts

454 lines
No EOL
14 KiB
TypeScript

import type { DeviceSpec, DeviceType, DeviceWithMetrics } from '@/lib/types'
import type { SortOrder } from '@/lib/types/service'
import { devices as deviceData } from '@/lib/devices/data'
/**
* Statistics and aggregated metrics for the device portfolio.
*
* @remarks
* Provides a comprehensive snapshot of device collection metrics including
* type distribution, manufacturer breakdown, age statistics, and device lifecycle data.
*
* @example
* ```ts
* const stats = DeviceService.getDeviceStats()
* console.log(`Total devices: ${stats.total}`)
* console.log(`Mobile devices: ${stats.mobile}`)
* console.log(`Average age: ${stats.averageAge.toFixed(1)} years`)
* ```
*
* @category Services
* @public
*/
export interface DevicePortfolioStats {
/** Total number of devices in the portfolio */
total: number
/** Number of mobile devices */
mobile: number
/** Number of digital audio players (DAPs) */
dap: number
/** Device count grouped by manufacturer name */
byManufacturer: Record<string, number>
/** Average age of all devices in years */
averageAge: number
/** Most recently released device */
newestDevice: DeviceWithMetrics
/** Oldest device by release year */
oldestDevice: DeviceWithMetrics
}
/**
* Service for managing device data and computing device metrics.
*
* @remarks
* Provides a centralized API for device portfolio management including:
* - Device enrichment with computed metrics (age, category labels)
* - Filtering by type, manufacturer, status, and release year
* - Sorting with type-safe key selection
* - Portfolio statistics and analytics
* - Related device discovery
*
* All methods are static and operate on the device data store without side effects.
*
* @example
* ```ts
* import { DeviceService } from '@/lib/services'
*
* // Get all enriched devices
* const devices = DeviceService.getAllDevicesEnriched()
*
* // Filter mobile devices only
* const mobile = DeviceService.getMobileDevices()
*
* // Get statistics
* const stats = DeviceService.getDeviceStats()
* console.log(`You have ${stats.total} devices`)
*
* // Find related devices
* const device = DeviceService.getDeviceBySlug('bonito')
* const related = DeviceService.getRelatedDevices(device!)
* ```
*
* @category Services
* @public
*/
export class DeviceService {
/**
* Computes the age of a device in years since release.
*
* @param device - Device specification
* @returns {number} Years since release, or 0 if no release year
* @internal
*/
private static computeAgeInYears(device: DeviceSpec): number {
if (!device.releaseYear) return 0
return new Date().getFullYear() - device.releaseYear
}
/**
* Checks if a device was released in the current year.
*
* @param device - Device specification
* @returns {boolean} True if released this year
* @internal
*/
private static isCurrentYear(device: DeviceSpec): boolean {
if (!device.releaseYear) return false
return device.releaseYear === new Date().getFullYear()
}
/**
* Determines a human-readable category label based on device type and status.
*
* @param device - Device specification
* @returns {string} Label like 'Daily Driver', 'Beta Testing', 'Digital Audio Player'
* @internal
*/
private static getCategoryLabel(device: DeviceSpec): string {
if (device.type === 'mobile') {
if (device.status?.toLowerCase().includes('daily')) return 'Daily Driver'
if (device.status?.toLowerCase().includes('beta')) return 'Beta Testing'
if (device.status?.toLowerCase().includes('experiment')) return 'Experimental'
return 'Mobile Device'
}
if (device.type === 'dap') {
return 'Digital Audio Player'
}
return 'Device'
}
/**
* Enriches a device specification with computed metrics.
*
* @param device - The device specification to enrich
* @returns {DeviceWithMetrics} Device with added properties:
* - `ageInYears: number` - Years since release
* - `isCurrentYear: boolean` - Released in current year
* - `categoryLabel: string` - Human-readable category ('Daily Driver', 'Digital Audio Player', etc.)
*
* @example
* ```ts
* const rawDevice = { slug: 'bonito', name: 'Pixel 3a XL', releaseYear: 2019, type: 'mobile', ... }
* const enriched = DeviceService.enrichDevice(rawDevice)
* console.log(enriched.ageInYears) // 6 (as of 2025)
* console.log(enriched.categoryLabel) // 'Mobile Device'
* ```
*/
static enrichDevice(device: DeviceSpec): DeviceWithMetrics {
return {
...device,
ageInYears: this.computeAgeInYears(device),
isCurrentYear: this.isCurrentYear(device),
categoryLabel: this.getCategoryLabel(device)
}
}
/**
* Retrieves all devices from the data store.
*
* @returns {DeviceSpec[]} Array of all device specifications
*
* @example
* ```ts
* const devices = DeviceService.getAllDevices()
* console.log(`Found ${devices.length} devices`)
* ```
*/
static getAllDevices(): DeviceSpec[] {
return Object.values(deviceData)
}
/**
* Retrieves all devices with enriched metrics.
*
* @returns {DeviceWithMetrics[]} Array of all devices with computed properties
*
* @example
* ```ts
* const enriched = DeviceService.getAllDevicesEnriched()
* enriched.forEach(device => {
* console.log(`${device.name} is ${device.ageInYears} years old`)
* })
* ```
*/
static getAllDevicesEnriched(): DeviceWithMetrics[] {
return this.getAllDevices().map(device => this.enrichDevice(device))
}
/**
* Retrieves a single device by its slug identifier.
*
* @param slug - The device slug (e.g., 'bonito', 'cheetah')
* @returns {DeviceWithMetrics | null} Enriched device or null if not found
*
* @example
* ```ts
* const device = DeviceService.getDeviceBySlug('bonito')
* if (device) {
* console.log(`Found: ${device.name}`)
* }
* ```
*/
static getDeviceBySlug(slug: string): DeviceWithMetrics | null {
const device = deviceData[slug]
return device ? this.enrichDevice(device) : null
}
/**
* Filters devices based on multiple criteria.
*
* @param filters - Filter criteria
* @param filters.type - Device type to filter by
* @param filters.manufacturer - Manufacturer name to filter by
* @param filters.status - Status string to filter by
* @param filters.releaseYear - Release year to filter by
* @returns {DeviceWithMetrics[]} Array of enriched devices matching all specified filters
*
* @example
* ```ts
* const mobileDevices = DeviceService.filterDevices({ type: 'mobile' })
* const googleDevices = DeviceService.filterDevices({ manufacturer: 'Google' })
* ```
*/
static filterDevices(filters: {
type?: DeviceType
manufacturer?: string
status?: string
releaseYear?: number
}): DeviceWithMetrics[] {
let filtered = this.getAllDevices()
if (filters.type) {
filtered = filtered.filter(d => d.type === filters.type)
}
if (filters.manufacturer) {
filtered = filtered.filter(d => d.manufacturer === filters.manufacturer)
}
if (filters.status) {
filtered = filtered.filter(d => d.status === filters.status)
}
if (filters.releaseYear) {
filtered = filtered.filter(d => d.releaseYear === filters.releaseYear)
}
return filtered.map(d => this.enrichDevice(d))
}
/**
* Retrieves devices filtered by device type.
*
* @param type - Device type ('mobile' or 'dap')
* @returns {DeviceWithMetrics[]} Array of enriched devices matching the type
*
* @remarks
* This is a convenience wrapper around `filterDevices()` for type-based filtering.
*
* @example
* ```ts
* const mobileDevices = DeviceService.getDevicesByType('mobile')
* const daps = DeviceService.getDevicesByType('dap')
* ```
*/
static getDevicesByType(type: DeviceType): DeviceWithMetrics[] {
return this.filterDevices({ type })
}
/**
* Retrieves all mobile devices from the portfolio.
*
* @returns {DeviceWithMetrics[]} Array of enriched mobile devices
*
* @remarks
* Convenience method equivalent to `getDevicesByType('mobile')`.
*
* @example
* ```ts
* const mobile = DeviceService.getMobileDevices()
* console.log(`You have ${mobile.length} mobile devices`)
* ```
*/
static getMobileDevices(): DeviceWithMetrics[] {
return this.getDevicesByType('mobile')
}
/**
* Retrieves all digital audio players (DAPs) from the portfolio.
*
* @returns {DeviceWithMetrics[]} Array of enriched DAP devices
*
* @remarks
* Convenience method equivalent to `getDevicesByType('dap')`.
*
* @example
* ```ts
* const daps = DeviceService.getDAPDevices()
* console.log(`You have ${daps.length} DAPs`)
* ```
*/
static getDAPDevices(): DeviceWithMetrics[] {
return this.getDevicesByType('dap')
}
/**
* Sorts devices by a specified property in ascending or descending order.
*
* @param devices - Array of devices to sort
* @param sortBy - Property key to sort by (type-safe, must be valid DeviceWithMetrics key)
* @param order - Sort direction ('asc' or 'desc'), defaults to 'asc'
* @returns {DeviceWithMetrics[]} New sorted array (original array is not modified)
*
* @remarks
* - Creates a shallow copy to avoid mutating the input array
* - Handles undefined values by placing them at the end (asc) or start (desc)
* - Works with any comparable property (strings, numbers, etc.)
*
* @example
* ```ts
* const devices = DeviceService.getAllDevicesEnriched()
*
* // Sort by release year, newest first
* const newest = DeviceService.sortDevices(devices, 'releaseYear', 'desc')
*
* // Sort by name alphabetically
* const alphabetical = DeviceService.sortDevices(devices, 'name', 'asc')
*
* // Sort by age
* const oldest = DeviceService.sortDevices(devices, 'ageInYears', 'desc')
* ```
*/
static sortDevices(
devices: DeviceWithMetrics[],
sortBy: keyof DeviceWithMetrics,
order: SortOrder = 'asc'
): DeviceWithMetrics[] {
return [...devices].sort((a, b) => {
const aVal = a[sortBy]
const bVal = b[sortBy]
if (aVal === undefined || bVal === undefined) {
if (aVal === undefined && bVal === undefined) return 0
if (aVal === undefined) return order === 'asc' ? 1 : -1
return order === 'asc' ? -1 : 1
}
if (aVal === bVal) return 0
const comparison = aVal < bVal ? -1 : 1
return order === 'asc' ? comparison : -comparison
})
}
/**
* Finds related devices based on shared type and manufacturer.
*
* @param device - The reference device to find related devices for
* @returns {DeviceWithMetrics[]} Up to 3 related devices (excludes the input device)
*
* @remarks
* The algorithm prioritizes devices that share:
* 1. Same device type (mobile or DAP)
* 2. Same manufacturer
*
* The method deduplicates results using a Map and returns up to 3 devices.
* Useful for "You might also like" or related device suggestions.
*
* @example
* ```ts
* const pixel3a = DeviceService.getDeviceBySlug('bonito')
* if (pixel3a) {
* const related = DeviceService.getRelatedDevices(pixel3a)
* console.log(`Related devices: ${related.map(d => d.name).join(', ')}`)
* // Example output: "Pixel 7a, Pixel 3, Pixel 4a"
* }
* ```
*
* @example
* ```ts
* // For a Samsung DAP, finds other DAPs and Samsung devices
* const dap = DeviceService.getDeviceBySlug('komodo')
* const similar = DeviceService.getRelatedDevices(dap!)
* ```
*/
static getRelatedDevices(device: DeviceSpec): DeviceWithMetrics[] {
const sameType = this.filterDevices({ type: device.type })
.filter(d => d.slug !== device.slug)
const sameManufacturer = device.manufacturer
? this.filterDevices({ manufacturer: device.manufacturer })
.filter(d => d.slug !== device.slug)
: []
const combined = new Map<string, DeviceWithMetrics>()
sameType.forEach(d => combined.set(d.slug, d))
sameManufacturer.forEach(d => combined.set(d.slug, d))
return Array.from(combined.values()).slice(0, 3)
}
/**
* Computes comprehensive statistics for the entire device portfolio.
*
* @returns {DevicePortfolioStats} Aggregated portfolio metrics
*
* @remarks
* Calculates portfolio-wide statistics including:
* - Type distribution (mobile vs DAP)
* - Manufacturer breakdown
* - Average device age
* - Newest and oldest devices
*
* All devices are enriched with metrics before calculations.
*
* @example
* ```ts
* const stats = DeviceService.getDeviceStats()
*
* console.log(`Portfolio Overview:`)
* console.log(`Total: ${stats.total} devices`)
* console.log(`Mobile: ${stats.mobile}, DAP: ${stats.dap}`)
* console.log(`Average age: ${stats.averageAge.toFixed(1)} years`)
* console.log(`Newest: ${stats.newestDevice.name}`)
* console.log(`Oldest: ${stats.oldestDevice.name}`)
*
* // Manufacturer breakdown
* Object.entries(stats.byManufacturer).forEach(([mfr, count]) => {
* console.log(`${mfr}: ${count} devices`)
* })
* ```
*
* @example
* ```ts
* // Use for dashboard summary cards
* const stats = DeviceService.getDeviceStats()
* return (
* <div>
* <StatsCard label="Total Devices" value={stats.total} />
* <StatsCard label="Mobile" value={stats.mobile} />
* <StatsCard label="Average Age" value={`${stats.averageAge.toFixed(1)}y`} />
* </div>
* )
* ```
*/
static getDeviceStats(): DevicePortfolioStats {
const enriched = this.getAllDevicesEnriched()
return {
total: enriched.length,
mobile: enriched.filter(d => d.type === 'mobile').length,
dap: enriched.filter(d => d.type === 'dap').length,
byManufacturer: enriched.reduce((acc, d) => {
if (d.manufacturer) {
acc[d.manufacturer] = (acc[d.manufacturer] || 0) + 1
}
return acc
}, {} as Record<string, number>),
averageAge: enriched.reduce((sum, d) => sum + d.ageInYears, 0) / enriched.length,
newestDevice: this.sortDevices(enriched, 'releaseYear', 'desc')[0],
oldestDevice: this.sortDevices(enriched, 'releaseYear', 'asc')[0]
}
}
}