feat: add shared @sexy.pivoine.art/types package and fix type safety across frontend/backend

- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.)
- Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:*
- Update backend Pothos objectRef types to use shared interfaces instead of inline types
- Update frontend $lib/types.ts to re-export from shared package
- Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[])
- Add artist_name to comment user select in backend resolver
- Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:01:11 +01:00
parent c6126c13e9
commit 97269788ee
31 changed files with 839 additions and 822 deletions

View File

@@ -0,0 +1,16 @@
{
"name": "@sexy.pivoine.art/types",
"version": "1.0.0",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts"
}
},
"scripts": {
"check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

281
packages/types/src/index.ts Normal file
View File

@@ -0,0 +1,281 @@
// ─── Core entities ───────────────────────────────────────────────────────────
export interface MediaFile {
id: string;
title: string | null;
description: string | null;
filename: string;
mime_type: string | null;
filesize: number | null;
duration: number | null;
uploaded_by: string | null;
date_created: Date;
}
export interface User {
id: string;
email: string;
first_name: string | null;
last_name: string | null;
artist_name: string | null;
slug: string | null;
description: string | null;
tags: string[] | null;
role: "model" | "viewer" | "admin";
/** UUID of the avatar file */
avatar: string | null;
/** UUID of the banner file */
banner: string | null;
email_verified: boolean;
date_created: Date;
}
export type CurrentUser = User;
// ─── Video ───────────────────────────────────────────────────────────────────
export interface VideoModel {
id: string;
artist_name: string | null;
slug: string | null;
avatar: string | null;
}
export interface VideoFile {
id: string;
filename: string;
mime_type: string | null;
duration: number | null;
}
export interface Video {
id: string;
slug: string;
title: string;
description: string | null;
image: string | null;
movie: string | null;
tags: string[] | null;
upload_date: Date;
premium: boolean | null;
featured: boolean | null;
likes_count: number | null;
plays_count: number | null;
models?: VideoModel[];
movie_file?: VideoFile | null;
}
// ─── Model ───────────────────────────────────────────────────────────────────
export interface ModelPhoto {
id: string;
filename: string;
}
export interface Model {
id: string;
slug: string | null;
artist_name: string | null;
description: string | null;
avatar: string | null;
banner: string | null;
tags: string[] | null;
date_created: Date;
photos?: ModelPhoto[];
}
// ─── Article ─────────────────────────────────────────────────────────────────
export interface ArticleAuthor {
first_name: string | null;
last_name: string | null;
avatar: string | null;
description: string | null;
website?: string | null;
}
export interface Article {
id: string;
slug: string;
title: string;
excerpt: string | null;
content: string | null;
image: string | null;
tags: string[] | null;
publish_date: Date;
category: string | null;
featured: boolean | null;
author?: ArticleAuthor | null;
}
// ─── Comment ─────────────────────────────────────────────────────────────────
export interface CommentUser {
id: string;
first_name: string | null;
last_name: string | null;
artist_name: string | null;
avatar: string | null;
}
export interface Comment {
id: number;
collection: string;
item_id: string;
comment: string;
user_id: string;
date_created: Date;
user?: CommentUser | null;
}
// ─── Stats ───────────────────────────────────────────────────────────────────
export interface Stats {
videos_count: number;
models_count: number;
viewers_count: number;
}
// ─── Recording ───────────────────────────────────────────────────────────────
export interface RecordedEvent {
timestamp: number;
deviceIndex: number;
deviceName: string;
actuatorIndex: number;
actuatorType: string;
value: number;
}
export interface DeviceInfo {
name: string;
index: number;
capabilities: string[];
}
export interface Recording {
id: string;
title: string;
description: string | null;
slug: string;
duration: number;
events: object[] | null;
device_info: object[] | null;
user_id: string;
status: "draft" | "published" | "archived";
tags: string[] | null;
linked_video: string | null;
featured: boolean | null;
public: boolean | null;
original_recording_id?: string | null;
date_created: Date;
date_updated: Date | null;
}
// ─── Video interactions ───────────────────────────────────────────────────────
export interface VideoLikeStatus {
liked: boolean;
}
export interface VideoPlayRecord {
id: string;
video_id: string;
duration_watched?: number;
completed: boolean;
}
export interface VideoLikeResponse {
liked: boolean;
likes_count: number;
}
export interface VideoPlayResponse {
success: boolean;
play_id: string;
plays_count: number;
}
// ─── Analytics ───────────────────────────────────────────────────────────────
export interface VideoAnalytics {
id: string;
title: string;
slug: string;
upload_date: Date;
likes: number;
plays: number;
completed_plays: number;
completion_rate: number;
avg_watch_time: number;
}
export interface Analytics {
total_videos: number;
total_likes: number;
total_plays: number;
plays_by_date: Record<string, number>;
likes_by_date: Record<string, number>;
videos: VideoAnalytics[];
}
// ─── Gamification ────────────────────────────────────────────────────────────
export interface LeaderboardEntry {
user_id: string;
display_name: string | null;
avatar: string | null;
total_weighted_points: number | null;
total_raw_points: number | null;
recordings_count: number | null;
playbacks_count: number | null;
achievements_count: number | null;
rank: number;
}
export interface UserStats {
user_id: string;
total_raw_points: number | null;
total_weighted_points: number | null;
recordings_count: number | null;
playbacks_count: number | null;
comments_count: number | null;
achievements_count: number | null;
rank: number;
}
export interface UserAchievement {
id: string;
code: string;
name: string;
description: string | null;
icon: string | null;
category: string | null;
date_unlocked: Date;
progress: number | null;
required_count: number;
}
export interface RecentPoint {
action: string;
points: number;
date_created: Date;
recording_id: string | null;
}
export interface UserGamification {
stats: UserStats | null;
achievements: UserAchievement[];
recent_points: RecentPoint[];
}
export interface Achievement {
id: string;
code: string;
name: string;
description: string | null;
icon: string | null;
category: string | null;
required_count: number;
points_reward: number;
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true
},
"include": ["src/**/*"]
}