pc: PC setup tracker at /trackers/pc on the hardware schema
All checks were successful
Deploy production / deploy (push) Successful in 1m4s
All checks were successful
Deploy production / deploy (push) Successful in 1m4s
- model the existing hardware schema in Drizzle (devices, components, new sensitivities) - idempotent reconciling migration: new columns, checks, setup_overview view, grants, meta.tables - seed: main PC labels/hostnames, planned AMD X3D + RTX 5090 build, stan-ovh (OVH RISE-S), other devices, sensitivity placeholders (no invented cm/360) - owner-only add/edit via modals; public view hides notes/order numbers - trackers dropdown entry + icon, seo/title/sitemap, owner write prefix
This commit is contained in:
177
src/lib/pc/shared.ts
Normal file
177
src/lib/pc/shared.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// Client-safe helpers for the PC setup tracker (/trackers/pc): labels, colours, formatting
|
||||
// and the cm/360 maths. No server imports here.
|
||||
|
||||
export type Specs = Record<string, string | number | boolean | null>;
|
||||
|
||||
export type Part = {
|
||||
id: number;
|
||||
deviceId: number;
|
||||
category: string;
|
||||
brand: string | null;
|
||||
model: string;
|
||||
quantity: number;
|
||||
specs: Specs;
|
||||
unitPriceCents: number | null;
|
||||
totalPriceCents: number | null;
|
||||
shop: string | null;
|
||||
orderDate: string | null;
|
||||
warrantyUntil: string | null;
|
||||
status: string;
|
||||
verified: boolean;
|
||||
dpi: number | null;
|
||||
measuredDpi: number | null;
|
||||
url: string | null;
|
||||
imageUrl: string | null;
|
||||
/** Owner only (null for everyone else). */
|
||||
notes: string | null;
|
||||
orderNumber: string | null;
|
||||
invoiceNumber: string | null;
|
||||
source: string | null;
|
||||
};
|
||||
|
||||
export type Device = {
|
||||
id: number;
|
||||
name: string;
|
||||
label: string | null;
|
||||
hostnames: string[];
|
||||
kind: string;
|
||||
status: string;
|
||||
brand: string | null;
|
||||
model: string | null;
|
||||
specs: Specs;
|
||||
priceCents: number | null;
|
||||
monthlyCents: number | null;
|
||||
budgetCents: number | null;
|
||||
shop: string | null;
|
||||
purchaseDate: string | null;
|
||||
url: string | null;
|
||||
imageUrl: string | null;
|
||||
sortOrder: number;
|
||||
notes: string | null;
|
||||
parts: Part[];
|
||||
};
|
||||
|
||||
export type Game = {
|
||||
id: number;
|
||||
game: string;
|
||||
cmPer360: number | null;
|
||||
yaw: number | null;
|
||||
inGameSens: number | null;
|
||||
mouseId: number | null;
|
||||
fov: string | null;
|
||||
notes: string | null;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
/** Build order, then peripherals. Gradient tiles: black/white page, colourful content. */
|
||||
export const CATEGORY_META: Record<string, { label: string; from: string; to: string }> = {
|
||||
case: { label: 'Case', from: '#e5e7eb', to: '#9ca3af' },
|
||||
cpu: { label: 'CPU', from: '#60a5fa', to: '#2563eb' },
|
||||
motherboard: { label: 'Motherboard', from: '#34d399', to: '#059669' },
|
||||
ram: { label: 'Memory', from: '#a78bfa', to: '#7c3aed' },
|
||||
cpu_cooler: { label: 'Cooler', from: '#67e8f9', to: '#0891b2' },
|
||||
gpu: { label: 'Graphics', from: '#f87171', to: '#dc2626' },
|
||||
psu: { label: 'Power', from: '#fde047', to: '#ca8a04' },
|
||||
storage: { label: 'Storage', from: '#fb923c', to: '#ea580c' },
|
||||
fan: { label: 'Fans', from: '#7dd3fc', to: '#0284c7' },
|
||||
lighting: { label: 'Lighting', from: '#f0abfc', to: '#c026d3' },
|
||||
monitor: { label: 'Monitor', from: '#818cf8', to: '#4338ca' },
|
||||
mouse: { label: 'Mouse', from: '#f472b6', to: '#db2777' },
|
||||
keyboard: { label: 'Keyboard', from: '#2dd4bf', to: '#0d9488' },
|
||||
headset: { label: 'Headset', from: '#c4b5fd', to: '#6d28d9' },
|
||||
audio: { label: 'Audio', from: '#fda4af', to: '#e11d48' },
|
||||
controller: { label: 'Controller', from: '#bef264', to: '#65a30d' },
|
||||
accessory: { label: 'Accessory', from: '#cbd5e1', to: '#64748b' },
|
||||
other: { label: 'Other', from: '#d4d4d8', to: '#71717a' }
|
||||
};
|
||||
export const CATEGORIES = Object.keys(CATEGORY_META);
|
||||
export const categoryLabel = (c: string) => CATEGORY_META[c]?.label ?? c;
|
||||
|
||||
export const DEVICE_KIND_META: Record<string, { label: string; from: string; to: string }> = {
|
||||
desktop: { label: 'Desktop', from: '#22d3ee', to: '#0891b2' },
|
||||
laptop: { label: 'Laptop', from: '#a78bfa', to: '#7c3aed' },
|
||||
server: { label: 'Server', from: '#34d399', to: '#059669' },
|
||||
vm: { label: 'VM', from: '#94a3b8', to: '#475569' },
|
||||
phone: { label: 'Phone', from: '#f472b6', to: '#db2777' },
|
||||
tablet: { label: 'Tablet', from: '#fb923c', to: '#ea580c' },
|
||||
monitor: { label: 'Monitor', from: '#818cf8', to: '#4338ca' },
|
||||
other: { label: 'Other', from: '#d4d4d8', to: '#71717a' }
|
||||
};
|
||||
export const DEVICE_KINDS = Object.keys(DEVICE_KIND_META);
|
||||
export const DEVICE_STATUSES = ['active', 'spare', 'planned', 'retired', 'sold'] as const;
|
||||
export const PART_STATUSES = ['in_use', 'planned', 'spare', 'retired', 'sold'] as const;
|
||||
|
||||
export const statusLabel = (s: string) => s.replace('_', ' ');
|
||||
|
||||
const eur2 = new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 2
|
||||
});
|
||||
const eur0 = new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
export const money = (cents: number | null | undefined) =>
|
||||
cents === null || cents === undefined
|
||||
? null
|
||||
: (cents % 100 === 0 ? eur0 : eur2).format(cents / 100);
|
||||
|
||||
export const shortDate = (d: string | null) =>
|
||||
d
|
||||
? new Date(`${d}T12:00:00Z`).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})
|
||||
: null;
|
||||
|
||||
/** "key: value" lines (the edit form) from specs and back. */
|
||||
export const specsToText = (specs: Specs) =>
|
||||
Object.entries(specs)
|
||||
.map(([k, v]) => `${k}: ${v ?? ''}`)
|
||||
.join('\n');
|
||||
|
||||
export function textToSpecs(text: string): Specs {
|
||||
const out: Specs = {};
|
||||
for (const line of text.split('\n')) {
|
||||
const at = line.indexOf(':');
|
||||
if (at < 1) continue;
|
||||
const key = line.slice(0, at).trim().slice(0, 60);
|
||||
const raw = line
|
||||
.slice(at + 1)
|
||||
.trim()
|
||||
.slice(0, 300);
|
||||
if (!key) continue;
|
||||
out[key] =
|
||||
raw === ''
|
||||
? null
|
||||
: raw === 'true'
|
||||
? true
|
||||
: raw === 'false'
|
||||
? false
|
||||
: /^-?\d+(\.\d+)?$/.test(raw)
|
||||
? Number(raw)
|
||||
: raw;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** "speed_mts" -> "speed mts", for spec chips. */
|
||||
export const specKey = (k: string) => k.replace(/_/g, ' ');
|
||||
|
||||
/**
|
||||
* In-game sensitivity for a physical cm/360 with a mouse of `dpi` real counts per inch, in a
|
||||
* game that turns `yaw` degrees per count at sensitivity 1: sens = 360 × 2.54 / (yaw × dpi × cm).
|
||||
*/
|
||||
export function sensFor(cmPer360: number | null, yaw: number | null, dpi: number | null) {
|
||||
if (!cmPer360 || !yaw || !dpi) return null;
|
||||
return (360 * 2.54) / (yaw * dpi * cmPer360);
|
||||
}
|
||||
|
||||
/** cm/360 that a given in-game sens gives with a mouse (inverse of sensFor). */
|
||||
export function cmFor(sens: number | null, yaw: number | null, dpi: number | null) {
|
||||
if (!sens || !yaw || !dpi) return null;
|
||||
return (360 * 2.54) / (yaw * dpi * sens);
|
||||
}
|
||||
Reference in New Issue
Block a user