Add shared package: types, schemas, plans and API client
Holds everything web, mobile and API must agree on: DTOs, zod input schemas, plan entitlements (the single place feature gates live), streaming-service link helpers, feed ordering and a typed API client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type { Entitlements, Plan } from './plans';
|
||||
import type { NowPlayingProvider, StreamingService } from './services';
|
||||
|
||||
export interface TrackDTO {
|
||||
id: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string | null;
|
||||
artworkUrl: string | null;
|
||||
durationMs: number | null;
|
||||
isrc: string | null;
|
||||
links: Partial<Record<StreamingService, string>>;
|
||||
}
|
||||
|
||||
export interface UserSummaryDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string | null;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
export interface MeDTO extends UserSummaryDTO {
|
||||
email: string;
|
||||
bio: string | null;
|
||||
preferredService: StreamingService | null;
|
||||
plan: Plan;
|
||||
entitlements: Entitlements;
|
||||
}
|
||||
|
||||
export interface NowPlayingDTO {
|
||||
user: UserSummaryDTO;
|
||||
track: TrackDTO;
|
||||
isPlaying: boolean;
|
||||
source: NowPlayingProvider;
|
||||
progressMs: number | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** A feed entry. pinnedAt is set when the viewer pinned this friend. */
|
||||
export interface FeedItemDTO extends NowPlayingDTO {
|
||||
pinnedAt: string | null;
|
||||
}
|
||||
|
||||
/** Pinned first (oldest pin on top, so order is stable), then live listeners, then most recent. */
|
||||
export function sortFeed<T extends FeedItemDTO>(items: T[]): T[] {
|
||||
return [...items].sort(
|
||||
(a, b) =>
|
||||
Number(Boolean(b.pinnedAt)) - Number(Boolean(a.pinnedAt)) ||
|
||||
(a.pinnedAt && b.pinnedAt ? a.pinnedAt.localeCompare(b.pinnedAt) : 0) ||
|
||||
Number(b.isPlaying) - Number(a.isPlaying) ||
|
||||
b.updatedAt.localeCompare(a.updatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
export interface FeaturedSongDTO {
|
||||
track: TrackDTO;
|
||||
comment: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProfileDTO {
|
||||
user: UserSummaryDTO & { bio: string | null };
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
isFollowing: boolean;
|
||||
isPinned: boolean;
|
||||
isSelf: boolean;
|
||||
featured: FeaturedSongDTO | null;
|
||||
nowPlaying: NowPlayingDTO | null;
|
||||
}
|
||||
|
||||
export interface ConnectionDTO {
|
||||
provider: NowPlayingProvider;
|
||||
externalUserId: string;
|
||||
displayName: string | null;
|
||||
status: 'active' | 'error';
|
||||
lastError: string | null;
|
||||
lastPolledAt: string | null;
|
||||
/** Secret URL the user can paste into Plex/Jellyfin for instant updates. */
|
||||
webhookUrl: string | null;
|
||||
}
|
||||
|
||||
export interface TrackSearchResultDTO {
|
||||
deezerId: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
album: string | null;
|
||||
artworkUrl: string | null;
|
||||
durationMs: number | null;
|
||||
}
|
||||
|
||||
/** Messages pushed over the realtime websocket. */
|
||||
export type RealtimeMessage = { type: 'now_playing'; data: NowPlayingDTO };
|
||||
Reference in New Issue
Block a user