some fixes

This commit is contained in:
Michael Freno
2025-12-21 01:15:27 -05:00
parent 200037e7a0
commit cf2a217afd
5 changed files with 207 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import {
onMount, onMount,
createEffect, createEffect,
createSignal, createSignal,
createMemo,
Show, Show,
For, For,
onCleanup onCleanup
@@ -18,6 +19,12 @@ import { DarkModeToggle } from "./DarkModeToggle";
import { SkeletonBox, SkeletonText } from "./SkeletonLoader"; import { SkeletonBox, SkeletonText } from "./SkeletonLoader";
import { env } from "~/env/client"; import { env } from "~/env/client";
function formatDomainName(url: string): string {
const domain = url.split("://")[1]?.split(":")[0] ?? url;
const withoutWww = domain.replace(/^www\./i, "");
return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1);
}
interface GitCommit { interface GitCommit {
sha: string; sha: string;
message: string; message: string;
@@ -385,10 +392,7 @@ export function LeftBar() {
<div class="flex h-full min-h-full flex-col overflow-y-auto"> <div class="flex h-full min-h-full flex-col overflow-y-auto">
<Typewriter speed={10} keepAlive={10000} class="z-50 pr-8 pl-4"> <Typewriter speed={10} keepAlive={10000} class="z-50 pr-8 pl-4">
<h3 class="hover:text-subtext0 w-fit pt-6 text-center text-3xl underline transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105"> <h3 class="hover:text-subtext0 w-fit pt-6 text-center text-3xl underline transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105">
<a href="/"> <a href="/">{formatDomainName(env.VITE_DOMAIN)}</a>
{env.VITE_DOMAIN.split("://")[1].charAt(0).toUpperCase() +
env.VITE_DOMAIN.split("://")[1].slice(1)}
</a>
</h3> </h3>
</Typewriter> </Typewriter>

View File

@@ -37,6 +37,11 @@ export default function CommentDeletionPrompt(
} else if (fullDeleteChecked()) { } else if (fullDeleteChecked()) {
deleteType = "database"; deleteType = "database";
} }
console.log("[CommentDeletionPrompt] Calling deleteComment:", {
commentID: props.commentID,
commenterID: props.commenterID,
deleteType
});
props.deleteComment(props.commentID, props.commenterID, deleteType); props.deleteComment(props.commentID, props.commenterID, deleteType);
}; };
@@ -45,7 +50,7 @@ export default function CommentDeletionPrompt(
return ( return (
<div class="flex justify-center"> <div class="flex justify-center">
<div class="fixed top-48 z-100 h-fit w-11/12 sm:w-4/5 md:w-2/3"> <div class="fixed top-48 z-100 h-fit">
<div <div
id="delete_prompt" id="delete_prompt"
class="fade-in bg-red rounded-md px-8 py-4 shadow-lg brightness-110" class="fade-in bg-red rounded-md px-8 py-4 shadow-lg brightness-110"

View File

@@ -10,6 +10,7 @@ import type {
DeletionType DeletionType
} from "~/types/comment"; } from "~/types/comment";
import { getSQLFormattedDate } from "~/lib/comment-utils"; import { getSQLFormattedDate } from "~/lib/comment-utils";
import { api } from "~/lib/api";
import CommentSection from "./CommentSection"; import CommentSection from "./CommentSection";
import CommentDeletionPrompt from "./CommentDeletionPrompt"; import CommentDeletionPrompt from "./CommentDeletionPrompt";
import EditCommentModal from "./EditCommentModal"; import EditCommentModal from "./EditCommentModal";
@@ -332,20 +333,31 @@ export default function CommentSectionWrapper(
}; };
// Comment deletion // Comment deletion
const deleteComment = ( const deleteComment = async (
commentID: number, commentID: number,
commenterID: string, commenterID: string,
deletionType: DeletionType deletionType: DeletionType
) => { ) => {
console.log("[deleteComment] Starting deletion:", {
commentID,
commenterID,
deletionType,
currentUserID: props.currentUserID,
socketState: socket?.readyState
});
setCommentDeletionLoading(true); setCommentDeletionLoading(true);
if (!props.currentUserID) { if (!props.currentUserID) {
console.warn("Cannot delete comment: user not authenticated"); console.warn(
"[deleteComment] Cannot delete comment: user not authenticated"
);
setCommentDeletionLoading(false); setCommentDeletionLoading(false);
return; return;
} }
if (socket) { if (socket && socket.readyState === WebSocket.OPEN) {
console.log("[deleteComment] Using WebSocket");
try { try {
socket.send( socket.send(
JSON.stringify({ JSON.stringify({
@@ -358,9 +370,53 @@ export default function CommentSectionWrapper(
}) })
); );
} catch (error) { } catch (error) {
console.error("Error sending comment deletion:", error); console.error(
setCommentDeletionLoading(false); "[deleteComment] WebSocket error, falling back to HTTP:",
error
);
// Fallback to HTTP API on WebSocket error
await fallbackCommentDeletion(commentID, commenterID, deletionType);
} }
} else {
console.log(
"[deleteComment] WebSocket not available, using HTTP fallback"
);
// Fallback to HTTP API if WebSocket unavailable
await fallbackCommentDeletion(commentID, commenterID, deletionType);
}
};
const fallbackCommentDeletion = async (
commentID: number,
commenterID: string,
deletionType: DeletionType
) => {
console.log("[fallbackCommentDeletion] Calling tRPC endpoint:", {
commentID,
commenterID,
deletionType
});
try {
const result = await api.database.deleteComment.mutate({
commentID,
commenterID,
deletionType
});
console.log("[fallbackCommentDeletion] Success:", result);
// Handle the deletion response
deleteCommentHandler({
action: "commentDeletionBroadcast",
commentID: commentID,
commentBody: result.commentBody || undefined,
commenterID: commenterID,
deletionType: deletionType
});
} catch (error) {
console.error("[fallbackCommentDeletion] Error:", error);
setCommentDeletionLoading(false);
} }
}; };

View File

@@ -40,7 +40,7 @@ export default function CommentSorting(props: CommentSortingProps) {
{(topLevelComment) => ( {(topLevelComment) => (
<div <div
onClick={() => checkForDoubleClick(topLevelComment.id)} onClick={() => checkForDoubleClick(topLevelComment.id)}
class="bg-crust mt-4 max-w-full rounded py-2 pl-2 shadow-xl select-none sm:pl-4 md:pl-8 lg:pl-12" class="bg-mantle mt-4 max-w-full rounded-lg py-2 pl-2 shadow-xl select-none sm:pl-4 md:pl-8 lg:pl-12"
> >
<Show <Show
when={showingBlock().get(topLevelComment.id)} when={showingBlock().get(topLevelComment.id)}

View File

@@ -1,4 +1,8 @@
import { createTRPCRouter, publicProcedure } from "../utils"; import {
createTRPCRouter,
publicProcedure,
protectedProcedure
} from "../utils";
import { z } from "zod"; import { z } from "zod";
import { ConnectionFactory } from "~/server/utils"; import { ConnectionFactory } from "~/server/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -122,6 +126,132 @@ export const databaseRouter = createTRPCRouter({
} }
}), }),
deleteComment: protectedProcedure
.input(
z.object({
commentID: z.number(),
commenterID: z.string(),
deletionType: z.enum(["user", "admin", "database"])
})
)
.mutation(async ({ input, ctx }) => {
try {
const conn = ConnectionFactory();
console.log("[deleteComment] Starting deletion:", {
commentID: input.commentID,
deletionType: input.deletionType,
userId: ctx.userId,
privilegeLevel: ctx.privilegeLevel
});
// Get the comment to check ownership
const commentQuery = await conn.execute({
sql: "SELECT * FROM Comment WHERE id = ?",
args: [input.commentID]
});
const comment = commentQuery.rows[0] as any;
if (!comment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Comment not found"
});
}
// Authorization checks
const isOwner = comment.commenter_id === ctx.userId;
const isAdmin = ctx.privilegeLevel === "admin";
console.log("[deleteComment] Authorization check:", {
isOwner,
isAdmin,
commentOwner: comment.commenter_id,
requestingUser: ctx.userId
});
// User can only delete their own comments with "user" type
if (input.deletionType === "user" && !isOwner && !isAdmin) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only delete your own comments"
});
}
// Only admins can do admin or database deletion
if (
(input.deletionType === "admin" ||
input.deletionType === "database") &&
!isAdmin
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required for this deletion type"
});
}
if (input.deletionType === "database") {
console.log("[deleteComment] Performing database deletion");
// Full deletion - remove from database
// First delete reactions
await conn.execute({
sql: "DELETE FROM CommentReaction WHERE comment_id = ?",
args: [input.commentID]
});
// Then delete the comment
await conn.execute({
sql: "DELETE FROM Comment WHERE id = ?",
args: [input.commentID]
});
console.log("[deleteComment] Database deletion successful");
return {
success: true,
deletionType: "database",
commentBody: null
};
} else if (input.deletionType === "admin") {
console.log("[deleteComment] Performing admin deletion");
// Admin delete - replace body with admin message
await conn.execute({
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
args: ["[deleted by admin]", "", input.commentID]
});
console.log("[deleteComment] Admin deletion successful");
return {
success: true,
deletionType: "admin",
commentBody: "[deleted by admin]"
};
} else {
console.log("[deleteComment] Performing user deletion");
// User delete - replace body with user message
await conn.execute({
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
args: ["[deleted]", "", input.commentID]
});
console.log("[deleteComment] User deletion successful");
return {
success: true,
deletionType: "user",
commentBody: "[deleted]"
};
}
} catch (error) {
console.error("[deleteComment] Failed to delete comment:", error);
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete comment"
});
}
}),
getCommentsByPostId: publicProcedure getCommentsByPostId: publicProcedure
.input(z.object({ post_id: z.string() })) .input(z.object({ post_id: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {