From de92010b69489b24af87ad9e775bfda01d7f53ae Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Wed, 30 Oct 2024 17:36:41 +0800 Subject: [PATCH 01/20] feat: update backend routes --- apps/history-service/handlers/create.go | 5 +- .../handlers/createOrUpdate.go | 9 +-- apps/history-service/handlers/delete.go | 3 +- .../handlers/listquestionhistory.go | 70 +++++++++++++++++++ .../handlers/{list.go => listuserhistory.go} | 0 apps/history-service/handlers/read.go | 5 +- apps/history-service/handlers/update.go | 9 +-- apps/history-service/main.go | 27 ++++--- 8 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 apps/history-service/handlers/listquestionhistory.go rename apps/history-service/handlers/{list.go => listuserhistory.go} (100%) diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index d981fb9190..7ce6037eff 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -1,12 +1,13 @@ package handlers import ( - "cloud.google.com/go/firestore" "encoding/json" - "google.golang.org/api/iterator" "history-service/models" "history-service/utils" "net/http" + + "cloud.google.com/go/firestore" + "google.golang.org/api/iterator" ) // Create a new code snippet diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go index f9df4bcc33..6f091eba19 100644 --- a/apps/history-service/handlers/createOrUpdate.go +++ b/apps/history-service/handlers/createOrUpdate.go @@ -1,15 +1,16 @@ package handlers import ( - "cloud.google.com/go/firestore" "encoding/json" + "history-service/models" + "history-service/utils" + "net/http" + + "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "history-service/models" - "history-service/utils" - "net/http" ) func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) { diff --git a/apps/history-service/handlers/delete.go b/apps/history-service/handlers/delete.go index a450b58ea4..f528e4bc34 100644 --- a/apps/history-service/handlers/delete.go +++ b/apps/history-service/handlers/delete.go @@ -1,10 +1,11 @@ package handlers import ( + "net/http" + "github.com/go-chi/chi/v5" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "net/http" ) // Delete a code snippet by ID diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go new file mode 100644 index 0000000000..486f83363a --- /dev/null +++ b/apps/history-service/handlers/listquestionhistory.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "encoding/json" + "history-service/models" + "net/http" + + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" +) + +func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + username := chi.URLParam(r, "username") + + // Reference collection + collRef := s.Client.Collection("collaboration-history") + + // Query data + iterUser := collRef.Where("user", "==", username).Documents(ctx) + iterMatchedUser := collRef.Where("matchedUser", "==", username).Documents(ctx) + + // Map data + var histories []models.CollaborationHistory + for { + doc, err := iterUser.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Failed to get histories for user", http.StatusInternalServerError) + return + } + + var history models.CollaborationHistory + if err := doc.DataTo(&history); err != nil { + http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) + return + } + history.MatchID = doc.Ref.ID + + histories = append(histories, history) + } + + for { + doc, err := iterMatchedUser.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Failed to get histories for matched user", http.StatusInternalServerError) + return + } + + var history models.CollaborationHistory + if err := doc.DataTo(&history); err != nil { + http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) + return + } + history.MatchID = doc.Ref.ID + + histories = append(histories, history) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(histories) +} diff --git a/apps/history-service/handlers/list.go b/apps/history-service/handlers/listuserhistory.go similarity index 100% rename from apps/history-service/handlers/list.go rename to apps/history-service/handlers/listuserhistory.go diff --git a/apps/history-service/handlers/read.go b/apps/history-service/handlers/read.go index fd97c5d031..d7c6ea23a0 100644 --- a/apps/history-service/handlers/read.go +++ b/apps/history-service/handlers/read.go @@ -2,10 +2,11 @@ package handlers import ( "encoding/json" - "github.com/go-chi/chi/v5" - "google.golang.org/api/iterator" "history-service/models" "net/http" + + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" ) // Read a code snippet by ID diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go index b6cb953709..401953a472 100644 --- a/apps/history-service/handlers/update.go +++ b/apps/history-service/handlers/update.go @@ -1,15 +1,16 @@ package handlers import ( - "cloud.google.com/go/firestore" "encoding/json" + "history-service/models" + "history-service/utils" + "net/http" + + "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "history-service/models" - "history-service/utils" - "net/http" ) // Update an existing code snippet diff --git a/apps/history-service/main.go b/apps/history-service/main.go index cf69c934d2..17194b9f49 100644 --- a/apps/history-service/main.go +++ b/apps/history-service/main.go @@ -1,20 +1,21 @@ package main import ( - "cloud.google.com/go/firestore" "context" - firebase "firebase.google.com/go/v4" "fmt" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" - "github.com/joho/godotenv" - "google.golang.org/api/option" "history-service/handlers" "log" "net/http" "os" "time" + + "cloud.google.com/go/firestore" + firebase "firebase.google.com/go/v4" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + "google.golang.org/api/option" ) func main() { @@ -75,14 +76,10 @@ func initChiRouter(service *handlers.Service) *chi.Mux { func registerRoutes(r *chi.Mux, service *handlers.Service) { r.Route("/histories", func(r chi.Router) { - r.Get("/{username}", service.ListUserHistories) - //r.Post("/", service.CreateHistory) - - r.Route("/{matchId}", func(r chi.Router) { - r.Put("/", service.CreateOrUpdateHistory) - r.Get("/", service.ReadHistory) - //r.Put("/", service.UpdateHistory) - //r.Delete("/", service.DeleteHistory) + r.Post("/", service.CreateHistory) + r.Route("/{username}", func(r chi.Router) { + r.Get("/", service.ListUserHistories) + r.Get("/{questionDocRefId}", service.ListUserQuestionHistories) }) }) } From 8eb66f44d0bd4d10befcf024d1c13745ab8f32a7 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 04:34:44 +0800 Subject: [PATCH 02/20] feat: update frontend for question page --- .../src/app/collaboration/[id]/page.tsx | 68 +---- .../src/app/collaboration/[id]/styles.scss | 33 -- apps/frontend/src/app/question/[id]/page.tsx | 234 ++++++--------- .../src/app/question/[id]/styles.scss | 284 +++++++----------- .../QuestionDetail/QuestionDetail.tsx | 42 +++ .../question/QuestionDetail/styles.scss | 30 ++ .../QuestionDetailFull/QuestionDetailFull.tsx | 35 +++ .../question/QuestionDetailFull/styles.scss | 9 + .../TestcaseDetail/TestcaseDetail.tsx | 60 ++++ .../question/TestcaseDetail/styles.scss | 24 ++ 10 files changed, 404 insertions(+), 415 deletions(-) create mode 100644 apps/frontend/src/components/question/QuestionDetail/QuestionDetail.tsx create mode 100644 apps/frontend/src/components/question/QuestionDetail/styles.scss create mode 100644 apps/frontend/src/components/question/QuestionDetailFull/QuestionDetailFull.tsx create mode 100644 apps/frontend/src/components/question/QuestionDetailFull/styles.scss create mode 100644 apps/frontend/src/components/question/TestcaseDetail/TestcaseDetail.tsx create mode 100644 apps/frontend/src/components/question/TestcaseDetail/styles.scss diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 41c56023f0..ee538edbf2 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -8,33 +8,26 @@ import { Modal, message, Row, - Select, - Tabs, TabsProps, Tag, - Typography, } from "antd"; import { Content } from "antd/es/layout/layout"; import "./styles.scss"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { GetSingleQuestion, Question } from "@/app/services/question"; import { ClockCircleOutlined, CodeOutlined, - FileDoneOutlined, - InfoCircleFilled, MessageOutlined, - PlayCircleOutlined, SendOutlined, } from "@ant-design/icons"; -import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; import CollaborativeEditor, { CollaborativeEditorHandle, } from "@/components/CollaborativeEditor/CollaborativeEditor"; import { CreateOrUpdateHistory } from "@/app/services/history"; -import { Language } from "@codemirror/language"; import { WebrtcProvider } from "y-webrtc"; +import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; interface CollaborationProps {} @@ -324,55 +317,14 @@ export default function CollaborationPage(props: CollaborationProps) { - -
-
{questionTitle}
-
- - {complexity && - complexity.charAt(0).toUpperCase() + complexity.slice(1)} - -
-
- Topics: - {categories.map((category) => ( - {category} - ))} -
-
{description}
-
-
- -
-
-
- - Test Cases -
- {/* TODO: Link to execution service for running code against test-cases */} - -
-
- -
-
-
+ diff --git a/apps/frontend/src/app/collaboration/[id]/styles.scss b/apps/frontend/src/app/collaboration/[id]/styles.scss index 4f7b068e42..116d6a3c2e 100644 --- a/apps/frontend/src/app/collaboration/[id]/styles.scss +++ b/apps/frontend/src/app/collaboration/[id]/styles.scss @@ -21,16 +21,6 @@ padding: 0rem 1rem; } -.question-row { - height: 60%; - padding: 1rem 0.25rem 0.25rem; -} - -.test-row { - height: 40%; - padding: 0.25rem; -} - .code-row { height: 100%; padding: 1rem 0.25rem 0.25rem; @@ -46,20 +36,6 @@ padding: 0.25rem; } -.question-container { - border: 2px solid #463f3a; - border-radius: 10px; - width: 100%; - padding: 1rem; -} - -.test-container { - border: 2px solid #463f3a; - border-radius: 10px; - width: 100%; - padding: 1rem; -} - .test-top-container, .code-top-container, .session-top-container { @@ -156,15 +132,6 @@ font-weight: bold; } -.question-description { - text-align: justify; - text-justify: inter-word; - line-height: 1.3; - margin-top: 1rem; - max-height: 270px; - overflow-y: scroll; -} - .title-icons { margin-right: 4px; } diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 30926fb169..64fc351c85 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -1,25 +1,34 @@ "use client"; import Header from "@/components/Header/header"; -import { Button, Col, Layout, message, Row, Tag, Select } from "antd"; +import { + Button, + Col, + Layout, + message, + Row, + Tag, + Select, + Table, + Input, +} from "antd"; import { Content } from "antd/es/layout/layout"; import { - PlusCircleOutlined, LeftOutlined, RightOutlined, CaretRightOutlined, - ClockCircleOutlined, - CommentOutlined, - CheckCircleOutlined, + CodeOutlined, + SendOutlined, + HistoryOutlined, } from "@ant-design/icons"; import "./styles.scss"; -import { useEffect, useState, useLayoutEffect } from "react"; +import { useEffect, useState } from "react"; import { GetSingleQuestion } from "../../services/question"; import React from "react"; import TextArea from "antd/es/input/TextArea"; import { useSearchParams } from "next/navigation"; import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; -import { ValidateUser, VerifyTokenResponseType } from "../../services/user"; import { useRouter } from "next/navigation"; +import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; export default function QuestionPage() { const [isLoading, setIsLoading] = useState(true); // Store the states related to table's loading @@ -47,160 +56,103 @@ export default function QuestionPage() { const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); const [selectedItem, setSelectedItem] = useState("python"); // State to hold the selected language item - + // When code editor page is initialised, fetch the particular question, and display in code editor useEffect(() => { if (!isLoading) { setIsLoading(true); } - GetSingleQuestion(docRefId).then((data: any) => { - setQuestionTitle(data.title); - setComplexity(data.complexity); - setCategories(data.categories); - setDescription(data.description); - }); + GetSingleQuestion(docRefId) + .then((data: any) => { + setQuestionTitle(data.title); + setComplexity(data.complexity); + setCategories(data.categories); + setDescription(data.description); + }) + .finally(() => { + setIsLoading(false); + }); }, [docRefId]); + // TODO: retrieve history + const history: any[] = []; + + const columns = [ + { + title: "Id", + dataIndex: "id", + key: "id", + }, + { + title: "Attempted at", + dataIndex: "attemptedAt", + key: "attemptedAt", + }, + { + title: "Language", + dataIndex: "language", + key: "language", + }, + { + title: "Matched with", + dataIndex: "matchedUser", + key: "matchedUser", + }, + ]; + return (
{contextHolder} - +
- - - - -
-
-

- {questionTitle} -

- - Solved  - - -
-
- - {complexity && - complexity.charAt(0).toUpperCase() + - complexity.slice(1)} - -
-
- Topics: - {categories.map((category) => ( - {category} - ))} -
-
- {description} -
-
-
- -
-
-

Testcases

- -
-
- - - -
-
- +
+ - - - -
-
-

- -  Session Details -

- -
-
-
- Start Time: - 01:23:45 -
- - Session Duration:{" "} - - 01:23:45 -
- Matched with: - John Doe + +
+
+
+ + Submitted Code
-
-
- -
-
-

- -  Chat -

+ {/* TODO: set value of code, refactor to look like collab editor but not editable */} +
+
diff --git a/apps/frontend/src/app/question/[id]/styles.scss b/apps/frontend/src/app/question/[id]/styles.scss index 764ed1f0c3..a0692e044c 100644 --- a/apps/frontend/src/app/question/[id]/styles.scss +++ b/apps/frontend/src/app/question/[id]/styles.scss @@ -1,247 +1,165 @@ -// start of code editor classes -.code-editor-layout { - background: white !important; - height: 97vh; - overflow: hidden; -} - -.code-editor-content { +.question-layout { + background: white; display: flex; flex-direction: column; + height: 100vh; } -.entire-page { +.question-content { + background: white; flex: 1; - padding: 1rem; -} - -.problem-description { - height: 70%; -} - -.code-editor { - height: 100.5% !important; -} - -.test-cases { - height: 30%; -} - -.session-details { - height: 20%; + overflow-y: auto; } -.chat-box { - height: 80%; +.first-col, +.second-col { + height: 90vh; } -.boxes { - border-radius: 10px; - border: solid; - margin: 4px; - padding: 5px; -} - -.problem-description-info { - width: 100%; - height: 100%; - overflow: auto; - margin-left: 5px; - padding: 8px; +.question-row { + padding: 0rem 1rem; } -.problem-description-top { - display: inline-flex; - flex-wrap: nowrap; - width: 100%; +.history-row { + height: 40%; + padding: 1rem 0.25rem 0.25rem; } -.problem-description-title { - padding: 0%; - margin: 0%; +.code-row { + height: 60%; + padding: 1rem 0.25rem 0.25rem; } -.problem-solve-status { - margin-left: auto; +.session-row { + height: 20%; + padding: 1rem 0.25rem 0.25rem; } -.test-cases-div { - height: 100%; - width: 100%; +.test-top-container, +.code-top-container, +.history-top-container, +.session-top-container { display: flex; - flex-direction: column; - padding: 8px; -} - -.test-cases-top { - display: inline-flex; - flex-wrap: nowrap; - margin-bottom: 5px; -} - -.testcase-title { - padding: 0%; - padding-left: 1%; - margin: 0%; + justify-content: space-between; } -.runtestcases-button { - margin-left: auto; -} - -.testcase-buttons { +.history-container, +.code-container { + border: 2px solid #463f3a; + border-radius: 10px; width: 100%; - padding-left: 1%; - display: inline-flex; - gap: 5px; + padding: 1rem; } -.testcase-code-div { - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: flex-end; - margin-top: 10px; +.history-language { + margin: auto 8px auto 0px; } -.testcase-code { - // max-height: 100%; - // height: 100%; - // padding-top: 4px; - // padding-left: 1%; - // flex:1; - height: 100%; - flex: 1 1 auto; +.language-select { + width: 120px; } -// .code-editor-box { -// height: 100% !important; -// flex-direction: column; -// display: flex; -// margin-bottom: 4px !important; -// } - -.code-editor-div { - height: 100%; +.session-container { + border: 2px solid #463f3a; + border-radius: 10px; width: 100%; - display: flex; - flex-direction: column; - padding: 8px; -} - -.code-editor-title { - padding: 0%; - padding-left: 1%; - margin: 0%; -} - -.code-editor-code-div { - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: flex-end; - margin-top: 10px; -} - -.code-editor-code { - height: 100%; - flex: 1 1 auto; -} - -.code-editor-top { - display: inline-flex; - flex-wrap: nowrap; + padding: 1rem; } -.language-select { - display: inline-flex; - flex-wrap: nowrap; - margin-top: 4px; +.chat-container { + border: 2px solid #463f3a; + border-radius: 10px; + width: 100%; + padding: 1rem; } -.submit-solution-button { - margin-left: auto; +.chat-message-box { + margin-top: 1rem; + height: 365px; + border: 1px solid #d9d9d9; + border-radius: 6px; + overflow-y: scroll; } -.select-language-button { - width: max-content; +.chat-header-message { + font-size: 14px; + color: #c6c6c6; + margin: 3px auto; + text-align: center; } -.session-details-title { - padding: 0%; - padding-left: 1%; - margin: 0%; +.chat-typing-box { + margin-top: 1rem; } -.session-details-top { - display: inline-flex; - flex-wrap: nowrap; +.question-title, +.test-title, +.code-title, +.history-title, +.session-title { + font-size: 16px; + font-weight: bold; } -.session-details-div { - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - padding: 8px; +.question-difficulty, +.question-topic { + margin: 4px 0px; } -.end-session-button { - margin-left: auto; +.session-duration, +.session-matched-user-label { + margin: 4px 0px; } -.session-details-text-div { - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: flex-end; - margin-top: 10px; +.session-duration-timer { + font-weight: normal; + margin-left: 8px; } -.session-details-text { - height: 100%; - flex: 1 1 auto; - margin-left: 5px; +.session-matched-user-name { + color: #e0afa0; + margin-left: 8px; } -.chat-box-div { - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - padding: 8px; +.topic-label, +.session-duration, +.session-matched-user-label { + font-size: 14px; + font-weight: bold; } -.chat-box-top { - display: inline-flex; - flex-wrap: nowrap; +.title-icons { + margin-right: 4px; } -.chat-box-title { - padding: 0%; - padding-left: 1%; - margin: 0%; +.session-end-button, +.test-case-button { + width: fit-content; } -.complexity-div { - margin: 4px 0; +.modal-description { + margin-bottom: 2rem; } -.topic-label, -.language-text { +.session-modal-question, +.session-modal-difficulty, +.session-modal-duration, +.session-modal-matched-user { font-weight: bold; } -.tag-container { - margin: 4px 0; +.session-modal-time, +.session-modal-title { + font-weight: normal; +} +.session-modal-matched-user-name { + color: #e0afa0; } -.description-text { - text-align: justify; - text-justify: inter-word; - line-height: 1.3; +.info-modal-icon { + color: red; } -.session-headers { - font-weight: bold; +.code-viewer { + resize: none; } diff --git a/apps/frontend/src/components/question/QuestionDetail/QuestionDetail.tsx b/apps/frontend/src/components/question/QuestionDetail/QuestionDetail.tsx new file mode 100644 index 0000000000..8d679bd53e --- /dev/null +++ b/apps/frontend/src/components/question/QuestionDetail/QuestionDetail.tsx @@ -0,0 +1,42 @@ +import { Tag } from "antd"; +import "./styles.scss"; + +interface QuestionDetailProps { + questionTitle?: string; + complexity?: string; + categories?: string[]; + description?: string; +} + +// Returns the html of question details (without testcase information) +export const QuestionDetail = (props: QuestionDetailProps) => { + return ( +
+
{props.questionTitle}
+
+ + {props.complexity && + props.complexity.charAt(0).toUpperCase() + + props.complexity.slice(1)} + +
+
+ Topics: + {props.categories?.map((category) => ( + {category} + ))} +
+
{props.description}
+
+ ); +}; diff --git a/apps/frontend/src/components/question/QuestionDetail/styles.scss b/apps/frontend/src/components/question/QuestionDetail/styles.scss new file mode 100644 index 0000000000..efcc62ee02 --- /dev/null +++ b/apps/frontend/src/components/question/QuestionDetail/styles.scss @@ -0,0 +1,30 @@ +.question-container { + border: 2px solid #463f3a; + border-radius: 10px; + width: 100%; + padding: 1rem; +} + +.question-title { + font-size: 16px; + font-weight: bold; +} + +.question-difficulty, +.question-topic { + margin: 4px 0px; +} + +.topic-label { + font-size: 14px; + font-weight: bold; +} + +.question-description { + text-align: justify; + text-justify: inter-word; + line-height: 1.3; + margin-top: 1rem; + max-height: 270px; + overflow-y: scroll; +} diff --git a/apps/frontend/src/components/question/QuestionDetailFull/QuestionDetailFull.tsx b/apps/frontend/src/components/question/QuestionDetailFull/QuestionDetailFull.tsx new file mode 100644 index 0000000000..8194cf2b40 --- /dev/null +++ b/apps/frontend/src/components/question/QuestionDetailFull/QuestionDetailFull.tsx @@ -0,0 +1,35 @@ +import { Row, TabsProps } from "antd"; +import { QuestionDetail } from "../QuestionDetail/QuestionDetail"; +import { TestcaseDetail } from "../TestcaseDetail/TestcaseDetail"; +import "./styles.scss"; + +// TODO: should add function for test case submission in props +interface QuestionDetailFullProps { + questionTitle?: string; + complexity?: string; + categories?: string[]; + description?: string; + testcaseItems?: TabsProps["items"]; + shouldShowSubmitButton?: boolean; +} + +export const QuestionDetailFull = (props: QuestionDetailFullProps) => { + return ( + <> + + + + + + + + ); +}; diff --git a/apps/frontend/src/components/question/QuestionDetailFull/styles.scss b/apps/frontend/src/components/question/QuestionDetailFull/styles.scss new file mode 100644 index 0000000000..26037db4a6 --- /dev/null +++ b/apps/frontend/src/components/question/QuestionDetailFull/styles.scss @@ -0,0 +1,9 @@ +.question-row { + height: 60%; + padding: 1rem 0.25rem 0.25rem; +} + +.test-row { + height: 40%; + padding: 1rem 0.25rem 0.25rem; +} diff --git a/apps/frontend/src/components/question/TestcaseDetail/TestcaseDetail.tsx b/apps/frontend/src/components/question/TestcaseDetail/TestcaseDetail.tsx new file mode 100644 index 0000000000..022f236e27 --- /dev/null +++ b/apps/frontend/src/components/question/TestcaseDetail/TestcaseDetail.tsx @@ -0,0 +1,60 @@ +import { FileDoneOutlined, PlayCircleOutlined } from "@ant-design/icons"; +import { Button, Input, Tabs, TabsProps } from "antd"; +import "./styles.scss"; + +interface TestcaseDetailProps { + testcaseItems?: TabsProps["items"]; + shouldShowSubmitButton?: boolean; +} + +export const TestcaseDetail = (props: TestcaseDetailProps) => { + // TODO: Tabs component items for testcases + // TODO: Setup test-cases in db for each qn and pull/paste here + const items: TabsProps["items"] = [ + { + key: "1", + label: "Case 1", + children: ( + + ), + }, + { + key: "2", + label: "Case 2", + children: ( + + ), + }, + // { + // key: "3", + // label: "Case 3", + // children: ( + // + // ), + // }, + ]; + + return ( +
+
+
+ + Test Cases +
+ {/* TODO: Link to execution service for running code against test-cases */} + {props.shouldShowSubmitButton && ( + + )} +
+
+ +
+
+ ); +}; diff --git a/apps/frontend/src/components/question/TestcaseDetail/styles.scss b/apps/frontend/src/components/question/TestcaseDetail/styles.scss new file mode 100644 index 0000000000..b3a0d26e9b --- /dev/null +++ b/apps/frontend/src/components/question/TestcaseDetail/styles.scss @@ -0,0 +1,24 @@ +.test-container { + border: 2px solid #463f3a; + border-radius: 10px; + width: 100%; + padding: 1rem; +} + +.test-title { + font-size: 16px; + font-weight: bold; +} + +.test-top-container { + display: flex; + justify-content: space-between; +} + +.test-case-button { + width: fit-content; +} + +.title-icons { + margin-right: 4px; +} From 4a896006c8e24a5cc438068db96a2f166306655f Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 17:47:42 +0800 Subject: [PATCH 03/20] feat: use codemirror editor --- apps/frontend/src/app/question/[id]/page.tsx | 56 ++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 64fc351c85..aee5f80bbf 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -21,7 +21,7 @@ import { HistoryOutlined, } from "@ant-design/icons"; import "./styles.scss"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { GetSingleQuestion } from "../../services/question"; import React from "react"; import TextArea from "antd/es/input/TextArea"; @@ -29,6 +29,13 @@ import { useSearchParams } from "next/navigation"; import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; import { useRouter } from "next/navigation"; import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; +import CollaborativeEditor, { + CollaborativeEditorHandle, +} from "@/components/CollaborativeEditor/CollaborativeEditor"; +import { WebrtcProvider } from "y-webrtc"; +import { Compartment, EditorState } from "@codemirror/state"; +import { basicSetup, EditorView } from "codemirror"; +import { javascript } from "@codemirror/lang-javascript"; export default function QuestionPage() { const [isLoading, setIsLoading] = useState(true); // Store the states related to table's loading @@ -44,6 +51,9 @@ export default function QuestionPage() { }; const router = useRouter(); + const editorRef = useRef(null); + const providerRef = useRef(null); + const languageConf = new Compartment(); // Retrieve the docRefId from query params during page navigation const searchParams = useSearchParams(); @@ -55,7 +65,17 @@ export default function QuestionPage() { const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); - const [selectedItem, setSelectedItem] = useState("python"); // State to hold the selected language item + + const state = EditorState.create({ + doc: "TODO: parse from code", + extensions: [ + basicSetup, + languageConf.of(javascript()), + EditorView.theme({ + "&": { height: "100%", overflow: "hidden" }, // Enable scroll + }), + ], + }); // When code editor page is initialised, fetch the particular question, and display in code editor useEffect(() => { @@ -73,6 +93,16 @@ export default function QuestionPage() { .finally(() => { setIsLoading(false); }); + + const view = new EditorView({ + state, + parent: editorRef.current || undefined, + }); + + return () => { + // Cleanup on component unmount + view.destroy(); + }; }, [docRefId]); // TODO: retrieve history @@ -144,14 +174,22 @@ export default function QuestionPage() { Submitted Code
+ + {/* TODO: add details of attempt here */} {/* TODO: set value of code, refactor to look like collab editor but not editable */} -
- +
From 3b810cf5174dc79a5559d290833064ac0ecfa3e0 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 17:52:53 +0800 Subject: [PATCH 04/20] fix: add token expire check and comment out backend token check request --- apps/frontend/package.json | 1 + apps/frontend/pnpm-lock.yaml | 420 +++++++++++++++++--------------- apps/frontend/src/middleware.ts | 38 ++- 3 files changed, 249 insertions(+), 210 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 94cc9de16b..593c6b7b73 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -20,6 +20,7 @@ "@codemirror/state": "^6.4.1", "antd": "^5.20.6", "codemirror": "^6.0.1", + "jwt-decode": "^4.0.0", "next": "14.2.13", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index c114790857..4db0322c24 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@ant-design/icons': specifier: ^5.5.1 - version: 5.5.1(react-dom@18.2.0)(react@18.2.0) + version: 5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@ant-design/nextjs-registry': specifier: ^1.0.1 - version: 1.0.1(@ant-design/cssinjs@1.21.1)(antd@5.20.6)(next@14.2.13)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.1(@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@codemirror/lang-cpp': specifier: ^6.0.2 version: 6.0.2 @@ -37,13 +37,16 @@ importers: version: 6.4.1 antd: specifier: ^5.20.6 - version: 5.20.6(react-dom@18.2.0)(react@18.2.0) + version: 5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) codemirror: specifier: ^6.0.1 version: 6.0.1(@lezer/common@1.2.3) + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 next: specifier: 14.2.13 - version: 14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2) + version: 14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2) react: specifier: ^18.2.0 version: 18.2.0 @@ -1178,6 +1181,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1962,22 +1969,22 @@ snapshots: dependencies: '@ctrl/tinycolor': 3.6.1 - '@ant-design/cssinjs-utils@1.1.0(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/cssinjs-utils@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@babel/runtime': 7.25.7 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@ant-design/cssinjs@1.21.1(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 classnames: 2.5.1 csstype: 3.1.3 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) stylis: 4.3.4 @@ -1988,21 +1995,21 @@ snapshots: '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons@5.5.1(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/icons@5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@ant-design/colors': 7.1.0 '@ant-design/icons-svg': 4.4.2 '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@ant-design/nextjs-registry@1.0.1(@ant-design/cssinjs@1.21.1)(antd@5.20.6)(next@14.2.13)(react-dom@18.2.0)(react@18.2.0)': + '@ant-design/nextjs-registry@1.0.1(@ant-design/cssinjs@1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) - antd: 5.20.6(react-dom@18.2.0)(react@18.2.0) - next: 14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + antd: 5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next: 14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2242,19 +2249,19 @@ snapshots: dependencies: '@babel/runtime': 7.25.7 - '@rc-component/color-picker@2.0.1(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/color-picker@2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@ant-design/fast-color': 2.0.6 '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/context@1.4.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/context@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2262,48 +2269,48 @@ snapshots: dependencies: '@babel/runtime': 7.25.7 - '@rc-component/mutate-observer@1.1.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/portal@1.1.2(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/portal@1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/qrcode@1.0.0(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/qrcode@1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/tour@1.15.1(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/tour@1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@rc-component/trigger@2.2.3(react-dom@18.2.0)(react@18.2.0)': + '@rc-component/trigger@2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2333,7 +2340,7 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0)(eslint@8.0.0)(typescript@5.0.2)': + '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0)(typescript@5.0.2)': dependencies: '@eslint-community/regexpp': 4.11.1 '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) @@ -2346,6 +2353,7 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2358,6 +2366,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.8.0 debug: 4.3.7 eslint: 8.0.0 + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2373,6 +2382,7 @@ snapshots: '@typescript-eslint/utils': 8.8.0(eslint@8.0.0)(typescript@5.0.2) debug: 4.3.7 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - eslint @@ -2390,6 +2400,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.0.2) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - supports-color @@ -2435,55 +2446,55 @@ snapshots: ansi-styles@6.2.1: {} - antd@5.20.6(react-dom@18.2.0)(react@18.2.0): + antd@5.20.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@ant-design/colors': 7.1.0 - '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0)(react@18.2.0) - '@ant-design/cssinjs-utils': 1.1.0(react-dom@18.2.0)(react@18.2.0) - '@ant-design/icons': 5.5.1(react-dom@18.2.0)(react@18.2.0) + '@ant-design/cssinjs': 1.21.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/cssinjs-utils': 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/icons': 5.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@ant-design/react-slick': 1.1.2(react@18.2.0) '@babel/runtime': 7.25.7 '@ctrl/tinycolor': 3.6.1 - '@rc-component/color-picker': 2.0.1(react-dom@18.2.0)(react@18.2.0) - '@rc-component/mutate-observer': 1.1.0(react-dom@18.2.0)(react@18.2.0) - '@rc-component/qrcode': 1.0.0(react-dom@18.2.0)(react@18.2.0) - '@rc-component/tour': 1.15.1(react-dom@18.2.0)(react@18.2.0) - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/color-picker': 2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/mutate-observer': 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/qrcode': 1.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/tour': 1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 copy-to-clipboard: 3.3.3 dayjs: 1.11.13 - rc-cascader: 3.28.1(react-dom@18.2.0)(react@18.2.0) - rc-checkbox: 3.3.0(react-dom@18.2.0)(react@18.2.0) - rc-collapse: 3.7.3(react-dom@18.2.0)(react@18.2.0) - rc-dialog: 9.5.2(react-dom@18.2.0)(react@18.2.0) - rc-drawer: 7.2.0(react-dom@18.2.0)(react@18.2.0) - rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-field-form: 2.4.0(react-dom@18.2.0)(react@18.2.0) - rc-image: 7.9.0(react-dom@18.2.0)(react@18.2.0) - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-input-number: 9.2.0(react-dom@18.2.0)(react@18.2.0) - rc-mentions: 2.15.0(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-notification: 5.6.2(react-dom@18.2.0)(react@18.2.0) - rc-pagination: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-picker: 4.6.15(dayjs@1.11.13)(react-dom@18.2.0)(react@18.2.0) - rc-progress: 4.0.0(react-dom@18.2.0)(react@18.2.0) - rc-rate: 2.13.0(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-segmented: 2.3.0(react-dom@18.2.0)(react@18.2.0) - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-slider: 11.1.6(react-dom@18.2.0)(react@18.2.0) - rc-steps: 6.0.1(react-dom@18.2.0)(react@18.2.0) - rc-switch: 4.1.0(react-dom@18.2.0)(react@18.2.0) - rc-table: 7.45.7(react-dom@18.2.0)(react@18.2.0) - rc-tabs: 15.1.1(react-dom@18.2.0)(react@18.2.0) - rc-textarea: 1.8.2(react-dom@18.2.0)(react@18.2.0) - rc-tooltip: 6.2.1(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-tree-select: 5.23.0(react-dom@18.2.0)(react@18.2.0) - rc-upload: 4.7.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-cascader: 3.28.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-checkbox: 3.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-collapse: 3.7.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dialog: 9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-drawer: 7.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dropdown: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-field-form: 2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-image: 7.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input-number: 9.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-mentions: 2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-notification: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-pagination: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-picker: 4.6.15(dayjs@1.11.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-progress: 4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-rate: 2.13.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-segmented: 2.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-slider: 11.1.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-steps: 6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-switch: 4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-table: 7.45.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tabs: 15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tooltip: 6.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree-select: 5.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-upload: 4.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scroll-into-view-if-needed: 3.1.0 @@ -2864,15 +2875,16 @@ snapshots: dependencies: '@next/eslint-plugin-next': 14.2.13 '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0)(eslint@8.0.0)(typescript@5.0.2) + '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0)(typescript@5.0.2) '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.0.0) eslint-plugin-react: 7.37.1(eslint@8.0.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.0.0) + optionalDependencies: typescript: 5.0.2 transitivePeerDependencies: - eslint-import-resolver-webpack @@ -2887,38 +2899,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.0.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0): dependencies: - '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.0.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -2927,7 +2940,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.0.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.0.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.0.0)(typescript@5.0.2))(eslint@8.0.0))(eslint@8.0.0))(eslint@8.0.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -2937,6 +2950,8 @@ snapshots: object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.8.0(eslint@8.0.0)(typescript@5.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3379,6 +3394,8 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + jwt-decode@4.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3431,7 +3448,7 @@ snapshots: natural-compare@1.4.0: {} - next@14.2.13(react-dom@18.2.0)(react@18.2.0)(sass@1.79.2): + next@14.2.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.79.2): dependencies: '@next/env': 14.2.13 '@swc/helpers': 0.5.5 @@ -3441,7 +3458,6 @@ snapshots: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - sass: 1.79.2 styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: '@next/swc-darwin-arm64': 14.2.13 @@ -3453,6 +3469,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.13 '@next/swc-win32-ia32-msvc': 14.2.13 '@next/swc-win32-x64-msvc': 14.2.13 + sass: 1.79.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -3558,321 +3575,322 @@ snapshots: dependencies: safe-buffer: 5.2.1 - rc-cascader@3.28.1(react-dom@18.2.0)(react@18.2.0): + rc-cascader@3.28.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 array-tree-filter: 2.1.0 classnames: 2.5.1 - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-checkbox@3.3.0(react-dom@18.2.0)(react@18.2.0): + rc-checkbox@3.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-collapse@3.7.3(react-dom@18.2.0)(react@18.2.0): + rc-collapse@3.7.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-dialog@9.5.2(react-dom@18.2.0)(react@18.2.0): + rc-dialog@9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-drawer@7.2.0(react-dom@18.2.0)(react@18.2.0): + rc-drawer@7.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-dropdown@4.2.0(react-dom@18.2.0)(react@18.2.0): + rc-dropdown@4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-field-form@2.4.0(react-dom@18.2.0)(react@18.2.0): + rc-field-form@2.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 '@rc-component/async-validator': 5.0.4 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-image@7.9.0(react-dom@18.2.0)(react@18.2.0): + rc-image@7.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-dialog: 9.5.2(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-dialog: 9.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-input-number@9.2.0(react-dom@18.2.0)(react@18.2.0): + rc-input-number@9.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 '@rc-component/mini-decimal': 1.1.0 classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-input@1.6.3(react-dom@18.2.0)(react@18.2.0): + rc-input@1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-mentions@2.15.0(react-dom@18.2.0)(react@18.2.0): + rc-mentions@2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-textarea: 1.8.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-menu@9.14.1(react-dom@18.2.0)(react@18.2.0): + rc-menu@9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-motion@2.9.3(react-dom@18.2.0)(react@18.2.0): + rc-motion@2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-notification@5.6.2(react-dom@18.2.0)(react@18.2.0): + rc-notification@5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-overflow@1.3.2(react-dom@18.2.0)(react@18.2.0): + rc-overflow@1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-pagination@4.2.0(react-dom@18.2.0)(react@18.2.0): + rc-pagination@4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-picker@4.6.15(dayjs@1.11.13)(react-dom@18.2.0)(react@18.2.0): + rc-picker@4.6.15(dayjs@1.11.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - dayjs: 1.11.13 - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + dayjs: 1.11.13 - rc-progress@4.0.0(react-dom@18.2.0)(react@18.2.0): + rc-progress@4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-rate@2.13.0(react-dom@18.2.0)(react@18.2.0): + rc-rate@2.13.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-resize-observer@1.4.0(react-dom@18.2.0)(react@18.2.0): + rc-resize-observer@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) resize-observer-polyfill: 1.5.1 - rc-segmented@2.3.0(react-dom@18.2.0)(react@18.2.0): + rc-segmented@2.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-select@14.15.2(react-dom@18.2.0)(react@18.2.0): + rc-select@14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-slider@11.1.6(react-dom@18.2.0)(react@18.2.0): + rc-slider@11.1.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-steps@6.0.1(react-dom@18.2.0)(react@18.2.0): + rc-steps@6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-switch@4.1.0(react-dom@18.2.0)(react@18.2.0): + rc-switch@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-table@7.45.7(react-dom@18.2.0)(react@18.2.0): + rc-table@7.45.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/context': 1.4.0(react-dom@18.2.0)(react@18.2.0) + '@rc-component/context': 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tabs@15.1.1(react-dom@18.2.0)(react@18.2.0): + rc-tabs@15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0) - rc-menu: 9.14.1(react-dom@18.2.0)(react@18.2.0) - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-dropdown: 4.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.14.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-textarea@1.8.2(react-dom@18.2.0)(react@18.2.0): + rc-textarea@1.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-input: 1.6.3(react-dom@18.2.0)(react@18.2.0) - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-input: 1.6.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tooltip@6.2.1(react-dom@18.2.0)(react@18.2.0): + rc-tooltip@6.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 - '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + '@rc-component/trigger': 2.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classnames: 2.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tree-select@5.23.0(react-dom@18.2.0)(react@18.2.0): + rc-tree-select@5.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-select: 14.15.2(react-dom@18.2.0)(react@18.2.0) - rc-tree: 5.9.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-select: 14.15.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-tree@5.9.0(react-dom@18.2.0)(react@18.2.0): + rc-tree@5.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) - rc-virtual-list: 3.14.8(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-upload@4.7.0(react-dom@18.2.0)(react@18.2.0): + rc-upload@4.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - rc-util@5.43.0(react-dom@18.2.0)(react@18.2.0): + rc-util@5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 18.3.1 - rc-virtual-list@3.14.8(react-dom@18.2.0)(react@18.2.0): + rc-virtual-list@3.14.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.7 classnames: 2.5.1 - rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) - rc-util: 5.43.0(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.43.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index 5ea9b2212a..210871af99 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -1,5 +1,6 @@ -import { NextURL } from 'next/dist/server/web/next-url'; -import { type NextRequest, NextResponse } from 'next/server'; +import { NextURL } from "next/dist/server/web/next-url"; +import { type NextRequest, NextResponse } from "next/server"; +import { jwtDecode } from "jwt-decode"; const PUBLIC_ROUTES = ["/login", "/register"]; @@ -17,24 +18,44 @@ async function isValidToken(TOKEN: string): Promise { return status === 200; } +function isTokenExpired(token: string) { + if (!token) return true; + + try { + const decodedToken = jwtDecode(token); + const currentTime = Date.now() / 1000; // Current time in seconds + return decodedToken?.exp != undefined && decodedToken.exp < currentTime; + } catch (error) { + return true; // Return true if token is invalid + } +} + export default async function middleware(request: NextRequest) { - const REDIRECT_TO_LOGIN = NextResponse.redirect(new NextURL("/login", request.url)); + const REDIRECT_TO_LOGIN = NextResponse.redirect( + new NextURL("/login", request.url) + ); const TOKEN = request.cookies.get("TOKEN"); if (TOKEN == undefined) { return REDIRECT_TO_LOGIN; } - - if (!await isValidToken(TOKEN.value)) { + + if (isTokenExpired(TOKEN.value)) { REDIRECT_TO_LOGIN.cookies.delete("TOKEN"); return REDIRECT_TO_LOGIN; } + // FIXME: isValidToken check leads to error: not being able to access user service. + // if (!(await isValidToken(TOKEN.value))) { + // REDIRECT_TO_LOGIN.cookies.delete("TOKEN"); + // return REDIRECT_TO_LOGIN; + // } + return NextResponse.next(); - } export const config = { - matcher: "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|login|register).*)", + matcher: + "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|login|register).*)", // matcher: [ // "/matching", // "/", @@ -42,5 +63,4 @@ export const config = { // "/question", // "/question/.*", // ], -} - +}; From 548443867a0ae0a33c7a3c7099750a2246a97a78 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 19:06:41 +0800 Subject: [PATCH 05/20] feat; update history service model --- apps/history-service/handlers/create.go | 4 ++-- .../handlers/createOrUpdate.go | 4 ++-- .../handlers/listquestionhistory.go | 4 ++-- .../handlers/listuserhistory.go | 9 ++++---- apps/history-service/handlers/read.go | 6 +++--- apps/history-service/handlers/update.go | 2 +- apps/history-service/main.go | 1 + apps/history-service/models/models.go | 21 ++++++++++++------- apps/history-service/models/pagination.go | 10 +++++++++ 9 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 apps/history-service/models/pagination.go diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index 7ce6037eff..1dd7e6efd8 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -22,7 +22,7 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { } // Document reference ID in firestore mapped to the match ID in model - docRef := s.Client.Collection("collaboration-history").Doc(collaborationHistory.MatchID) + docRef := s.Client.Collection("collaboration-history").Doc(collaborationHistory.HistoryDocRefID) _, err := docRef.Set(ctx, map[string]interface{}{ "title": collaborationHistory.Title, @@ -58,7 +58,7 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to map history data", http.StatusInternalServerError) return } - collaborationHistory.MatchID = doc.Ref.ID + collaborationHistory.HistoryDocRefID = doc.Ref.ID w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go index 6f091eba19..1c3ba5d352 100644 --- a/apps/history-service/handlers/createOrUpdate.go +++ b/apps/history-service/handlers/createOrUpdate.go @@ -66,7 +66,7 @@ func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) http.Error(w, "Failed to map history data", http.StatusInternalServerError) return } - collaborationHistory.MatchID = doc.Ref.ID + collaborationHistory.HistoryDocRefID = doc.Ref.ID w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -118,7 +118,7 @@ func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) http.Error(w, "Failed to map history data", http.StatusInternalServerError) return } - collaborationHistory.MatchID = doc.Ref.ID + collaborationHistory.HistoryDocRefID = doc.Ref.ID w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go index 486f83363a..6c8ee3f62f 100644 --- a/apps/history-service/handlers/listquestionhistory.go +++ b/apps/history-service/handlers/listquestionhistory.go @@ -39,7 +39,7 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) return } - history.MatchID = doc.Ref.ID + history.HistoryDocRefID = doc.Ref.ID histories = append(histories, history) } @@ -59,7 +59,7 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) return } - history.MatchID = doc.Ref.ID + history.HistoryDocRefID = doc.Ref.ID histories = append(histories, history) } diff --git a/apps/history-service/handlers/listuserhistory.go b/apps/history-service/handlers/listuserhistory.go index 554dc159ca..712c649020 100644 --- a/apps/history-service/handlers/listuserhistory.go +++ b/apps/history-service/handlers/listuserhistory.go @@ -2,10 +2,11 @@ package handlers import ( "encoding/json" - "github.com/go-chi/chi/v5" - "google.golang.org/api/iterator" "history-service/models" "net/http" + + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" ) func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { @@ -38,7 +39,7 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) return } - history.MatchID = doc.Ref.ID + history.HistoryDocRefID = doc.Ref.ID histories = append(histories, history) } @@ -58,7 +59,7 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) return } - history.MatchID = doc.Ref.ID + history.HistoryDocRefID = doc.Ref.ID histories = append(histories, history) } diff --git a/apps/history-service/handlers/read.go b/apps/history-service/handlers/read.go index d7c6ea23a0..f986393919 100644 --- a/apps/history-service/handlers/read.go +++ b/apps/history-service/handlers/read.go @@ -13,10 +13,10 @@ import ( func (s *Service) ReadHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - matchId := chi.URLParam(r, "matchId") + historyDocRefId := chi.URLParam(r, "historyDocRefId") // Reference document - docRef := s.Client.Collection("collaboration-history").Doc(matchId) + docRef := s.Client.Collection("collaboration-history").Doc(historyDocRefId) // Get data doc, err := docRef.Get(ctx) @@ -35,7 +35,7 @@ func (s *Service) ReadHistory(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to map history data", http.StatusInternalServerError) return } - collaborationHistory.MatchID = doc.Ref.ID + collaborationHistory.HistoryDocRefID = doc.Ref.ID w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go index 401953a472..f91d9794e9 100644 --- a/apps/history-service/handlers/update.go +++ b/apps/history-service/handlers/update.go @@ -69,7 +69,7 @@ func (s *Service) UpdateHistory(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to map history data", http.StatusInternalServerError) return } - updatedHistory.MatchID = doc.Ref.ID + updatedHistory.HistoryDocRefID = doc.Ref.ID w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/apps/history-service/main.go b/apps/history-service/main.go index 17194b9f49..f57f9663ea 100644 --- a/apps/history-service/main.go +++ b/apps/history-service/main.go @@ -77,6 +77,7 @@ func initChiRouter(service *handlers.Service) *chi.Mux { func registerRoutes(r *chi.Mux, service *handlers.Service) { r.Route("/histories", func(r chi.Router) { r.Post("/", service.CreateHistory) + r.Get("/{historyDocRefId}", service.ReadHistory) r.Route("/{username}", func(r chi.Router) { r.Get("/", service.ListUserHistories) r.Get("/{questionDocRefId}", service.ListUserQuestionHistories) diff --git a/apps/history-service/models/models.go b/apps/history-service/models/models.go index 9928b8809b..b8cc571593 100644 --- a/apps/history-service/models/models.go +++ b/apps/history-service/models/models.go @@ -3,18 +3,23 @@ package models import "time" type CollaborationHistory struct { + // Submission related details + Code string `json:"code" firestore:"code"` + Language string `json:"language" firestore:"language"` + + // Match related details + User string `json:"user" firestore:"user"` + MatchedUser string `json:"matchedUser" firestore:"matchedUser"` + MatchedTopics []string `json:"matchedTopics" firestore:"matchedTopics"` + + // Question related details Title string `json:"title" firestore:"title"` - Code string `json:"code" firestore:"code"` - Language string `json:"language" firestore:"language"` - User string `json:"user" firestore:"user"` - MatchedUser string `json:"matchedUser" firestore:"matchedUser"` - MatchID string `json:"matchId" firestore:"matchId"` - MatchedTopics []string `json:"matchedTopics" firestore:"matchedTopics"` QuestionDocRefID string `json:"questionDocRefId" firestore:"questionDocRefId"` QuestionDifficulty string `json:"questionDifficulty" firestore:"questionDifficulty"` QuestionTopics []string `json:"questionTopics" firestore:"questionTopics"` // Special DB fields - CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` - UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` + CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` // updatedAt is unused as history is never updated once created + HistoryDocRefID string `json:"historyDocRefId"` } diff --git a/apps/history-service/models/pagination.go b/apps/history-service/models/pagination.go new file mode 100644 index 0000000000..a5a74ba7ea --- /dev/null +++ b/apps/history-service/models/pagination.go @@ -0,0 +1,10 @@ +package models + +type HistoriesResponse struct { + TotalCount int `json:"totalCount"` + TotalPages int `json:"totalPages"` + CurrentPage int `json:"currentPage"` + Limit int `json:"limit"` + HasNextPage bool `json:"hasNextPage"` + Questions []CollaborationHistory `json:"histories"` +} From 4297c31cb3eb7870965b95b7b717cc495fc9c141 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 20:52:53 +0800 Subject: [PATCH 06/20] feat: fetch from backend for a submission --- apps/frontend/src/app/question/[id]/page.tsx | 82 ++++++++-- .../src/app/question/[id]/styles.scss | 31 +--- apps/frontend/src/app/services/history.ts | 150 ++++++++++-------- .../handlers/listquestionhistory.go | 5 +- apps/history-service/main.go | 6 +- 5 files changed, 166 insertions(+), 108 deletions(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index aee5f80bbf..23616cd404 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -36,6 +36,15 @@ import { WebrtcProvider } from "y-webrtc"; import { Compartment, EditorState } from "@codemirror/state"; import { basicSetup, EditorView } from "codemirror"; import { javascript } from "@codemirror/lang-javascript"; +import { GetHistory, GetUserQuestionHistories } from "@/app/services/history"; +import { ValidateUser, VerifyTokenResponseType } from "@/app/services/user"; + +interface Submission { + submittedAt: string; + language: string; + matchedUser: string; + code: string; +} export default function QuestionPage() { const [isLoading, setIsLoading] = useState(true); // Store the states related to table's loading @@ -52,7 +61,6 @@ export default function QuestionPage() { const router = useRouter(); const editorRef = useRef(null); - const providerRef = useRef(null); const languageConf = new Compartment(); // Retrieve the docRefId from query params during page navigation @@ -65,9 +73,13 @@ export default function QuestionPage() { const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); + const [username, setUsername] = useState(""); + const [userQuestionHistories, setUserQuestionHistories] = + useState(); + const [submission, setSubmission] = useState(); const state = EditorState.create({ - doc: "TODO: parse from code", + doc: "", extensions: [ basicSetup, languageConf.of(javascript()), @@ -93,20 +105,53 @@ export default function QuestionPage() { .finally(() => { setIsLoading(false); }); + }, [docRefId]); + useEffect(() => { + ValidateUser().then((data: VerifyTokenResponseType) => { + setUsername(data.data.username); + }); + }, []); + + useEffect(() => { const view = new EditorView({ state, parent: editorRef.current || undefined, }); + // TODO: get from a specific history which was selected. + GetHistory("182d0ae6db66fdbefb657f09df3a44a8").then((data: any) => { + setSubmission({ + submittedAt: data.createdAt, + language: data.language, + matchedUser: data.matchedUser, + code: data.code, + }); + + view.dispatch( + view.state.update({ + changes: { from: 0, to: state.doc.length, insert: data.code }, + }) + ); + }); + return () => { // Cleanup on component unmount view.destroy(); }; - }, [docRefId]); + }, []); - // TODO: retrieve history - const history: any[] = []; + useEffect(() => { + GetUserQuestionHistories(username, docRefId).then((data: any) => { + setUserQuestionHistories(data); + }); + }, [docRefId, username]); + + useEffect(() => { + GetUserQuestionHistories(username, docRefId).then((data: any) => { + setUserQuestionHistories(data); + }); + }, [docRefId, username]); const columns = [ { @@ -115,9 +160,9 @@ export default function QuestionPage() { key: "id", }, { - title: "Attempted at", - dataIndex: "attemptedAt", - key: "attemptedAt", + title: "Submitted at", + dataIndex: "createdAt", + key: "createdAt", }, { title: "Language", @@ -159,7 +204,7 @@ export default function QuestionPage() {
@@ -174,6 +219,23 @@ export default function QuestionPage() { Submitted Code +
+
+ Submitted at: {submission?.submittedAt || "-"} +
+
+ Language: {submission?.language || "-"} +
+
+ Matched with: {submission?.matchedUser || "-"} +
+
{/* TODO: add details of attempt here */} {/* TODO: set value of code, refactor to look like collab editor but not editable */} @@ -190,7 +252,7 @@ export default function QuestionPage() { overflow: "scroll", border: "1px solid #ddd", }} - /> + > diff --git a/apps/frontend/src/app/question/[id]/styles.scss b/apps/frontend/src/app/question/[id]/styles.scss index a0692e044c..7382768adf 100644 --- a/apps/frontend/src/app/question/[id]/styles.scss +++ b/apps/frontend/src/app/question/[id]/styles.scss @@ -66,32 +66,6 @@ padding: 1rem; } -.chat-container { - border: 2px solid #463f3a; - border-radius: 10px; - width: 100%; - padding: 1rem; -} - -.chat-message-box { - margin-top: 1rem; - height: 365px; - border: 1px solid #d9d9d9; - border-radius: 6px; - overflow-y: scroll; -} - -.chat-header-message { - font-size: 14px; - color: #c6c6c6; - margin: 3px auto; - text-align: center; -} - -.chat-typing-box { - margin-top: 1rem; -} - .question-title, .test-title, .code-title, @@ -163,3 +137,8 @@ .code-viewer { resize: none; } + +.submission-header-detail { + font-weight: normal; + padding: 0px 10px 0px 10px; +} diff --git a/apps/frontend/src/app/services/history.ts b/apps/frontend/src/app/services/history.ts index fc01f913ec..b007d832f7 100644 --- a/apps/frontend/src/app/services/history.ts +++ b/apps/frontend/src/app/services/history.ts @@ -1,84 +1,102 @@ const HISTORY_SERVICE_URL = process.env.NEXT_PUBLIC_HISTORY_SERVICE_URL; export interface History { - title: string; - code: string; - language: string; - user: string; - matchedUser: string; - matchId: string; - matchedTopics: string[]; - questionDocRefId: string; - questionDifficulty: string; - questionTopics: string[]; - createdAt?: string; - updatedAt?: string; + title: string; + code: string; + language: string; + user: string; + matchedUser: string; + historyDocRefId: string; + matchedTopics: string[]; + questionDocRefId: string; + questionDifficulty: string; + questionTopics: string[]; + createdAt?: string; + updatedAt?: string; } export const CreateOrUpdateHistory = async ( - history: History, - matchId: string, + history: History, + historyDocRefId: string ): Promise => { - const response = await fetch( - `${HISTORY_SERVICE_URL}histories/${matchId}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(history), - } - ); - - if (response.status === 200) { - return response.json(); - } else { - throw new Error( - `Error saving history: ${response.status} ${response.statusText}` - ); + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/${historyDocRefId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(history), } -} + ); -export const GetHistory = async ( - matchId: string, -): Promise => { - const response = await fetch( - `${HISTORY_SERVICE_URL}histories/${matchId}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error saving history: ${response.status} ${response.statusText}` ); + } +}; - if (response.status === 200) { - return response.json(); - } else { - throw new Error( - `Error reading history: ${response.status} ${response.statusText}` - ); +export const GetHistory = async (historyDocRefId: string): Promise => { + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/${historyDocRefId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, } -} + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error reading history: ${response.status} ${response.statusText}` + ); + } +}; export const GetUserHistories = async ( - username: string, + username: string ): Promise => { - const response = await fetch( - `${HISTORY_SERVICE_URL}histories/${username}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } + const response = await fetch(`${HISTORY_SERVICE_URL}histories/${username}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error reading user histories: ${response.status} ${response.statusText}` ); + } +}; - if (response.status === 200) { - return response.json(); - } else { - throw new Error( - `Error reading user histories: ${response.status} ${response.statusText}` - ); +export const GetUserQuestionHistories = async ( + username: string, + questionDocRefId: string +): Promise => { + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/user/${username}/question/${questionDocRefId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, } -} + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error reading user histories: ${response.status} ${response.statusText}` + ); + } +}; diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go index 6c8ee3f62f..3f0d7153a8 100644 --- a/apps/history-service/handlers/listquestionhistory.go +++ b/apps/history-service/handlers/listquestionhistory.go @@ -14,13 +14,14 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque // Parse request username := chi.URLParam(r, "username") + questionDocRefID := chi.URLParam(r, "questionDocRefId") // Reference collection collRef := s.Client.Collection("collaboration-history") // Query data - iterUser := collRef.Where("user", "==", username).Documents(ctx) - iterMatchedUser := collRef.Where("matchedUser", "==", username).Documents(ctx) + iterUser := collRef.Where("user", "==", username).Where("questionDocRefId", "==", questionDocRefID).Documents(ctx) + iterMatchedUser := collRef.Where("matchedUser", "==", username).Where("questionDocRefId", "==", questionDocRefID).Documents(ctx) // Map data var histories []models.CollaborationHistory diff --git a/apps/history-service/main.go b/apps/history-service/main.go index f57f9663ea..751f2ed525 100644 --- a/apps/history-service/main.go +++ b/apps/history-service/main.go @@ -78,10 +78,8 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { r.Route("/histories", func(r chi.Router) { r.Post("/", service.CreateHistory) r.Get("/{historyDocRefId}", service.ReadHistory) - r.Route("/{username}", func(r chi.Router) { - r.Get("/", service.ListUserHistories) - r.Get("/{questionDocRefId}", service.ListUserQuestionHistories) - }) + r.Get("/user/{username}", service.ListUserHistories) + r.Get("/user/{username}/question/{questionDocRefId}", service.ListUserQuestionHistories) }) } From 3d66e85a35492a1de141abfd0473e2177b0da5e7 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Sun, 3 Nov 2024 23:57:25 +0800 Subject: [PATCH 07/20] fix: use historyDocRefId --- apps/frontend/src/app/collaboration/[id]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index ee538edbf2..c3aefc9af4 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -152,7 +152,7 @@ export default function CollaborationPage(props: CollaborationProps) { language: selectedLanguage, user: currentUser ?? "", matchedUser: matchedUser ?? "", - matchId: collaborationId ?? "", + historyDocRefId: collaborationId ?? "", matchedTopics: matchedTopics ?? [], questionDocRefId: questionDocRefId ?? "", questionDifficulty: complexity ?? "", From 259e4620290f14673c974a4d9ca48f7c62150845 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 01:30:50 +0800 Subject: [PATCH 08/20] feat: clickable row entries --- .../src/app/collaboration/[id]/page.tsx | 4 +- apps/frontend/src/app/question/[id]/page.tsx | 179 +++++++++--------- apps/frontend/src/app/services/history.ts | 19 +- 3 files changed, 102 insertions(+), 100 deletions(-) diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index c3aefc9af4..840614f4f9 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -25,7 +25,7 @@ import { import CollaborativeEditor, { CollaborativeEditorHandle, } from "@/components/CollaborativeEditor/CollaborativeEditor"; -import { CreateOrUpdateHistory } from "@/app/services/history"; +import { CreateHistory } from "@/app/services/history"; import { WebrtcProvider } from "y-webrtc"; import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; @@ -145,7 +145,7 @@ export default function CollaborationPage(props: CollaborationProps) { if (!collaborationId) { throw new Error("Collaboration ID not found"); } - const data = await CreateOrUpdateHistory( + const data = await CreateHistory( { title: questionTitle ?? "", code: code, diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 23616cd404..ff0d4512f6 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -1,38 +1,15 @@ "use client"; import Header from "@/components/Header/header"; -import { - Button, - Col, - Layout, - message, - Row, - Tag, - Select, - Table, - Input, -} from "antd"; +import { Col, Layout, message, Row, Table } from "antd"; import { Content } from "antd/es/layout/layout"; -import { - LeftOutlined, - RightOutlined, - CaretRightOutlined, - CodeOutlined, - SendOutlined, - HistoryOutlined, -} from "@ant-design/icons"; +import { CodeOutlined, HistoryOutlined } from "@ant-design/icons"; import "./styles.scss"; import { useEffect, useRef, useState } from "react"; import { GetSingleQuestion } from "../../services/question"; import React from "react"; -import TextArea from "antd/es/input/TextArea"; import { useSearchParams } from "next/navigation"; -import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; import { useRouter } from "next/navigation"; import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; -import CollaborativeEditor, { - CollaborativeEditorHandle, -} from "@/components/CollaborativeEditor/CollaborativeEditor"; -import { WebrtcProvider } from "y-webrtc"; import { Compartment, EditorState } from "@codemirror/state"; import { basicSetup, EditorView } from "codemirror"; import { javascript } from "@codemirror/lang-javascript"; @@ -44,6 +21,7 @@ interface Submission { language: string; matchedUser: string; code: string; + historyDocRefId: string; } export default function QuestionPage() { @@ -73,10 +51,14 @@ export default function QuestionPage() { const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); - const [username, setUsername] = useState(""); + const [username, setUsername] = useState(undefined); const [userQuestionHistories, setUserQuestionHistories] = useState(); const [submission, setSubmission] = useState(); + const [isHistoryLoading, setIsHistoryLoading] = useState(true); + const [currentSubmissionId, setCurrentSubmissionId] = useState< + string | undefined + >(undefined); const state = EditorState.create({ doc: "", @@ -107,6 +89,18 @@ export default function QuestionPage() { }); }, [docRefId]); + useEffect(() => { + if (username === undefined) return; + GetUserQuestionHistories(username, docRefId) + .then((data: any) => { + console.log(data); + setUserQuestionHistories(data); + }) + .finally(() => { + setIsHistoryLoading(false); + }); + }, [username]); + useEffect(() => { ValidateUser().then((data: VerifyTokenResponseType) => { setUsername(data.data.username); @@ -114,22 +108,27 @@ export default function QuestionPage() { }, []); useEffect(() => { + if (currentSubmissionId === undefined) return; + const view = new EditorView({ state, parent: editorRef.current || undefined, }); // TODO: get from a specific history which was selected. - GetHistory("182d0ae6db66fdbefb657f09df3a44a8").then((data: any) => { + // Show latest history by default, or load specific history + GetHistory(currentSubmissionId).then((data: any) => { + const submittedAt = new Date(data.createdAt); setSubmission({ - submittedAt: data.createdAt, + submittedAt: submittedAt.toLocaleString("en-US"), language: data.language, matchedUser: data.matchedUser, code: data.code, + historyDocRefId: data.historyDocRefId, }); view.dispatch( - view.state.update({ + state.update({ changes: { from: 0, to: state.doc.length, insert: data.code }, }) ); @@ -139,23 +138,11 @@ export default function QuestionPage() { // Cleanup on component unmount view.destroy(); }; - }, []); - - useEffect(() => { - GetUserQuestionHistories(username, docRefId).then((data: any) => { - setUserQuestionHistories(data); - }); - }, [docRefId, username]); - - useEffect(() => { - GetUserQuestionHistories(username, docRefId).then((data: any) => { - setUserQuestionHistories(data); - }); - }, [docRefId, username]); + }, [currentSubmissionId]); const columns = [ { - title: "Id", + title: "ID", dataIndex: "id", key: "id", }, @@ -163,6 +150,9 @@ export default function QuestionPage() { title: "Submitted at", dataIndex: "createdAt", key: "createdAt", + render: (date: string) => { + return new Date(date).toLocaleString(); + }, }, { title: "Language", @@ -176,6 +166,10 @@ export default function QuestionPage() { }, ]; + const handleRowClick = (s: Submission) => { + setCurrentSubmissionId(s.historyDocRefId); + }; + return (
{contextHolder} @@ -206,56 +200,67 @@ export default function QuestionPage() { rowKey="id" dataSource={userQuestionHistories} columns={columns} - loading={isLoading} + onRow={(record: any) => { + return { + onClick: () => handleRowClick(record), + style: { cursor: "pointer" }, + }; + }} + loading={isHistoryLoading} />
- -
-
-
- - Submitted Code -
-
-
-
- Submitted at: {submission?.submittedAt || "-"} -
-
- Language: {submission?.language || "-"} -
-
- Matched with: {submission?.matchedUser || "-"} -
-
+ {currentSubmissionId && ( + +
+ <> +
+
+ + Submitted Code +
+
- {/* TODO: add details of attempt here */} - {/* TODO: set value of code, refactor to look like collab editor but not editable */} -
-
+ {/* Details of submission */} +
+
+ Submitted at: {submission?.submittedAt || "-"} +
+
+ Language: {submission?.language || "-"} +
+
+ Matched with: {submission?.matchedUser || "-"} +
+
+ + {/* Code Editor */} +
+
+
+
-
-
+ + )} diff --git a/apps/frontend/src/app/services/history.ts b/apps/frontend/src/app/services/history.ts index b007d832f7..a2e52bcaf5 100644 --- a/apps/frontend/src/app/services/history.ts +++ b/apps/frontend/src/app/services/history.ts @@ -15,20 +15,17 @@ export interface History { updatedAt?: string; } -export const CreateOrUpdateHistory = async ( +export const CreateHistory = async ( history: History, historyDocRefId: string ): Promise => { - const response = await fetch( - `${HISTORY_SERVICE_URL}histories/${historyDocRefId}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(history), - } - ); + const response = await fetch(`${HISTORY_SERVICE_URL}histories/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(history), + }); if (response.status === 200) { return response.json(); From 657a29d9bdea3023b41d8d03ff3743a6c6aa7238 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 01:47:09 +0800 Subject: [PATCH 09/20] feat: modify create so that history is always created --- apps/history-service/handlers/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index 1dd7e6efd8..6290a44d28 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -22,9 +22,9 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { } // Document reference ID in firestore mapped to the match ID in model - docRef := s.Client.Collection("collaboration-history").Doc(collaborationHistory.HistoryDocRefID) + collection := s.Client.Collection("collaboration-history") - _, err := docRef.Set(ctx, map[string]interface{}{ + docRef, _, err := collection.Add(ctx, map[string]interface{}{ "title": collaborationHistory.Title, "code": collaborationHistory.Code, "language": collaborationHistory.Language, From 1ae0f5bc54ecc6d6de50537362661dc45fa0e23b Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 02:09:08 +0800 Subject: [PATCH 10/20] feat: order by created at desc --- apps/history-service/handlers/createOrUpdate.go | 1 + apps/history-service/handlers/delete.go | 2 +- .../handlers/listquestionhistory.go | 15 +++++++++++++-- apps/history-service/handlers/listuserhistory.go | 13 +++++++++++-- apps/history-service/handlers/update.go | 2 +- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go index 1c3ba5d352..561c463d43 100644 --- a/apps/history-service/handlers/createOrUpdate.go +++ b/apps/history-service/handlers/createOrUpdate.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/status" ) +// Unused func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apps/history-service/handlers/delete.go b/apps/history-service/handlers/delete.go index f528e4bc34..29121908d7 100644 --- a/apps/history-service/handlers/delete.go +++ b/apps/history-service/handlers/delete.go @@ -8,7 +8,7 @@ import ( "google.golang.org/grpc/status" ) -// Delete a code snippet by ID +// Delete a code snippet by ID: unused func (s *Service) DeleteHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go index 3f0d7153a8..999a04993c 100644 --- a/apps/history-service/handlers/listquestionhistory.go +++ b/apps/history-service/handlers/listquestionhistory.go @@ -5,6 +5,7 @@ import ( "history-service/models" "net/http" + "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" ) @@ -20,8 +21,14 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque collRef := s.Client.Collection("collaboration-history") // Query data - iterUser := collRef.Where("user", "==", username).Where("questionDocRefId", "==", questionDocRefID).Documents(ctx) - iterMatchedUser := collRef.Where("matchedUser", "==", username).Where("questionDocRefId", "==", questionDocRefID).Documents(ctx) + iterUser := collRef.Where("user", "==", username). + Where("questionDocRefId", "==", questionDocRefID). + OrderBy("createdAt", firestore.Desc). + Documents(ctx) + iterMatchedUser := collRef.Where("matchedUser", "==", username). + Where("questionDocRefId", "==", questionDocRefID). + OrderBy("createdAt", firestore.Desc). + Documents(ctx) // Map data var histories []models.CollaborationHistory @@ -62,6 +69,10 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque } history.HistoryDocRefID = doc.Ref.ID + // Swap matched user and user + history.MatchedUser = history.User + history.User = username + histories = append(histories, history) } diff --git a/apps/history-service/handlers/listuserhistory.go b/apps/history-service/handlers/listuserhistory.go index 712c649020..80c06d0963 100644 --- a/apps/history-service/handlers/listuserhistory.go +++ b/apps/history-service/handlers/listuserhistory.go @@ -5,6 +5,7 @@ import ( "history-service/models" "net/http" + "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" ) @@ -19,8 +20,12 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { collRef := s.Client.Collection("collaboration-history") // Query data - iterUser := collRef.Where("user", "==", username).Documents(ctx) - iterMatchedUser := collRef.Where("matchedUser", "==", username).Documents(ctx) + iterUser := collRef.Where("user", "==", username). + OrderBy("createdAt", firestore.Desc). + Documents(ctx) + iterMatchedUser := collRef.Where("matchedUser", "==", username). + OrderBy("createdAt", firestore.Desc). + Documents(ctx) // Map data var histories []models.CollaborationHistory @@ -61,6 +66,10 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { } history.HistoryDocRefID = doc.Ref.ID + // Swap matched user and user + history.MatchedUser = history.User + history.User = username + histories = append(histories, history) } diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go index f91d9794e9..9f946ca3e9 100644 --- a/apps/history-service/handlers/update.go +++ b/apps/history-service/handlers/update.go @@ -13,7 +13,7 @@ import ( "google.golang.org/grpc/status" ) -// Update an existing code snippet +// Update an existing code snippet: Unused func (s *Service) UpdateHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() From a3bcb12da7f8a518f106ae3251ee0ada944b44b5 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 02:09:30 +0800 Subject: [PATCH 11/20] fix: swap matched user and user on frontend for specific submission --- apps/frontend/src/app/question/[id]/page.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index ff0d4512f6..f7fd03da5e 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -122,7 +122,8 @@ export default function QuestionPage() { setSubmission({ submittedAt: submittedAt.toLocaleString("en-US"), language: data.language, - matchedUser: data.matchedUser, + matchedUser: + username == data.matchedUser ? data.User : data.matchedUser, code: data.code, historyDocRefId: data.historyDocRefId, }); @@ -141,11 +142,6 @@ export default function QuestionPage() { }, [currentSubmissionId]); const columns = [ - { - title: "ID", - dataIndex: "id", - key: "id", - }, { title: "Submitted at", dataIndex: "createdAt", @@ -206,6 +202,7 @@ export default function QuestionPage() { style: { cursor: "pointer" }, }; }} + scroll={{ y: "max-content" }} loading={isHistoryLoading} />
From 4015266b2919966aec8233e6b855b56893034191 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 02:25:49 +0800 Subject: [PATCH 12/20] fix: backend history sorting --- apps/history-service/handlers/listquestionhistory.go | 9 ++++++--- apps/history-service/handlers/listuserhistory.go | 9 ++++++--- apps/history-service/models/models.go | 9 +++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go index 999a04993c..481770f85e 100644 --- a/apps/history-service/handlers/listquestionhistory.go +++ b/apps/history-service/handlers/listquestionhistory.go @@ -4,8 +4,8 @@ import ( "encoding/json" "history-service/models" "net/http" + "sort" - "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" ) @@ -23,12 +23,12 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque // Query data iterUser := collRef.Where("user", "==", username). Where("questionDocRefId", "==", questionDocRefID). - OrderBy("createdAt", firestore.Desc). Documents(ctx) + defer iterUser.Stop() iterMatchedUser := collRef.Where("matchedUser", "==", username). Where("questionDocRefId", "==", questionDocRefID). - OrderBy("createdAt", firestore.Desc). Documents(ctx) + defer iterMatchedUser.Stop() // Map data var histories []models.CollaborationHistory @@ -76,6 +76,9 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque histories = append(histories, history) } + // Sort the histories by created at time + sort.Sort(models.HistorySorter(histories)) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(histories) diff --git a/apps/history-service/handlers/listuserhistory.go b/apps/history-service/handlers/listuserhistory.go index 80c06d0963..269f6486ed 100644 --- a/apps/history-service/handlers/listuserhistory.go +++ b/apps/history-service/handlers/listuserhistory.go @@ -4,8 +4,8 @@ import ( "encoding/json" "history-service/models" "net/http" + "sort" - "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" ) @@ -21,11 +21,11 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { // Query data iterUser := collRef.Where("user", "==", username). - OrderBy("createdAt", firestore.Desc). Documents(ctx) + defer iterUser.Stop() iterMatchedUser := collRef.Where("matchedUser", "==", username). - OrderBy("createdAt", firestore.Desc). Documents(ctx) + defer iterUser.Stop() // Map data var histories []models.CollaborationHistory @@ -73,6 +73,9 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { histories = append(histories, history) } + // Sort the histories by created at time + sort.Sort(models.HistorySorter(histories)) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(histories) diff --git a/apps/history-service/models/models.go b/apps/history-service/models/models.go index b8cc571593..3e1f3a7836 100644 --- a/apps/history-service/models/models.go +++ b/apps/history-service/models/models.go @@ -23,3 +23,12 @@ type CollaborationHistory struct { UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` // updatedAt is unused as history is never updated once created HistoryDocRefID string `json:"historyDocRefId"` } + +// Sorting interface for history, which sorts by created at in desc order +type HistorySorter []CollaborationHistory + +func (s HistorySorter) Len() int { return len(s) } +func (s HistorySorter) Less(i, j int) bool { + return s[i].CreatedAt.After(s[j].CreatedAt) +} +func (s HistorySorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } From 36d7b07058446476a4561a93977284183a064eb1 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 03:43:14 +0800 Subject: [PATCH 13/20] feat: implement pagination for histories on question page --- apps/frontend/src/app/question/[id]/page.tsx | 63 +++++++++++++++---- apps/frontend/src/app/services/history.ts | 16 ++++- apps/history-service/handlers/create.go | 2 +- .../handlers/createOrUpdate.go | 2 +- .../handlers/listquestionhistory.go | 33 ++++++++-- .../handlers/listuserhistory.go | 33 ++++++++-- apps/history-service/handlers/read.go | 2 +- apps/history-service/handlers/update.go | 2 +- apps/history-service/models/models.go | 4 +- apps/history-service/models/pagination.go | 44 +++++++++++-- 10 files changed, 166 insertions(+), 35 deletions(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index f7fd03da5e..a28d2a1a49 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; import Header from "@/components/Header/header"; -import { Col, Layout, message, Row, Table } from "antd"; +import { Col, Layout, message, PaginationProps, Row, Table } from "antd"; import { Content } from "antd/es/layout/layout"; import { CodeOutlined, HistoryOutlined } from "@ant-design/icons"; import "./styles.scss"; @@ -24,6 +24,13 @@ interface Submission { historyDocRefId: string; } +interface TablePagination { + totalCount: number; + totalPages: number; + currentPage: number; + limit: number; +} + export default function QuestionPage() { const [isLoading, setIsLoading] = useState(true); // Store the states related to table's loading @@ -59,6 +66,12 @@ export default function QuestionPage() { const [currentSubmissionId, setCurrentSubmissionId] = useState< string | undefined >(undefined); + const [paginationParams, setPaginationParams] = useState({ + totalCount: 0, + totalPages: 0, + currentPage: 1, + limit: 3, + }); const state = EditorState.create({ doc: "", @@ -71,6 +84,33 @@ export default function QuestionPage() { ], }); + // Handler for change in page jumper + const onPageJump: PaginationProps["onChange"] = (pageNumber) => { + setPaginationParams((prev) => { + loadQuestionHistories(pageNumber, paginationParams.limit); + return { ...paginationParams, currentPage: pageNumber }; + }); + }; + + async function loadQuestionHistories(currentPage: number, limit: number) { + if (username === undefined) return; + setIsHistoryLoading(true); + GetUserQuestionHistories(username, docRefId, currentPage, limit) + .then((data: any) => { + setUserQuestionHistories(data.histories); + setPaginationParams({ + ...paginationParams, + totalCount: data.totalCount, + totalPages: data.totalPages, + currentPage: data.currentPage, + limit: data.limit, + }); + }) + .finally(() => { + setIsHistoryLoading(false); + }); + } + // When code editor page is initialised, fetch the particular question, and display in code editor useEffect(() => { if (!isLoading) { @@ -90,15 +130,7 @@ export default function QuestionPage() { }, [docRefId]); useEffect(() => { - if (username === undefined) return; - GetUserQuestionHistories(username, docRefId) - .then((data: any) => { - console.log(data); - setUserQuestionHistories(data); - }) - .finally(() => { - setIsHistoryLoading(false); - }); + loadQuestionHistories(paginationParams.currentPage, paginationParams.limit); }, [username]); useEffect(() => { @@ -108,6 +140,7 @@ export default function QuestionPage() { }, []); useEffect(() => { + // Only show history if a history is selected if (currentSubmissionId === undefined) return; const view = new EditorView({ @@ -115,8 +148,6 @@ export default function QuestionPage() { parent: editorRef.current || undefined, }); - // TODO: get from a specific history which was selected. - // Show latest history by default, or load specific history GetHistory(currentSubmissionId).then((data: any) => { const submittedAt = new Date(data.createdAt); setSubmission({ @@ -202,8 +233,14 @@ export default function QuestionPage() { style: { cursor: "pointer" }, }; }} - scroll={{ y: "max-content" }} loading={isHistoryLoading} + pagination={{ + size: "small", + current: paginationParams.currentPage, + total: paginationParams.totalCount, + pageSize: paginationParams.limit, + onChange: onPageJump, + }} /> diff --git a/apps/frontend/src/app/services/history.ts b/apps/frontend/src/app/services/history.ts index a2e52bcaf5..5ecdda6efa 100644 --- a/apps/frontend/src/app/services/history.ts +++ b/apps/frontend/src/app/services/history.ts @@ -77,10 +77,22 @@ export const GetUserHistories = async ( export const GetUserQuestionHistories = async ( username: string, - questionDocRefId: string + questionDocRefId: string, + currentPage?: number, + limit?: number ): Promise => { + let query_params = ""; + + if (currentPage) { + query_params += `?offset=${(currentPage - 1) * (limit ? limit : 10)}`; + } + + if (limit) { + query_params += `${query_params.length > 0 ? "&" : "?"}limit=${limit}`; + } + const response = await fetch( - `${HISTORY_SERVICE_URL}histories/user/${username}/question/${questionDocRefId}`, + `${HISTORY_SERVICE_URL}histories/user/${username}/question/${questionDocRefId}${query_params}`, { method: "GET", headers: { diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index 6290a44d28..a960da60ef 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -15,7 +15,7 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse request - var collaborationHistory models.CollaborationHistory + var collaborationHistory models.SubmissionHistory if err := utils.DecodeJSONBody(w, r, &collaborationHistory); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go index 561c463d43..c87aa026fb 100644 --- a/apps/history-service/handlers/createOrUpdate.go +++ b/apps/history-service/handlers/createOrUpdate.go @@ -19,7 +19,7 @@ func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) // Parse request matchId := chi.URLParam(r, "matchId") - var collaborationHistory models.CollaborationHistory + var collaborationHistory models.SubmissionHistory if err := utils.DecodeJSONBody(w, r, &collaborationHistory); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/apps/history-service/handlers/listquestionhistory.go b/apps/history-service/handlers/listquestionhistory.go index 481770f85e..239f9aeed5 100644 --- a/apps/history-service/handlers/listquestionhistory.go +++ b/apps/history-service/handlers/listquestionhistory.go @@ -5,6 +5,7 @@ import ( "history-service/models" "net/http" "sort" + "strconv" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" @@ -31,7 +32,7 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque defer iterMatchedUser.Stop() // Map data - var histories []models.CollaborationHistory + var histories []models.SubmissionHistory for { doc, err := iterUser.Next() if err == iterator.Done { @@ -42,7 +43,7 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque return } - var history models.CollaborationHistory + var history models.SubmissionHistory if err := doc.DataTo(&history); err != nil { http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) return @@ -62,7 +63,7 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque return } - var history models.CollaborationHistory + var history models.SubmissionHistory if err := doc.DataTo(&history); err != nil { http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) return @@ -79,7 +80,31 @@ func (s *Service) ListUserQuestionHistories(w http.ResponseWriter, r *http.Reque // Sort the histories by created at time sort.Sort(models.HistorySorter(histories)) + // Pagination + limitParam := r.URL.Query().Get("limit") + limit := 10 + if limitParam != "" { + l, err := strconv.Atoi(limitParam) // convert limit to integer + if err != nil || l <= 0 { + http.Error(w, "Invalid limit: "+strconv.Itoa(l), http.StatusBadRequest) + return + } + limit = l + } + offsetParam := r.URL.Query().Get("offset") + offset := 0 + if offsetParam != "" { + o, err := strconv.Atoi(offsetParam) // convert offset to integer + if err != nil { + http.Error(w, "Invalid offset: "+strconv.Itoa(o), http.StatusBadRequest) + return + } + offset = o + } + + response := models.PaginateResponse(limit, offset, histories) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(histories) + json.NewEncoder(w).Encode(response) } diff --git a/apps/history-service/handlers/listuserhistory.go b/apps/history-service/handlers/listuserhistory.go index 269f6486ed..5fa8a021d2 100644 --- a/apps/history-service/handlers/listuserhistory.go +++ b/apps/history-service/handlers/listuserhistory.go @@ -5,6 +5,7 @@ import ( "history-service/models" "net/http" "sort" + "strconv" "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" @@ -28,7 +29,7 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { defer iterUser.Stop() // Map data - var histories []models.CollaborationHistory + var histories []models.SubmissionHistory for { doc, err := iterUser.Next() if err == iterator.Done { @@ -39,7 +40,7 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { return } - var history models.CollaborationHistory + var history models.SubmissionHistory if err := doc.DataTo(&history); err != nil { http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) return @@ -59,7 +60,7 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { return } - var history models.CollaborationHistory + var history models.SubmissionHistory if err := doc.DataTo(&history); err != nil { http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) return @@ -76,7 +77,31 @@ func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { // Sort the histories by created at time sort.Sort(models.HistorySorter(histories)) + // Pagination + limitParam := r.URL.Query().Get("limit") + limit := 10 + if limitParam != "" { + l, err := strconv.Atoi(limitParam) // convert limit to integer + if err != nil || l <= 0 { + http.Error(w, "Invalid limit: "+strconv.Itoa(l), http.StatusBadRequest) + return + } + limit = l + } + offsetParam := r.URL.Query().Get("offset") + offset := 0 + if offsetParam != "" { + o, err := strconv.Atoi(offsetParam) // convert offset to integer + if err != nil { + http.Error(w, "Invalid offset: "+strconv.Itoa(o), http.StatusBadRequest) + return + } + offset = o + } + + response := models.PaginateResponse(limit, offset, histories) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(histories) + json.NewEncoder(w).Encode(response) } diff --git a/apps/history-service/handlers/read.go b/apps/history-service/handlers/read.go index f986393919..6f7c35404d 100644 --- a/apps/history-service/handlers/read.go +++ b/apps/history-service/handlers/read.go @@ -30,7 +30,7 @@ func (s *Service) ReadHistory(w http.ResponseWriter, r *http.Request) { } // Map data - var collaborationHistory models.CollaborationHistory + var collaborationHistory models.SubmissionHistory if err := doc.DataTo(&collaborationHistory); err != nil { http.Error(w, "Failed to map history data", http.StatusInternalServerError) return diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go index 9f946ca3e9..a25b983368 100644 --- a/apps/history-service/handlers/update.go +++ b/apps/history-service/handlers/update.go @@ -19,7 +19,7 @@ func (s *Service) UpdateHistory(w http.ResponseWriter, r *http.Request) { // Parse request matchId := chi.URLParam(r, "matchId") - var updatedHistory models.CollaborationHistory + var updatedHistory models.SubmissionHistory if err := utils.DecodeJSONBody(w, r, &updatedHistory); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/apps/history-service/models/models.go b/apps/history-service/models/models.go index 3e1f3a7836..158ec947d6 100644 --- a/apps/history-service/models/models.go +++ b/apps/history-service/models/models.go @@ -2,7 +2,7 @@ package models import "time" -type CollaborationHistory struct { +type SubmissionHistory struct { // Submission related details Code string `json:"code" firestore:"code"` Language string `json:"language" firestore:"language"` @@ -25,7 +25,7 @@ type CollaborationHistory struct { } // Sorting interface for history, which sorts by created at in desc order -type HistorySorter []CollaborationHistory +type HistorySorter []SubmissionHistory func (s HistorySorter) Len() int { return len(s) } func (s HistorySorter) Less(i, j int) bool { diff --git a/apps/history-service/models/pagination.go b/apps/history-service/models/pagination.go index a5a74ba7ea..6553325500 100644 --- a/apps/history-service/models/pagination.go +++ b/apps/history-service/models/pagination.go @@ -1,10 +1,42 @@ package models type HistoriesResponse struct { - TotalCount int `json:"totalCount"` - TotalPages int `json:"totalPages"` - CurrentPage int `json:"currentPage"` - Limit int `json:"limit"` - HasNextPage bool `json:"hasNextPage"` - Questions []CollaborationHistory `json:"histories"` + TotalCount int `json:"totalCount"` + TotalPages int `json:"totalPages"` + CurrentPage int `json:"currentPage"` + Limit int `json:"limit"` + HasNextPage bool `json:"hasNextPage"` + Questions []SubmissionHistory `json:"histories"` +} + +func PaginateResponse(limit, offset int, histories []SubmissionHistory) *HistoriesResponse { + start := offset + end := offset + limit + + var paginatedHistory []SubmissionHistory + if start < len(histories) { + if end > len(histories) { + end = len(histories) + } + paginatedHistory = histories[start:end] + } + + // Calculate pagination info + totalCount := len(histories) + totalPages := (totalCount + limit - 1) / limit + currentPage := (offset / limit) + 1 + if len(paginatedHistory) == 0 { + currentPage = 0 + } + hasNextPage := totalPages > currentPage + + // Construct response + return &HistoriesResponse{ + TotalCount: totalCount, + TotalPages: totalPages, + CurrentPage: currentPage, + Limit: limit, + HasNextPage: hasNextPage, + Questions: paginatedHistory, + } } From fb89725d10a6fc7da93ee9171b5773781100fbbe Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 03:57:08 +0800 Subject: [PATCH 14/20] fix: heights of submission editor --- apps/frontend/src/app/question/[id]/page.tsx | 5 +++-- apps/frontend/src/app/question/[id]/styles.scss | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index a28d2a1a49..82600926c0 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -79,7 +79,7 @@ export default function QuestionPage() { basicSetup, languageConf.of(javascript()), EditorView.theme({ - "&": { height: "100%", overflow: "hidden" }, // Enable scroll + "&": { height: "100%", overflow: "hidden" }, // Enable Scroll }), ], }); @@ -241,6 +241,7 @@ export default function QuestionPage() { pageSize: paginationParams.limit, onChange: onPageJump, }} + scroll={{ y: 200 }} /> @@ -279,7 +280,7 @@ export default function QuestionPage() {
Date: Mon, 4 Nov 2024 03:58:51 +0800 Subject: [PATCH 15/20] fix: disable editor --- apps/frontend/src/app/question/[id]/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 82600926c0..8719beeb32 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -81,6 +81,7 @@ export default function QuestionPage() { EditorView.theme({ "&": { height: "100%", overflow: "hidden" }, // Enable Scroll }), + EditorView.editable.of(false), // Disable editing ], }); From 1d5a38ddc1f04727389c4f358f986b1c3e44828d Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 03:59:55 +0800 Subject: [PATCH 16/20] fix: other user query --- apps/frontend/src/app/question/[id]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 8719beeb32..95cf48164c 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -155,7 +155,7 @@ export default function QuestionPage() { submittedAt: submittedAt.toLocaleString("en-US"), language: data.language, matchedUser: - username == data.matchedUser ? data.User : data.matchedUser, + username == data.matchedUser ? data.user : data.matchedUser, code: data.code, historyDocRefId: data.historyDocRefId, }); From b1dd96c64e33bb255abf482ef7d02563c1f61d33 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 04:59:44 +0800 Subject: [PATCH 17/20] feat: add history page --- apps/frontend/src/app/history/page.tsx | 191 ++++++++++++++++++ apps/frontend/src/app/history/styles.scss | 64 ++++++ apps/frontend/src/app/question/[id]/page.tsx | 18 +- apps/frontend/src/app/services/history.ts | 29 ++- .../frontend/src/components/Header/header.tsx | 19 +- 5 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 apps/frontend/src/app/history/page.tsx create mode 100644 apps/frontend/src/app/history/styles.scss diff --git a/apps/frontend/src/app/history/page.tsx b/apps/frontend/src/app/history/page.tsx new file mode 100644 index 0000000000..8a6023e4a2 --- /dev/null +++ b/apps/frontend/src/app/history/page.tsx @@ -0,0 +1,191 @@ +"use client"; +import Header from "@/components/Header/header"; +import { Layout, message, PaginationProps, Row, Table, Tag } from "antd"; +import { Content } from "antd/es/layout/layout"; +import { HistoryOutlined } from "@ant-design/icons"; +import "./styles.scss"; +import { useEffect, useState } from "react"; +import React from "react"; +import { useRouter } from "next/navigation"; +import { GetUserHistories, History } from "@/app/services/history"; +import { ValidateUser, VerifyTokenResponseType } from "@/app/services/user"; + +interface TablePagination { + totalCount: number; + totalPages: number; + currentPage: number; + limit: number; +} + +export default function QuestionPage() { + // Message States + const [messageApi, contextHolder] = message.useMessage(); + + const error = (message: string) => { + messageApi.open({ + type: "error", + content: message, + }); + }; + + const router = useRouter(); + + const [username, setUsername] = useState(undefined); + const [userQuestionHistories, setUserQuestionHistories] = + useState(); + const [isHistoryLoading, setIsHistoryLoading] = useState(true); + const [paginationParams, setPaginationParams] = useState({ + totalCount: 0, + totalPages: 0, + currentPage: 1, + limit: 10, + }); + + // Handler for change in page jumper + const onPageJump: PaginationProps["onChange"] = (pageNumber) => { + setPaginationParams((prev) => { + loadQuestionHistories(pageNumber, paginationParams.limit); + return { ...paginationParams, currentPage: pageNumber }; + }); + }; + + // Handler for show size change for pagination + const onShowSizeChange: PaginationProps["onShowSizeChange"] = ( + current, + pageSize + ) => { + setPaginationParams((prev) => { + loadQuestionHistories(current, pageSize); + return { ...paginationParams, currentPage: current, limit: pageSize }; + }); + }; + + async function loadQuestionHistories(currentPage: number, limit: number) { + if (username === undefined) return; + setIsHistoryLoading(true); + GetUserHistories(username, currentPage, limit) + .then((data: any) => { + setUserQuestionHistories(data.histories); + setPaginationParams({ + ...paginationParams, + totalCount: data.totalCount, + totalPages: data.totalPages, + currentPage: data.currentPage, + limit: data.limit, + }); + }) + .finally(() => { + setIsHistoryLoading(false); + }); + } + + useEffect(() => { + loadQuestionHistories(paginationParams.currentPage, paginationParams.limit); + }, [username]); + + useEffect(() => { + ValidateUser().then((data: VerifyTokenResponseType) => { + setUsername(data.data.username); + }); + }, []); + + const columns = [ + { + title: "Title", + dataIndex: "title", + key: "title", + }, + { + title: "Categories", + dataIndex: "questionTopics", + key: "questionTopics", + render: (categories: string[]) => + categories.map((category) => {category}), + }, + { + title: "Difficulty", + dataIndex: "questionDifficulty", + key: "questionDifficulty", + render: (difficulty: string) => { + let color = ""; + if (difficulty === "easy") { + color = "#2DB55D"; + } else if (difficulty === "medium") { + color = "orange"; + } else if (difficulty === "hard") { + color = "red"; + } + return ( +
+ {difficulty.charAt(0).toUpperCase() + difficulty.slice(1)} +
+ ); + }, + }, + { + title: "Submitted at", + dataIndex: "createdAt", + key: "createdAt", + render: (date: string) => { + return new Date(date).toLocaleString(); + }, + }, + { + title: "Language", + dataIndex: "language", + key: "language", + }, + { + title: "Matched with", + dataIndex: "matchedUser", + key: "matchedUser", + }, + ]; + + const handleRowClick = (h: History) => { + // Link to page + // questionId is just read as "history", as only the doc ref id is involved in requests + // If the question database is reloaded, then the questionDocRefId may not be correct + router.push( + `/question/history?data=${h.questionDocRefId}&history=${h.historyDocRefId}` + ); + }; + + return ( +
+ {contextHolder} + +
+ +
+
+
Submission History
+
+
+
{ + return { + onClick: () => handleRowClick(record), + style: { cursor: "pointer" }, + }; + }} + loading={isHistoryLoading} + pagination={{ + current: paginationParams.currentPage, + total: paginationParams.totalCount, + pageSize: paginationParams.limit, + onChange: onPageJump, + showSizeChanger: true, + onShowSizeChange: onShowSizeChange, + }} + /> + + + + + + ); +} diff --git a/apps/frontend/src/app/history/styles.scss b/apps/frontend/src/app/history/styles.scss new file mode 100644 index 0000000000..3d435ad375 --- /dev/null +++ b/apps/frontend/src/app/history/styles.scss @@ -0,0 +1,64 @@ +.layout { + background: white; +} + +.content { + background: white; +} + +.content-card { + margin: 4rem 8rem; + padding: 2rem 4rem; + background-color: white; + min-height: 100vh; + border-radius: 30px; + box-shadow: 0px 10px 60px rgba(226, 236, 249, 0.5); +} + +.content-row-1 { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.content-title { + // font-family: "Poppins"; + // font-style: normal; + font-weight: 600; + font-size: 22px; + line-height: 33px; + letter-spacing: -0.01em; + color: #000000; +} + +.content-filter { + margin: 1.5rem 0; +} + +.edit-button { + margin-right: 0.5rem; +} + +.filter-button { + margin-left: 8px; + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02) !important; +} + +.clear-button { + width: 100%; +} + +.categories-multi-select, +.difficulty-select, +.order-select { + width: 100%; +} + +.create-title, +.new-problem-categories-multi-select, +.new-problem-difficulty-select, +.create-description, +.create-problem-id { + width: 100%; + margin: 5px; +} diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 95cf48164c..4a03f91ee9 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -20,8 +20,8 @@ interface Submission { submittedAt: string; language: string; matchedUser: string; - code: string; historyDocRefId: string; + code: string; } interface TablePagination { @@ -44,13 +44,13 @@ export default function QuestionPage() { }); }; - const router = useRouter(); const editorRef = useRef(null); const languageConf = new Compartment(); - // Retrieve the docRefId from query params during page navigation + // Retrieve the questionDocRefId and historyDocRefId from query params during page navigation const searchParams = useSearchParams(); - const docRefId: string = searchParams?.get("data") ?? ""; + const questionDocRefId: string = searchParams?.get("data") ?? ""; + const historyDocRefId: string = searchParams?.get("history") ?? ""; // Code Editor States const [questionTitle, setQuestionTitle] = useState( undefined @@ -65,7 +65,7 @@ export default function QuestionPage() { const [isHistoryLoading, setIsHistoryLoading] = useState(true); const [currentSubmissionId, setCurrentSubmissionId] = useState< string | undefined - >(undefined); + >(historyDocRefId == "" ? undefined : historyDocRefId); const [paginationParams, setPaginationParams] = useState({ totalCount: 0, totalPages: 0, @@ -96,7 +96,7 @@ export default function QuestionPage() { async function loadQuestionHistories(currentPage: number, limit: number) { if (username === undefined) return; setIsHistoryLoading(true); - GetUserQuestionHistories(username, docRefId, currentPage, limit) + GetUserQuestionHistories(username, questionDocRefId, currentPage, limit) .then((data: any) => { setUserQuestionHistories(data.histories); setPaginationParams({ @@ -118,7 +118,7 @@ export default function QuestionPage() { setIsLoading(true); } - GetSingleQuestion(docRefId) + GetSingleQuestion(questionDocRefId) .then((data: any) => { setQuestionTitle(data.title); setComplexity(data.complexity); @@ -128,7 +128,7 @@ export default function QuestionPage() { .finally(() => { setIsLoading(false); }); - }, [docRefId]); + }, [questionDocRefId]); useEffect(() => { loadQuestionHistories(paginationParams.currentPage, paginationParams.limit); @@ -156,8 +156,8 @@ export default function QuestionPage() { language: data.language, matchedUser: username == data.matchedUser ? data.user : data.matchedUser, - code: data.code, historyDocRefId: data.historyDocRefId, + code: data.code, }); view.dispatch( diff --git a/apps/frontend/src/app/services/history.ts b/apps/frontend/src/app/services/history.ts index 5ecdda6efa..f98f23c657 100644 --- a/apps/frontend/src/app/services/history.ts +++ b/apps/frontend/src/app/services/history.ts @@ -57,14 +57,29 @@ export const GetHistory = async (historyDocRefId: string): Promise => { }; export const GetUserHistories = async ( - username: string + username: string, + currentPage?: number, + limit?: number ): Promise => { - const response = await fetch(`${HISTORY_SERVICE_URL}histories/${username}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + let query_params = ""; + + if (currentPage) { + query_params += `?offset=${(currentPage - 1) * (limit ? limit : 10)}`; + } + + if (limit) { + query_params += `${query_params.length > 0 ? "&" : "?"}limit=${limit}`; + } + + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/user/${username}${query_params}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); if (response.status === 200) { return response.json(); diff --git a/apps/frontend/src/components/Header/header.tsx b/apps/frontend/src/components/Header/header.tsx index a9b2df7091..387699f5db 100644 --- a/apps/frontend/src/components/Header/header.tsx +++ b/apps/frontend/src/components/Header/header.tsx @@ -11,14 +11,17 @@ import { Header as AntdHeader } from "antd/es/layout/layout"; import { useRouter } from "next/navigation"; import "./styles.scss"; import DropdownButton from "antd/es/dropdown/dropdown-button"; -import { LogoutOutlined, UserOutlined } from "@ant-design/icons"; +import { + HistoryOutlined, + LogoutOutlined, + UserOutlined, +} from "@ant-design/icons"; import { deleteToken } from "@/app/services/login-store"; interface HeaderProps { selectedKey: string[] | undefined; } const Header = (props: HeaderProps): JSX.Element => { - const { push } = useRouter(); // Stores the details for the header buttons const items = [ @@ -45,10 +48,20 @@ const Header = (props: HeaderProps): JSX.Element => { onClick: () => push("/profile"), }, { - type: "divider", + key: 1, + label: ( +
+ History +
+ ), + onClick: () => push("/history"), }, { key: 2, + type: "divider", + }, + { + key: 3, label: (
Logout From 76ce6c73aab6ec4a6076ae73d77594ab623b236f Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 05:16:41 +0800 Subject: [PATCH 18/20] fix: pagination --- apps/frontend/src/app/history/page.tsx | 2 +- apps/frontend/src/app/question/[id]/page.tsx | 2 +- apps/history-service/models/pagination.go | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/app/history/page.tsx b/apps/frontend/src/app/history/page.tsx index 8a6023e4a2..a1942ea8ad 100644 --- a/apps/frontend/src/app/history/page.tsx +++ b/apps/frontend/src/app/history/page.tsx @@ -44,7 +44,7 @@ export default function QuestionPage() { // Handler for change in page jumper const onPageJump: PaginationProps["onChange"] = (pageNumber) => { setPaginationParams((prev) => { - loadQuestionHistories(pageNumber, paginationParams.limit); + loadQuestionHistories(pageNumber, prev.limit); return { ...paginationParams, currentPage: pageNumber }; }); }; diff --git a/apps/frontend/src/app/question/[id]/page.tsx b/apps/frontend/src/app/question/[id]/page.tsx index 4a03f91ee9..d05e1a7535 100644 --- a/apps/frontend/src/app/question/[id]/page.tsx +++ b/apps/frontend/src/app/question/[id]/page.tsx @@ -88,7 +88,7 @@ export default function QuestionPage() { // Handler for change in page jumper const onPageJump: PaginationProps["onChange"] = (pageNumber) => { setPaginationParams((prev) => { - loadQuestionHistories(pageNumber, paginationParams.limit); + loadQuestionHistories(pageNumber, prev.limit); return { ...paginationParams, currentPage: pageNumber }; }); }; diff --git a/apps/history-service/models/pagination.go b/apps/history-service/models/pagination.go index 6553325500..3b9f1d38c3 100644 --- a/apps/history-service/models/pagination.go +++ b/apps/history-service/models/pagination.go @@ -18,8 +18,12 @@ func PaginateResponse(limit, offset int, histories []SubmissionHistory) *Histori if end > len(histories) { end = len(histories) } - paginatedHistory = histories[start:end] + } else { + start = 0 + offset = 0 + end = limit } + paginatedHistory = histories[start:end] // Calculate pagination info totalCount := len(histories) From 4c4ada94bd16a24adf49ce240bf89bf4a7296dd8 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 05:19:28 +0800 Subject: [PATCH 19/20] feat: remove buttons in match found modal --- apps/frontend/src/app/history/page.tsx | 2 +- .../src/app/matching/modalContent/MatchFoundContent.tsx | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/frontend/src/app/history/page.tsx b/apps/frontend/src/app/history/page.tsx index a1942ea8ad..e881b3118b 100644 --- a/apps/frontend/src/app/history/page.tsx +++ b/apps/frontend/src/app/history/page.tsx @@ -91,7 +91,7 @@ export default function QuestionPage() { const columns = [ { - title: "Title", + title: "Question Title", dataIndex: "title", key: "title", }, diff --git a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx index e7ff88bc7b..340a6e13c2 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx @@ -53,12 +53,6 @@ const MatchFoundContent: React.FC = ({
Joining in... {formatTime(totalSeconds)}
- -
); }; From d00edf0d46a4890f50be1916438d02565f822702 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Mon, 4 Nov 2024 05:27:16 +0800 Subject: [PATCH 20/20] feat: remove cancel button in match found modal --- .../src/app/matching/MatchingModal.tsx | 10 +- .../modalContent/JoinedMatchContent.tsx | 103 ++++++++---------- .../modalContent/MatchFoundContent.tsx | 3 + 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/apps/frontend/src/app/matching/MatchingModal.tsx b/apps/frontend/src/app/matching/MatchingModal.tsx index cbb4e67a75..29b1eaa2c6 100644 --- a/apps/frontend/src/app/matching/MatchingModal.tsx +++ b/apps/frontend/src/app/matching/MatchingModal.tsx @@ -103,8 +103,14 @@ const MatchingModal: React.FC = ({ matchingState.info.matchedUser ); localStorage.setItem("collabId", matchingState.info.matchId); - localStorage.setItem("questionDocRefId", matchingState.info.questionDocRefId); - localStorage.setItem("matchedTopics", matchingState.info.matchedTopics.join(",")); + localStorage.setItem( + "questionDocRefId", + matchingState.info.questionDocRefId + ); + localStorage.setItem( + "matchedTopics", + matchingState.info.matchedTopics.join(",") + ); // Redirect to collaboration page router.push(`/collaboration/${matchingState.info.matchId}`); diff --git a/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx b/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx index 55e40888f1..940d4fb64d 100644 --- a/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx @@ -1,64 +1,55 @@ -import React from 'react'; -import { - Avatar, - } from 'antd'; -import { - UserOutlined, -} from '@ant-design/icons'; -import 'typeface-montserrat'; -import './styles.scss'; -import { handleCancelMatch } from '../handlers'; -import { formatTime } from '@/utils/DateTime'; - +import React from "react"; +import { Avatar } from "antd"; +import { UserOutlined } from "@ant-design/icons"; +import "typeface-montserrat"; +import "./styles.scss"; +import { handleCancelMatch } from "../handlers"; +import { formatTime } from "@/utils/DateTime"; interface Props { - cancel(): void - name1: string, // user's username - name2: string, // matched user's username + cancel(): void; + name1: string; // user's username + name2: string; // matched user's username } -const JoinedMatchContent: React.FC = ({cancel, name1: me, name2: you}) => { - const matchAlreadyJoined = () => { - throw new Error('Match already joined.'); - } +const JoinedMatchContent: React.FC = ({ + cancel, + name1: me, + name2: you, +}) => { + const matchAlreadyJoined = () => { + throw new Error("Match already joined."); + }; - return ( -
-
-
- } /> -
{me}
-
- - - -
- } /> -
{you}
-
-
-
Match Found!
-
- Waiting for others... {formatTime(83)} -
- - + return ( +
+
+
+ } /> +
{me}
- ) -} + + + +
+ } /> +
{you}
+
+
+
Match Found!
+
Waiting for others...
+ +
+ ); +}; export default JoinedMatchContent; diff --git a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx index 340a6e13c2..4efb1a1a04 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx @@ -53,6 +53,9 @@ const MatchFoundContent: React.FC = ({
Joining in... {formatTime(totalSeconds)}
+
); };