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

@@ -5,6 +5,7 @@ import {
type APIResponse,
type Comment,
isCreateCommentRequest,
isUpdateCommentRequest,
} from "../model/interfaces.ts";
import { authMiddleware } from "../middleware/auth.ts";
import { verifyJWT } from "../lib/jwt.ts";
@@ -12,11 +13,13 @@ import {
createComment,
deleteComment,
getComments,
updateComment,
} from "../services/comment-service.ts";
import { getDump } from "../services/dump-service.ts";
import {
broadcastCommentCreated,
broadcastCommentDeleted,
broadcastCommentUpdated,
} from "../services/ws-service.ts";
const router = new Router({ prefix: "/api" });
@@ -62,6 +65,29 @@ router.post("/dumps/:dumpId/comments", authMiddleware, async (ctx) => {
ctx.response.body = responseBody;
});
// PATCH /api/comments/:commentId — auth required
router.patch("/comments/:commentId", authMiddleware, async (ctx) => {
const userId = ctx.state.user.userId as string;
const isAdmin = (ctx.state.user.isAdmin ?? false) as boolean;
const body = await ctx.request.body.json();
if (!isUpdateCommentRequest(body)) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid comment data",
);
}
const { comment, isPrivate } = updateComment(
ctx.params.commentId,
body.body,
userId,
isAdmin,
);
if (!isPrivate) broadcastCommentUpdated(comment);
const responseBody: APIResponse<Comment> = { success: true, data: comment };
ctx.response.body = responseBody;
});
// DELETE /api/comments/:commentId — auth required
router.delete("/comments/:commentId", authMiddleware, (ctx) => {
const userId = ctx.state.user.userId as string;

View File

@@ -14,6 +14,7 @@ import {
createUser,
getUserById,
getUserByUsername,
searchUsers,
} from "../services/user-service.ts";
import { redeemInvite, validateInvite } from "../services/invite-service.ts";
import {
@@ -131,6 +132,13 @@ router.get("/me", authMiddleware, (ctx: AuthContext) => {
}
});
// User search for @mention autocomplete
router.get("/search", (ctx) => {
const q = (ctx.request.url.searchParams.get("q") ?? "").trim();
const results = searchUsers(q, 8);
ctx.response.body = { success: true, data: results };
});
// Public user profile by internal ID (used when only userId is available, e.g. dump.userId)
router.get("/by-id/:userId", (ctx) => {
const user = getUserById(ctx.params.userId);