Implemented comment viewing and posting

This commit is contained in:
2026-03-01 12:52:22 -05:00
parent 5cdbf72328
commit 5483e42bb4
9 changed files with 203 additions and 18 deletions

View File

@@ -105,6 +105,7 @@ import { calendarRouter } from './routes/calendar';
import { docsRouter } from './routes/docs';
import { units } from './routes/units';
import { modRequestRouter } from './routes/modRequest'
import { discussionRouter } from './routes/discussion';
app.use('/application', applicationRouter);
app.use('/ranks', ranks);
@@ -121,6 +122,7 @@ app.use('/calendar', calendarRouter)
app.use('/units', units)
app.use('/docs', docsRouter)
app.use('/mod-request', modRequestRouter)
app.use('/discussions', discussionRouter)
app.use('/', authRouter)
app.get('/ping', (req, res) => {

View File

@@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
import { Request, Response } from 'express';
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
import { logger } from '../services/logging/logger';
import { audit } from '../services/logging/auditLog';
import { MemberState } from '@app/shared/types/member';
import { createDiscussion, getAllDiscussions, getDiscussionById, postComment } from '../services/db/discussionService';
import { ModRequest } from '@app/shared/schemas/modRequest';
import { DiscussionComment } from '@app/shared/types/discussion';
router.use(requireLogin);
router.use(requireMemberState(MemberState.Member));
router.post('/comment', async (req: Request, res: Response) => {
try {
let comment = req.body as DiscussionComment;
console.log(comment);
await postComment(comment, req.user.id);
res.sendStatus(201);
} catch (error) {
}
});
router.delete('/comment/:id', async (req: Request, res: Response) => {
})
export const discussionRouter = router;

View File

@@ -1,10 +1,9 @@
import { toDateTime } from "@app/shared/utils/time";
import pool from "../../db";
import { LOARequest, LOAType } from '@app/shared/types/loa'
import { PagedData } from '@app/shared/types/pagination'
import { DiscussionPost } from '@app/shared/types/discussion';
import { DiscussionComment } from '@app/shared/types/discussion';
/**
* Retrieves all discussion posts with pagination and optional type filtering.
@@ -98,7 +97,8 @@ export async function createDiscussion<T>(type: string, authorID: number, postTi
* @returns {Promise<DiscussionPost<T> | null>} The discussion post or null if not found
*/
export async function getDiscussionById<T>(id: number): Promise<DiscussionPost<T> | null> {
const sql = `
// Get the post
const postSql = `
SELECT
p.*,
m.name as poster_name
@@ -107,14 +107,42 @@ export async function getDiscussionById<T>(id: number): Promise<DiscussionPost<T
WHERE p.id = ?
LIMIT 1;
`;
const results = (await pool.query(sql, [id])) as DiscussionPost<T>[];
if (results.length === 0) {
const postResults = (await pool.query(postSql, [id])) as DiscussionPost<T>[];
if (postResults.length === 0) {
return null;
}
return results[0];
const post = postResults[0];
// Get comments for the post
const commentSql = `
SELECT
c.*
FROM discussion_comments AS c
WHERE c.post_id = ?
AND c.is_deleted = FALSE
ORDER BY c.created_at ASC;
`;
const comments = (await pool.query(commentSql, [id])) as DiscussionComment[];
// Attach comments to post
post.comments = comments;
return post;
}
export async function postComment() {
export async function getPostComments(postID: number): Promise<DiscussionComment[]> {
let comments = await pool.query("SELECT * FROM discussion_comments WHERE post_id = ?", [postID]);
return comments;
}
export async function postComment(commentData: DiscussionComment, poster: number) {
const sql = `
INSERT INTO discussion_comments (post_id, poster_id, content) VALUES (?, ?, ?);
`;
const result = await pool.query(sql, [commentData.post_id, poster, commentData.content]);
if (!result.affectedRows || result.affectedRows !== 1) {
throw new Error('Failed to insert comment: expected 1 row to be inserted');
}
}

View File

@@ -16,9 +16,9 @@ export interface DiscussionPost<T = any> {
export interface DiscussionComment {
id?: number;
post_id: number;
poster_id: number;
poster_id?: number;
content: string;
created_at: Date;
updated_at: Date;
is_deleted: boolean;
created_at?: Date;
updated_at?: Date;
is_deleted?: boolean;
}

22
ui/src/api/discussion.ts Normal file
View File

@@ -0,0 +1,22 @@
import { DiscussionComment } from "@shared/types/discussion";
//@ts-expect-error
const addr = import.meta.env.VITE_APIHOST;
export async function postComment(comment: DiscussionComment) {
const res = await fetch(`${addr}/discussions/comment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(comment),
credentials: 'include',
});
if (res.ok) {
return;
} else {
throw new Error("Failed to submit LOA");
}
}

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { ref } from 'vue';
import Textarea from '../ui/textarea/Textarea.vue';
import Button from '../ui/button/Button.vue';
import { postComment } from '@/api/discussion';
import { DiscussionComment } from '@shared/types/discussion';
const props = defineProps<{
parentId: number,
}>();
const emit = defineEmits(['commentPosted']);
const commentText = ref('');
const error = ref('');
async function submitComment(e: Event) {
e.preventDefault();
error.value = '';
if (!commentText.value.trim()) {
error.value = 'Comment cannot be empty';
return;
}
let newComment: DiscussionComment = { post_id: props.parentId, content: commentText.value.trim() };
await postComment(newComment);
emit('commentPosted');
commentText.value = '';
}
</script>
<template>
<form @submit="submitComment">
<div class="mb-2">
<label class="block font-medium mb-1">Add a comment</label>
<Textarea rows="3" class="bg-neutral-800 resize-none w-full" v-model="commentText"
placeholder="Add a comment…" />
<div class="h-4 text-red-500 text-sm mt-1" v-if="error">{{ error }}</div>
</div>
<div class="mt-2 flex justify-end">
<Button type="submit" :disabled="!commentText.trim()">Post Comment</Button>
</div>
</form>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { DiscussionComment } from '@shared/types/discussion';
import DiscussionCommentView from './DiscussionCommentView.vue';
import CommentForm from './CommentForm.vue';
const props = defineProps<{
parentId: number;
comments: DiscussionComment[];
}>();
function onCommentPosted() {
// TODO: Refresh comments if needed
}
</script>
<template>
<div class="mt-10 *:my-5">
<h3 class="text-xl font-semibold">Discussion</h3>
<div class="comment-thread">
<div class="comments-list flex flex-col gap-5">
<div v-for="comment in comments" :key="comment.id" class="comment">
<DiscussionCommentView :comment="comment"></DiscussionCommentView>
</div>
</div>
<CommentForm :parent-id="props.parentId" @commentPosted="onCommentPosted" />
</div>
</div>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { DiscussionComment } from '@shared/types/discussion';
import MemberCard from '../members/MemberCard.vue';
const props = defineProps<{
comment: DiscussionComment;
}>();
</script>
<template>
<div class="rounded-md border border-neutral-800 p-3 space-y-5">
<!-- Comment header -->
<div class="flex justify-between">
<MemberCard :member-id="comment.poster_id"></MemberCard>
<p class="text-muted-foreground">{{ new Date(comment.created_at).toLocaleString("EN-us", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }}</p>
</div>
<p>{{ comment.content }}</p>
</div>
</template>

View File

@@ -6,6 +6,7 @@
import { getModRequest } from '@/api/modRequests';
import Button from '@/components/ui/button/Button.vue';
import Badge from '@/components/ui/badge/Badge.vue';
import CommentThread from '../discussion/CommentThread.vue';
const route = useRoute();
const router = useRouter();
@@ -41,7 +42,7 @@
<div class="max-w-[80rem] mt-5 mx-auto px-2 lg:px-20 w-full">
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="scroll-m-20 text-3xl font-semibold tracking-tight mb-2">Mod Request: {{ post.title }}</h1>
<h1 class="scroll-m-20 text-3xl font-semibold tracking-tight mb-2">Mod Request: {{ post?.title }}</h1>
<p class="text-muted-foreground" v-if="post">
Requested by {{ post.poster_name || 'Unknown' }} on {{ new
Date(post.created_at).toLocaleDateString() }}
@@ -95,10 +96,8 @@
</div>
<!-- discussion placeholder -->
<div class="mt-10">
<h3 class="text-xl font-semibold">Discussion</h3>
<p class="text-muted-foreground">Comments and thread will appear here once implemented.</p>
</div>
<CommentThread :parent-id="post.id" :comments="post.comments" />
</div>
<div v-else>
<p class="text-muted-foreground">Unable to locate this mod request.</p>