Kyle Pope cb9f74a387 Entity pages enhancement: backend model extensions, shared components, Locations rebuild, panel animations
- Add migrations 019/020: extend Person (first/last name, nickname, is_favourite, company, job_title, mobile, category) and Location (is_frequent, contact_number, email)
- Update Person/Location models, schemas, and routers with new fields + name denormalisation
- Create shared component library: EntityTable, EntityDetailPanel, CategoryFilterBar, CopyableField, CategoryAutocomplete, useTableVisibility hook
- Rebuild LocationsPage: table layout with sortable columns, detail side panel, category filter bar, frequent pinned section
- Extend LocationForm with contact number, email, frequent toggle, category autocomplete
- Add animated panel transitions to ProjectDetail (55/45 split with cubic-bezier easing)
- Update TypeScript interfaces for Person and Location

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:10:26 +08:00

57 lines
1.8 KiB
TypeScript

import { formatDistanceToNow, parseISO, addYears, differenceInDays } from 'date-fns';
// Deterministic avatar color from name hash
export const avatarColors = [
'bg-rose-500/20 text-rose-400',
'bg-blue-500/20 text-blue-400',
'bg-purple-500/20 text-purple-400',
'bg-pink-500/20 text-pink-400',
'bg-teal-500/20 text-teal-400',
'bg-orange-500/20 text-orange-400',
'bg-green-500/20 text-green-400',
'bg-amber-500/20 text-amber-400',
];
export function getInitials(name: string): string {
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
return name.slice(0, 2).toUpperCase();
}
export function getAvatarColor(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
}
return avatarColors[Math.abs(hash) % avatarColors.length];
}
export function formatUpdatedAt(updatedAt: string): string {
try {
return `Updated ${formatDistanceToNow(parseISO(updatedAt), { addSuffix: true })}`;
} catch {
return '';
}
}
export function getNextBirthday(birthday: string): Date {
const today = new Date();
const parsed = parseISO(birthday);
const thisYear = new Date(today.getFullYear(), parsed.getMonth(), parsed.getDate());
if (thisYear < today) {
return addYears(thisYear, 1);
}
return thisYear;
}
export function getDaysUntilBirthday(birthday: string): number {
const next = getNextBirthday(birthday);
return differenceInDays(next, new Date());
}
export function splitName(name: string): { firstName: string; lastName: string } {
const idx = name.indexOf(' ');
if (idx === -1) return { firstName: name, lastName: '' };
return { firstName: name.slice(0, idx), lastName: name.slice(idx + 1) };
}