diff --git a/web/app/solutions/[id]/page.tsx b/web/app/solutions/[id]/page.tsx
index 64e7cc1..02b8b2e 100644
--- a/web/app/solutions/[id]/page.tsx
+++ b/web/app/solutions/[id]/page.tsx
@@ -15,6 +15,7 @@ import NavigateBack from "@/components/NavigateBack";
import FileStructureDisplay from "@/components/FileStructureDisplay";
import QuestionAnswersDisplay from "@/components/solution/questions/QuestionAnswersDisplay";
import {useSpotlightStage2} from "@/hooks/spotlight/stage2";
+import CommentTab from "@/components/solution/CommentTab";
// Every 30s
const REFETCH_INTERVAL = 1000 * 30;
@@ -120,6 +121,7 @@ const SolutionDetailsPage = ({ params }: { params: { id: string } }) => {
Code
>
)}
+ Comments
{solution.assignment.language === AssignmentLanguage.QuestionBased ? (
@@ -144,6 +146,9 @@ const SolutionDetailsPage = ({ params }: { params: { id: string } }) => {
>
)}
+
+
+
{executorModalOpen &&
solution.job !== undefined &&
diff --git a/web/components/solution/CommentTab.tsx b/web/components/solution/CommentTab.tsx
new file mode 100644
index 0000000..8f4b6ad
--- /dev/null
+++ b/web/components/solution/CommentTab.tsx
@@ -0,0 +1,53 @@
+import useApiServiceClient from "@/hooks/useApiServiceClient";
+import useClientQuery from "@/hooks/useClientQuery";
+import { Solution } from "@/service/types/tasky";
+import {Badge, Button, Card, Group, Stack, Title} from "@mantine/core";
+import AssignmentDateDisplay from "@/components/assignments/AssignmentDateDisplay";
+import RichTextDisplay from "@/components/display/RichTextDisplay";
+import useCurrentUser from "@/hooks/useCurrentUser";
+import {IconPlus} from "@tabler/icons-react";
+import {useState} from "react";
+import CreateCommentModal from "@/components/solution/CreateCommentModal";
+
+interface CommentTabProps {
+ solution: Solution;
+}
+
+const CommentTab = ({solution}: CommentTabProps) => {
+
+ const api = useApiServiceClient();
+ const {user} = useCurrentUser();
+ const [createModalOpen, setCreateModalOpen] = useState(false);
+ const [comments, refetch] = useClientQuery(() => api.getCodeComments(solution.id));
+
+ return (
+ <>
+
+
+
+
+ {(comments ?? []).map((comment) => (
+
+
+ {comment.title}
+ {comment.commentor === user?.id && (
+ Your comment
+ )}
+
+
+
+ ))}
+
+ {createModalOpen && (
+ setCreateModalOpen(false)}
+ />
+ )}
+ >
+ );
+}
+
+export default CommentTab;
diff --git a/web/components/solution/CreateCommentModal.tsx b/web/components/solution/CreateCommentModal.tsx
new file mode 100644
index 0000000..be7337c
--- /dev/null
+++ b/web/components/solution/CreateCommentModal.tsx
@@ -0,0 +1,63 @@
+import { Solution } from "@/service/types/tasky";
+import {Button, Divider, Group, Modal, Stack, TextInput} from "@mantine/core";
+import {useForm} from "@mantine/form";
+import RichTextInput from "@/components/form/RichTextInput";
+import useApiServiceClient from "@/hooks/useApiServiceClient";
+import {notifications} from "@mantine/notifications";
+
+
+interface CreateCommentModalProps {
+ solution: Solution;
+ refetch: () => void;
+ onClose: () => void;
+}
+
+const CreateCommentModal = ({solution, refetch, onClose}: CreateCommentModalProps) => {
+
+ const form = useForm({
+ initialValues: {
+ title: '',
+ content: ''
+ }
+ });
+ const api = useApiServiceClient();
+
+ const onSubmit = form.onSubmit(async (values) => {
+ try {
+ await api.createCodeComment(solution.id, values.title, values.content);
+ refetch();
+ onClose();
+ } catch (e: any) {
+ notifications.show({
+ title: 'Error',
+ message: e?.message ?? "Failed to create comment",
+ })
+ }
+ });
+
+ return (
+
+
+
+ );
+}
+
+export default CreateCommentModal;
diff --git a/web/service/ApiService.ts b/web/service/ApiService.ts
index 4b95144..a7bdbde 100644
--- a/web/service/ApiService.ts
+++ b/web/service/ApiService.ts
@@ -15,7 +15,7 @@ import {
Solution,
SolutionFilesResponse,
SolutionsResponse,
- AssignmentWish
+ AssignmentWish, CodeComment
} from "@/service/types/tasky";
import { FileStructureTree } from "@/components/FileStructure";
@@ -219,6 +219,14 @@ class ApiService {
await this.delete(`/tasky/groups/${groupId}/assignment_wishes/${wishId}`);
}
+ public async getCodeComments(solutionId: number): Promise {
+ return await this.get(`/tasky/solutions/${solutionId}/code_comments`);
+ }
+
+ public async createCodeComment(solutionId: number, title: string, content: string): Promise {
+ return await this.post(`/tasky/solutions/${solutionId}/code_comments`, {title, content});
+ }
+
public async createCodeTests(
groupId: number,
assignmentId: number,
diff --git a/web/service/types/tasky.ts b/web/service/types/tasky.ts
index 5fe8ad1..ab4ae99 100644
--- a/web/service/types/tasky.ts
+++ b/web/service/types/tasky.ts
@@ -144,3 +144,10 @@ export interface AssignmentWish {
title: string;
description: string;
}
+
+export interface CodeComment {
+ id: number;
+ title: string;
+ content: string;
+ commentor: number;
+}