v3: added content slugs, fixed real-time updates in client, added @mentions across the app, added new file selector and drop zone

This commit is contained in:
khannurien
2026-03-22 16:06:26 +00:00
parent 39a0cc397e
commit 34e908d1bc
42 changed files with 2170 additions and 628 deletions

View File

@@ -23,9 +23,11 @@ export interface Dump {
id: string;
kind: "url" | "file";
title: string;
slug?: string;
comment?: string;
userId: string;
createdAt: Date;
updatedAt?: Date;
url?: string;
richContent?: RichContent;
fileName?: string;
@@ -45,6 +47,7 @@ export interface User {
username: string;
isAdmin: boolean;
createdAt: Date;
updatedAt?: Date;
avatarMime?: string;
invitedByUsername?: string;
}
@@ -55,14 +58,15 @@ export interface PublicUser {
username: string;
isAdmin: boolean;
createdAt: Date;
updatedAt?: Date;
avatarMime?: string;
invitedByUsername?: string;
}
// Wire types — createdAt arrives as an ISO string from API/WS/localStorage
type WithStringDate<T extends { createdAt: Date }> = Omit<T, "createdAt"> & {
createdAt: string;
};
// Wire types — createdAt/updatedAt arrive as ISO strings from API/WS/localStorage
type WithStringDate<T extends { createdAt: Date }> =
& Omit<T, "createdAt" | "updatedAt">
& { createdAt: string; updatedAt?: string };
export type RawDump = WithStringDate<Dump>;
export type RawUser = WithStringDate<User>;
export type RawPublicUser = WithStringDate<PublicUser>;
@@ -70,15 +74,27 @@ export type RawAuthResponse = Omit<AuthResponse, "user"> & { user: RawUser };
// Deserializers — convert wire types to domain types at API/WS/localStorage boundaries
export function deserializeDump(raw: RawDump): Dump {
return { ...raw, createdAt: new Date(raw.createdAt) };
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
};
}
export function deserializeUser(raw: RawUser): User {
return { ...raw, createdAt: new Date(raw.createdAt) };
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
};
}
export function deserializePublicUser(raw: RawPublicUser): PublicUser {
return { ...raw, createdAt: new Date(raw.createdAt) };
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
};
}
export function deserializeAuthResponse(raw: RawAuthResponse): AuthResponse {
@@ -117,6 +133,7 @@ export interface Comment {
parentId?: string;
body: string;
createdAt: Date;
updatedAt?: Date;
deleted: boolean;
authorUsername: string;
authorAvatarMime?: string;
@@ -125,7 +142,11 @@ export interface Comment {
export type RawComment = WithStringDate<Comment>;
export function deserializeComment(raw: RawComment): Comment {
return { ...raw, createdAt: new Date(raw.createdAt) };
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
};
}
/**
@@ -136,9 +157,11 @@ export interface Playlist {
id: string;
userId: string;
title: string;
slug?: string;
description?: string;
isPublic: boolean;
createdAt: Date;
updatedAt?: Date;
imageMime?: string;
dumpCount?: number;
ownerUsername?: string;
@@ -163,7 +186,11 @@ export type RawPlaylistWithDumps =
export type RawPlaylistMembership = { playlist: RawPlaylist; hasDump: boolean };
export function deserializePlaylist(raw: RawPlaylist): Playlist {
return { ...raw, createdAt: new Date(raw.createdAt) };
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
};
}
export function deserializePlaylistWithDumps(
@@ -172,6 +199,7 @@ export function deserializePlaylistWithDumps(
return {
...raw,
createdAt: new Date(raw.createdAt),
updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined,
dumps: raw.dumps.map(deserializeDump),
};
}
@@ -340,7 +368,8 @@ export type NotificationType =
| "user_followed"
| "user_dump_posted"
| "playlist_dump_added"
| "dump_upvoted";
| "dump_upvoted"
| "user_mentioned";
export interface PlaylistFollowedData {
followerId: string;
@@ -375,12 +404,22 @@ export interface DumpUpvotedData {
dumpTitle: string;
}
export interface UserMentionedData {
mentionerId: string;
mentionerUsername: string;
contextType: "comment" | "dump" | "playlist";
contextId: string;
contextTitle: string;
dumpId?: string;
}
export type NotificationData =
| PlaylistFollowedData
| UserFollowedData
| UserDumpPostedData
| PlaylistDumpAddedData
| DumpUpvotedData;
| DumpUpvotedData
| UserMentionedData;
export interface Notification {
id: string;