diff --git a/.gitignore b/.gitignore
index 845959db09..4e9218f2e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
-**/.env
\ No newline at end of file
+**/.env
+.idea
+*.log
+main.exe
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000000..fa9613cdf2
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+cd ./peerprep && npx lint-staged
\ No newline at end of file
diff --git a/.lefthook.yml b/.lefthook.yml
new file mode 100644
index 0000000000..d57925957c
--- /dev/null
+++ b/.lefthook.yml
@@ -0,0 +1,10 @@
+#pre-commit:
+# parallel: true
+# commands:
+# golangci-lint:
+# glob: "*.go"
+# run: golangci-lint run {staged_files}
+# lint-staged:
+# root: "peerprep/"
+# glob: "*.(ts|tsx|css|scss|md|json)"
+# run: npx lint-staged
diff --git a/backend/common/question_struct.go b/backend/common/question_struct.go
index cdbf8221a0..1ac42365c4 100644
--- a/backend/common/question_struct.go
+++ b/backend/common/question_struct.go
@@ -1,4 +1,4 @@
-// defines the JSON format of quesitons.
+// defines the JSON format of questions.
package common
type Question struct {
@@ -17,3 +17,8 @@ type FrontendQuestion struct {
TopicTags []string `json:"topicTags"`
Content string `json:"content"`
}
+
+type MatchingQuestion struct {
+ TopicTags []string `json:"topicTags"`
+ Difficulty string `json:"difficulty"`
+}
\ No newline at end of file
diff --git a/backend/database/database_interactions.go b/backend/database/database_interactions.go
index b9bbc4a251..f079ded7f8 100644
--- a/backend/database/database_interactions.go
+++ b/backend/database/database_interactions.go
@@ -10,6 +10,7 @@ import (
"peerprep/common"
"go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@@ -34,6 +35,36 @@ func (db *QuestionDB) GetAllQuestionsWithQuery(
return questions, nil
}
+func (db *QuestionDB) GetOneQuestionWithQuery(
+ logger *common.Logger,
+ filter bson.D,
+) (*common.Question, error) {
+ // Define the aggregation pipeline with the $match and $sample stages
+ pipeline := mongo.Pipeline{
+ {{Key: "$match", Value: filter}},
+ {{Key: "$sample", Value: bson.D{{Key: "size", Value: 1}}}},
+ }
+
+ // Execute the aggregation pipeline
+ cursor, err := db.questions.Aggregate(context.Background(), pipeline)
+ if err != nil {
+ logger.Log.Error("Error retrieving questions: ", err.Error())
+ return nil, err
+ }
+
+ var questions []common.Question
+ if err = cursor.All(context.Background(), &questions); err != nil {
+ logger.Log.Error("Error decoding questions: ", err.Error())
+ return nil, err
+ }
+
+ if len(questions) == 0 {
+ return nil, nil
+ }
+
+ return &questions[0], nil
+}
+
func (db *QuestionDB) AddQuestion(logger *common.Logger, question *common.Question) (int, error) {
if db.QuestionExists(question) {
logger.Log.Warn("Cannot add question: question already exists")
diff --git a/backend/main.go b/backend/main.go
index 5009865e49..747789f078 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -1,4 +1,3 @@
-// this is the main file to run the server
package main
import (
diff --git a/backend/transport/get_random_matching_question.go b/backend/transport/get_random_matching_question.go
new file mode 100644
index 0000000000..1d2191a72a
--- /dev/null
+++ b/backend/transport/get_random_matching_question.go
@@ -0,0 +1,43 @@
+package transport
+
+import (
+ "net/http"
+ "peerprep/common"
+ "peerprep/database"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func GetRandomMatchingQuestion(db *database.QuestionDB, logger *common.Logger) (gin.HandlerFunc) {
+ return func(ctx *gin.Context) {
+ var request common.MatchingQuestion
+
+ err := ctx.BindJSON(&request)
+
+ if err != nil {
+ ctx.JSON(http.StatusBadGateway, "error binding request from JSON")
+ logger.Log.Error("Error converting JSON to matching request:", err.Error())
+ return
+ }
+
+ filter := bson.D{
+ {Key: "topicTags", Value: bson.D{{Key: "$in", Value: request.TopicTags}}},
+ {Key: "difficulty", Value: request.Difficulty},
+ }
+ question, err := db.GetOneQuestionWithQuery(logger, filter)
+
+ if err != nil {
+ ctx.JSON(http.StatusBadGateway, "error retrieving questions from database")
+ return
+ }
+
+ if question == nil {
+ ctx.JSON(http.StatusNotFound, "no questions found matching the request")
+ return
+ }
+
+ ctx.JSON(http.StatusOK, question)
+ logger.Log.Info("matching-service request handled successfully")
+ }
+}
\ No newline at end of file
diff --git a/backend/transport/question_http_requests.go b/backend/transport/question_http_requests.go
index c22deceb0a..cc8c69f159 100644
--- a/backend/transport/question_http_requests.go
+++ b/backend/transport/question_http_requests.go
@@ -19,12 +19,13 @@ func SetAllEndpoints(router *gin.Engine, db *database.QuestionDB, logger *common
router.DELETE("/questions/delete/:id", DeleteQuestionWithLogger(db, logger))
router.PUT("/questions/replace/:id", ReplaceQuestionWithLogger(db, logger))
router.GET("/health", HealthCheck(logger))
+ router.POST("/match", GetRandomMatchingQuestion(db, logger))
}
// enable CORS for the frontend
func SetCors(router *gin.Engine, origin string) {
router.Use(cors.New(cors.Config{
- AllowOrigins: []string{origin},
+ AllowOrigins: []string{"http://host.docker.internal", origin},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Content-Length", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
diff --git a/backend/transport/replace_question.go b/backend/transport/replace_question.go
index 2c4b783571..cdfcd1c176 100644
--- a/backend/transport/replace_question.go
+++ b/backend/transport/replace_question.go
@@ -33,21 +33,21 @@ func ReplaceQuestionWithLogger(db *database.QuestionDB, logger *common.Logger) g
return
}
- if id_param >= db.FindNextQuestionId() {
- logger.Log.Info(
- "Attempting to update a question with an ID greater than next ID, creating a new question",
- )
- status, err := db.AddQuestion(logger, &new_question)
-
- if err != nil {
- ctx.JSON(status, err.Error())
- return
- }
-
- ctx.JSON(status, "Question added successfully")
- logger.Log.Info("Question added successfully")
- return
- }
+ //if id_param >= db.FindNextQuestionId() {
+ // logger.Log.Info(
+ // "Attempting to update a question with an ID greater than next ID, creating a new question",
+ // )
+ // status, err := db.AddQuestion(logger, &new_question)
+ //
+ // if err != nil {
+ // ctx.JSON(status, err.Error())
+ // return
+ // }
+ //
+ // ctx.JSON(status, "Question added successfully")
+ // logger.Log.Info("Question added successfully")
+ // return
+ //}
logger.Log.Info("Replacing question with ID: ", id_param)
new_question.Id = id_param
diff --git a/collab/Dockerfile b/collab/Dockerfile
new file mode 100644
index 0000000000..be3fb98083
--- /dev/null
+++ b/collab/Dockerfile
@@ -0,0 +1,16 @@
+FROM golang:1.20
+
+WORKDIR /collab
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# Build
+RUN CGO_ENABLED=0 GOOS=linux go build -o /collab/app
+
+EXPOSE 4000
+
+# Run
+CMD ["/collab/app"]
\ No newline at end of file
diff --git a/collab/go.mod b/collab/go.mod
new file mode 100644
index 0000000000..baf51b62a7
--- /dev/null
+++ b/collab/go.mod
@@ -0,0 +1,39 @@
+module collab
+go 1.20
+
+require (
+ github.com/gin-gonic/gin v1.10.0
+ github.com/go-redis/redis/v8 v8.11.5
+ github.com/gorilla/websocket v1.5.3
+)
+
+require (
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/collab/go.sum b/collab/go.sum
new file mode 100644
index 0000000000..dc30be64b8
--- /dev/null
+++ b/collab/go.sum
@@ -0,0 +1,100 @@
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/collab/main.go b/collab/main.go
new file mode 100644
index 0000000000..f5b602bcbb
--- /dev/null
+++ b/collab/main.go
@@ -0,0 +1,471 @@
+package main
+
+import (
+ "collab/verify"
+ "context"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+)
+
+// types
+const (
+ AUTH = "auth"
+ AUTH_SUCCESS = "auth_success"
+ AUTH_FAIL = "auth_fail"
+ CLOSE_SESSION = "close_session"
+ CONTENT_CHANGE = "content_change"
+ PING = "ping"
+)
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+}
+
+type Client struct {
+ conn *websocket.Conn
+ roomID string
+ authenticated bool
+}
+
+type Hub struct {
+ clients map[*Client]bool
+ workspaces map[string]string
+ broadcast chan Message
+ register chan *Client
+ unregister chan *Client
+ mutex sync.Mutex
+}
+
+type Message struct {
+ Type string `json:"type"`
+ RoomID string `json:"roomId"`
+ Content string `json:"data"`
+ UserID string `json:"userId"`
+ Token string `json:"token"`
+ MatchHash string `json:"matchHash"`
+}
+
+func verifyToken(token string) (bool, string) {
+ client := &http.Client{}
+ USER_SERVICE_URI := os.Getenv("USER_SERVICE_URI")
+ if USER_SERVICE_URI == "" {
+ USER_SERVICE_URI = "http://localhost:3001"
+ }
+ req, err := http.NewRequest("GET", USER_SERVICE_URI+"/auth/verify-token", nil)
+ if err != nil {
+ log.Println("Error creating request:", err)
+ return false, ""
+ }
+
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Println("Error making request:", err)
+ return false, ""
+ }
+ defer resp.Body.Close()
+
+ var response struct {
+ Message string `json:"message"`
+ Data struct {
+ ID string `json:"id"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ IsAdmin bool `json:"isAdmin"`
+ } `json:"data"`
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Println("Error reading response body:", err)
+ return false, ""
+ }
+
+ // Unmarshal the response body into the struct
+ if err := json.Unmarshal(body, &response); err != nil {
+ log.Println("Error unmarshaling response:", err)
+ return false, ""
+ }
+
+ // Check if the token was verified successfully
+ if resp.StatusCode != http.StatusOK {
+ log.Println("Token verification failed with status:", resp.Status)
+ return false, ""
+ }
+
+ // Return true and the ID from the response
+ return true, response.Data.ID
+}
+
+// NewHub creates a new hub instance
+func NewHub() *Hub {
+ return &Hub{
+ clients: make(map[*Client]bool),
+ workspaces: make(map[string]string),
+ broadcast: make(chan Message),
+ register: make(chan *Client),
+ unregister: make(chan *Client),
+ }
+}
+
+// Run starts the hub's main loop
+func (h *Hub) Run() {
+ for {
+ select {
+ case client := <-h.register:
+ h.mutex.Lock()
+ h.clients[client] = true
+ h.mutex.Unlock()
+
+ case client := <-h.unregister:
+ h.mutex.Lock()
+ if _, ok := h.clients[client]; ok {
+ delete(h.clients, client)
+ client.conn.Close()
+ }
+ h.mutex.Unlock()
+
+ case message := <-h.broadcast:
+ h.mutex.Lock()
+ // Update the current workspace for this RoomID
+ if message.Content != "" {
+ h.workspaces[message.RoomID] = message.Content
+ }
+ for client := range h.clients {
+ if client.roomID == message.RoomID {
+
+ log.Println("Original message: ", message)
+
+ msgJson, _ := json.Marshal(message)
+
+ log.Printf("Sending message to client: %s", msgJson)
+
+ err := client.conn.WriteMessage(websocket.TextMessage,
+ msgJson,
+ )
+ if err != nil {
+ log.Printf("Error sending message: %v", err)
+ client.conn.Close()
+ delete(h.clients, client)
+ }
+ }
+ }
+ h.mutex.Unlock()
+ }
+
+ }
+}
+
+// ServeWs handles WebSocket requests
+func serveWs(
+ hub *Hub, c *gin.Context,
+ roomMappings *verify.RoomMappings,
+ persistMappings *verify.PersistMappings,
+) {
+ log.Println("handler called!")
+ roomID := c.Query("roomID")
+ if roomID == "" {
+ http.Error(c.Writer, "roomID required", http.StatusBadRequest)
+ return
+ }
+
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ log.Println("Failed to upgrade:", err)
+ return
+ }
+
+ client := &Client{conn: conn, roomID: roomID}
+ hub.register <- client
+
+ go handleMessages(client, hub, roomMappings, persistMappings)
+}
+
+func authenticateClient(
+ token string, match string, client *Client,
+ roomMappings *verify.RoomMappings,
+ persistMappings *verify.PersistMappings,
+) bool {
+ ok, userID := verifyToken(token)
+ if !ok {
+ log.Println("bad token in request")
+ return false
+ }
+ return verify.VerifyRoomAndMoveToPersist(
+ roomMappings, client.roomID, userID, match, persistMappings)
+}
+
+func authenticateClientNoMatch(
+ token string, client *Client,
+ persistMappings *verify.PersistMappings,
+) bool {
+ ok, userID := verifyToken(token)
+ if !ok {
+ log.Println("bad token in request")
+ return false
+ }
+ return verify.VerifyPersist(persistMappings, client.roomID, userID)
+}
+
+func handleMessages(
+ client *Client, hub *Hub,
+ roomMappings *verify.RoomMappings,
+ persistMappings *verify.PersistMappings,
+) {
+ defer func() {
+ hub.unregister <- client
+ }()
+
+ for {
+ _, message, err := client.conn.ReadMessage()
+ if err != nil {
+ log.Printf("WebSocket error: %v", err)
+ break
+ }
+
+ log.Printf("Raw message received: %s", string(message))
+
+ var msgData Message
+ if err := json.Unmarshal(message, &msgData); err != nil {
+ log.Printf("Failed to parse message: %v", err)
+ continue
+ }
+
+ log.Printf("Raw message parsed: %s", msgData)
+
+ if msgData.Type == AUTH {
+ token := msgData.Token
+ if token == "" {
+ log.Println("Authentication failed - no token attached")
+
+ msg := Message{
+ Type: AUTH_FAIL,
+ RoomID: client.roomID,
+ Content: "Authentication failed - no token attached",
+ }
+ msgJson, _ := json.Marshal(msg)
+ client.conn.WriteMessage(websocket.TextMessage, msgJson)
+ client.conn.Close()
+ break
+ }
+ isSuccess := false
+ match := msgData.MatchHash
+ if match != "" &&
+ !authenticateClient(token, match, client, roomMappings, persistMappings) {
+ log.Println(
+ "failed to find a matching room from match hash, proceeding with persistence check",
+ )
+ }
+ // I will ping the persistent map even if I've found it in the original map
+ if !authenticateClientNoMatch(token, client, persistMappings) {
+ log.Println("failed to find a persistent room")
+ isSuccess = false
+ } else {
+ isSuccess = true
+ }
+ if !isSuccess {
+ msg := Message{
+ Type: AUTH_FAIL,
+ RoomID: client.roomID,
+ Content: "Authentication failed",
+ }
+ msgJson, _ := json.Marshal(msg)
+ client.conn.WriteMessage(websocket.TextMessage, msgJson)
+
+ client.conn.Close()
+ break
+ }
+ client.authenticated = true
+
+ serverContent := hub.workspaces[client.roomID]
+
+ newMsg := Message{
+ Type: AUTH_SUCCESS,
+ RoomID: client.roomID,
+ Content: serverContent,
+ }
+ msgJson, _ := json.Marshal(newMsg)
+ client.conn.WriteMessage(websocket.TextMessage, msgJson)
+
+ log.Println("Client authenticated successfully")
+ }
+
+ // old logic before type changes
+ // if msgData["type"] == "ping" {
+ // //receives ping from client1, need to send a ping to client2
+ // //eventually, if present, client2 will send the ping back, which will be broadcasted back to client1.
+
+ // userID, _ := msgData["userId"].(string)
+ // request := Message {
+ // RoomID: client.roomID,
+ // UserID: userID,
+ // Content: []byte("ping request"),
+ // }
+
+ // hub.broadcast <- request
+ // }
+
+ if msgData.Type == CLOSE_SESSION {
+ closeMessage := Message{
+ Type: CLOSE_SESSION,
+ RoomID: client.roomID,
+ Content: "The session has been closed by a user.",
+ }
+ targetId := msgData.UserID
+ ownData, err := persistMappings.Conn.HGetAll(context.Background(), targetId).Result()
+ if err != nil {
+ log.Printf("Error retrieving data for userID %s: %v", targetId, err)
+ } else {
+ // delete room under user id if it curr matches the room ID
+ ownRoomId := ownData["roomId"]
+ if ownRoomId == client.roomID {
+ _, err := persistMappings.Conn.Del(context.Background(), targetId).Result()
+ if err != nil {
+ log.Printf("Error deleting data for userID %s: %v", targetId, err)
+ }
+ }
+ // delete room under other user if it curr matches the room ID
+ otherUser := ownData["otherUser"]
+ othRoomId, err := persistMappings.Conn.HGet(context.Background(), otherUser, "roomId").Result()
+ if err != nil {
+ log.Printf("Error retrieving data for otherUser %s: %v", otherUser, err)
+ } else {
+ if othRoomId == client.roomID {
+ _, err := persistMappings.Conn.Del(context.Background(), otherUser).Result()
+ if err != nil {
+ log.Printf("Error deleting data for other user %s: %v", otherUser, err)
+ }
+ }
+ }
+ }
+ hub.broadcast <- closeMessage
+ } else if msgData.Type == CONTENT_CHANGE {
+ // Broadcast the message to other clients
+ hub.broadcast <- Message{
+ RoomID: client.roomID,
+ Content: msgData.Content,
+ Type: msgData.Type,
+ UserID: msgData.UserID,
+ }
+ } else if msgData.Type == PING {
+ // Broadcast the message to other clients
+ hub.broadcast <- Message{
+ RoomID: client.roomID,
+ Type: msgData.Type,
+ UserID: msgData.UserID,
+ }
+
+ extendExpiryTime(msgData.UserID, persistMappings)
+ } else {
+ log.Printf("Unknown message type: %s", msgData.Type)
+ }
+ }
+}
+
+func extendExpiryTime(userId string, persistMappings *verify.PersistMappings) {
+
+ ctx := context.Background()
+ if err := persistMappings.Conn.Expire(ctx, userId, time.Minute*10).Err(); err != nil {
+ log.Println("Error extending room time on ping: ", err.Error())
+ } else {
+
+ log.Printf("expiration reset for 10 minutes for user %s: ", userId)
+ }
+
+}
+
+type ClientWorkspace struct {
+ Clients int `json:"clients"`
+ Workspace string `json:"workspace"`
+}
+
+// Status endpoint that shows the number of clients and the current color for each roomID
+func statusHandler(hub *Hub) gin.HandlerFunc {
+
+ return func(c *gin.Context) {
+ hub.mutex.Lock()
+ defer hub.mutex.Unlock()
+
+ status := make(map[string]ClientWorkspace)
+ for client := range hub.clients {
+ roomID := client.roomID
+ currentStatus, ok := status[roomID]
+ if !ok {
+ // Initialize status for a new roomID
+ status[roomID] = ClientWorkspace{
+ Clients: 1,
+ Workspace: hub.workspaces[roomID],
+ }
+ } else {
+ // Update the client count for an existing roomID
+ status[roomID] = ClientWorkspace{
+ Clients: currentStatus.Clients + 1,
+ Workspace: hub.workspaces[roomID],
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, status)
+ }
+}
+
+func main() {
+ r := gin.Default()
+ hub := NewHub()
+ go hub.Run()
+
+ REDIS_URI := os.Getenv("REDIS_URI")
+ if REDIS_URI == "" {
+ REDIS_URI = "localhost:9190"
+ }
+
+ REDIS_ROOM_MAPPING := 1
+ REDIS_ROOM_PERSIST := 2
+
+ if os.Getenv("REDIS_ROOM_MAPPING") != "" {
+ num, err := strconv.Atoi(os.Getenv("REDIS_ROOM_MAPPING"))
+ if err != nil {
+ log.Fatal("DB no of room map is badly formatted" + err.Error())
+ } else {
+ REDIS_ROOM_MAPPING = num
+ }
+ }
+
+ if os.Getenv("REDIS_ROOM_PERSIST") != "" {
+ num, err := strconv.Atoi(os.Getenv("REDIS_ROOM_PERSIST"))
+ if err != nil {
+ log.Fatal("DB no of room persistance store is badly formatted" + err.Error())
+ } else {
+ REDIS_ROOM_PERSIST = num
+ }
+ }
+
+ roomMappings := verify.InitialiseRoomMappings(REDIS_URI, REDIS_ROOM_MAPPING)
+ persistMappings := verify.InitialisePersistMappings(REDIS_URI, REDIS_ROOM_PERSIST)
+
+ // WebSocket connection endpoint
+ r.GET("/ws", func(c *gin.Context) {
+ serveWs(hub, c, roomMappings, persistMappings)
+ })
+
+ // Status endpoint
+ r.GET("/status", statusHandler(hub))
+
+ PORT := os.Getenv("PORT")
+ if PORT == "" {
+ PORT = ":4000"
+ }
+ r.Run(PORT)
+}
diff --git a/collab/verify/persistMappings.go b/collab/verify/persistMappings.go
new file mode 100644
index 0000000000..b0945a617b
--- /dev/null
+++ b/collab/verify/persistMappings.go
@@ -0,0 +1,36 @@
+package verify
+
+import (
+ "context"
+ "log"
+
+ redis "github.com/go-redis/redis/v8"
+)
+
+// same as room mappings, but separated for type safety
+type PersistMappings struct {
+ Conn *redis.Client
+}
+
+func InitialisePersistMappings(addr string, db_num int) *PersistMappings {
+ conn := redis.NewClient(&redis.Options{
+ Addr: addr,
+ DB: db_num,
+ })
+
+ return &PersistMappings{
+ Conn: conn,
+ }
+}
+
+func VerifyPersist(persistMappings *PersistMappings, roomID string, userID string) bool {
+ data, err := persistMappings.Conn.HGetAll(context.Background(), userID).Result()
+ if err != nil {
+ log.Printf("Error retrieving data for userID %s: %v", userID, err)
+ return false
+ }
+
+ log.Printf("current roomID: %s, expected roomID: %s", data["roomId"], roomID)
+
+ return data["roomId"] == roomID
+}
diff --git a/collab/verify/roomMappings.go b/collab/verify/roomMappings.go
new file mode 100644
index 0000000000..9d3e45728c
--- /dev/null
+++ b/collab/verify/roomMappings.go
@@ -0,0 +1,67 @@
+package verify
+
+import (
+ "context"
+ "log"
+ //"time"
+
+ redis "github.com/go-redis/redis/v8"
+)
+
+// same as client mappings, but separated for type safety
+type RoomMappings struct {
+ Conn *redis.Client
+}
+
+func InitialiseRoomMappings(addr string, db_num int) *RoomMappings {
+ conn := redis.NewClient(&redis.Options{
+ Addr: addr,
+ DB: db_num,
+ })
+
+ return &RoomMappings{
+ Conn: conn,
+ }
+}
+
+func VerifyRoomAndMoveToPersist(
+ roomMappings *RoomMappings,
+ roomID string,
+ userId string,
+ matchHash string,
+ persistMappings *PersistMappings,
+) bool {
+ ctx := context.Background()
+ data, err := roomMappings.Conn.HGetAll(ctx, matchHash).Result()
+ if err != nil {
+ log.Printf("Error retrieving data for matchHash %s: %v", matchHash, err)
+ return false
+ }
+
+ if data["roomId"] != roomID || data["thisUser"] != userId {
+ log.Printf("Mismatch in room data and user data")
+ return false
+ }
+
+ roomMappings.Conn.Del(ctx, matchHash);
+ persistentRoom := map[string]interface{}{
+ "roomId": roomID,
+ "otherUser": data["otherUser"],
+ "requestTime": data["requestTime"],
+ }
+
+ // this always overrides the persistent room
+ if err := persistMappings.Conn.HSet(ctx, userId, persistentRoom).Err(); err != nil {
+ log.Printf("error sending room to persistent storage: %s", err.Error())
+ }
+
+ /*
+ if err := persistMappings.Conn.Expire(ctx, userId, 20 * time.Second).Err(); err != nil {
+ log.Printf("error setting expiration for persisting room storage: %s", err.Error())
+ } else {
+ log.Printf("expiration set for 10 minutes for user %s: ", userId)
+
+ }
+ */
+ return true
+}
diff --git a/comms/.env.example b/comms/.env.example
new file mode 100644
index 0000000000..da78d27a07
--- /dev/null
+++ b/comms/.env.example
@@ -0,0 +1 @@
+FRONTEND=
diff --git a/comms/.gitignore b/comms/.gitignore
new file mode 100644
index 0000000000..37d7e73486
--- /dev/null
+++ b/comms/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.env
diff --git a/comms/Dockerfile b/comms/Dockerfile
new file mode 100644
index 0000000000..b895ccf667
--- /dev/null
+++ b/comms/Dockerfile
@@ -0,0 +1,8 @@
+FROM node:lts-alpine3.20
+
+WORKDIR /comms
+COPY package*.json ./
+RUN npm install --force
+COPY . .
+EXPOSE 4001
+CMD ["npm", "run", "dev"]
diff --git a/comms/package-lock.json b/comms/package-lock.json
new file mode 100644
index 0000000000..b8c4b5da93
--- /dev/null
+++ b/comms/package-lock.json
@@ -0,0 +1,1377 @@
+{
+ "name": "collabcomms",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "collabcomms",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.21.1",
+ "nodemon": "^3.1.7",
+ "socket.io": "^4.8.1"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.17",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.8.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz",
+ "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.8"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
+ "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.4.1",
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.10",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
+ "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/comms/package.json b/comms/package.json
new file mode 100644
index 0000000000..b068cb72e8
--- /dev/null
+++ b/comms/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "collabcomms",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "express": "^4.21.1",
+ "nodemon": "^3.1.7",
+ "socket.io": "^4.8.1"
+ }
+}
diff --git a/comms/server.js b/comms/server.js
new file mode 100644
index 0000000000..fd69980ebd
--- /dev/null
+++ b/comms/server.js
@@ -0,0 +1,67 @@
+const express = require("express");
+const http = require("http");
+const app = express();
+
+const server = http.createServer(app);
+const io = require("socket.io")(server, {
+ path: '/comms',
+ cors: {
+ // temporarily use * to allow all origins
+ origin: `*`,
+ methods: ["GET", "POST"]
+ }
+});
+
+io.on("connection", (socket) => {
+ // emit endCall to the room it was in.
+ socket.on("disconnecting", () => {
+ // for each room in the disconnecting socket...
+ socket.rooms.forEach((target) => {
+ // ignoring the room matching its own id...
+ if (target === socket.id) {
+ return;
+ }
+ // get the user ids within the room...
+ io.sockets.adapter.rooms
+ .get(target)
+ .forEach(
+ (id) => {
+ // and for each user id in the room not matching
+ // its own id...
+ if (id === socket.id) {
+ return;
+ }
+ // leave the target room...
+ io.sockets.sockets.get(id).leave(target);
+ console.log(id + " leaves " + target);
+ // then tell the user id to endCall.
+ io.to(id).emit("endCall");
+ }
+ );
+ });
+ });
+
+ // join a room and inform the peer that the other person has joined
+ socket.on("joinRoom", (data) => {
+ console.log(socket.id + " is joining " + data.target);
+ socket.join(data.target);
+ socket.to(data.target).emit("peerConnected");
+ });
+
+ // propagate the socket events for starting and handshaking a call forward.
+ socket.on("startCall", (data) => {
+ console.log(socket.id + " is starting call in "+ data.target);
+ socket.to(data.target).emit("startCall", {
+ signal: data.signalData
+ });
+ });
+
+ socket.on("handshakeCall", (data) => {
+ console.log("handshaking call in " + data.target);
+ socket.to(data.target).emit("handshakeCall", {
+ signal: data.signal
+ });
+ });
+});
+
+server.listen(4001, () => console.log("comms server is running on port 4001"));
diff --git a/compose.yaml b/compose.yaml
index f01f53b6e3..d3d02ce05c 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,6 +1,7 @@
services:
peerprep:
build: peerprep
+ image: distractedcat/peerprep
env_file:
- peerprep/.env
ports:
@@ -14,8 +15,10 @@ services:
path: peerprep
target: /frontend
+
user-service:
build: user-service
+ image: distractedcat/user-service
volumes:
- /user-service/node_modules
env_file:
@@ -30,9 +33,9 @@ services:
path: user-service
target: /user-service
-
backend:
build: backend
+ image: distractedcat/backend
env_file:
- backend/.env
ports:
@@ -43,7 +46,147 @@ services:
path: backend
target: backend/app
+ # blob and mq
+ redis:
+ image: redis
+ container_name: redis-peerprep
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "9190:6379"
+
+ rabbitmq:
+ image: rabbitmq:3-management
+ container_name: rabbitmq
+ environment:
+ RABBITMQ_DEFAULT_USER: grp14
+ RABBITMQ_DEFAULT_PASS: grp14
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "9100:5672"
+ - "9101:15672"
+ healthcheck:
+ test: rabbitmq-diagnostics check_port_connectivity
+ interval: 30s
+ timeout: 30s
+ retries: 10
+
+ matching-service:
+ build: matching-service
+ image: distractedcat/matching-service
+ env_file:
+ - matching-service/.env
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ develop:
+ watch:
+ - action: rebuild
+ path: matching-service
+ target: matching-service/app
+ depends_on:
+ rabbitmq:
+ condition: service_healthy
+
+ matching-service-api:
+ build: matching-service-api
+ image: distractedcat/matching-service-api
+ env_file:
+ - matching-service-api/.env
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "9200:9200"
+ develop:
+ watch:
+ - action: rebuild
+ path: matching-service
+ target: matching-service/app
+ depends_on:
+ rabbitmq:
+ condition: service_healthy
+
+ storage-blob-api:
+ build: storage-blob-api
+ image: distractedcat/storage-blob-api
+ env_file:
+ - storage-blob-api/.env
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "9300:9300"
+ develop:
+ watch:
+ - action: rebuild
+ path: storage-blob-api
+ target: storage-blob-api/app
+ depends_on:
+ - redis
+
+ collab:
+ build: collab
+ image: distractedcat/collab
+ env_file:
+ - collab/.env
+ ports:
+ - "4000:4000"
+ develop:
+ watch:
+ - action: rebuild
+ path: collab
+ target: collab/app
+
+ formatter:
+ build: formatter
+ image: distractedcat/formatter
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ ports:
+ - "5000:5000"
+ develop:
+ watch:
+ - action: sync
+ path: formatter
+ target: formatter/app
+
+ comms:
+ build: comms
+ image: distractedcat/comms
+ #env_file:
+ #- comms/.env
+ ports:
+ - "4001:4001"
+ develop:
+ watch:
+ - action: sync
+ path: comms
+ target: comms/app
+
+ nginx:
+ build: nginx
+ image: distractedcat/nginx
+ volumes:
+ - ./nginx/nginx.conf:/etc/nginx/internal.conf
+ ports:
+ - "70:70"
+ depends_on:
+ - peerprep
+ - backend
+ - user-service
+ - storage-blob-api
+ - matching-service-api
+
+ inbound-gateway:
+ build: inbound-gateway
+ image: wzwren/inbound-gateway
+ ports:
+ - "80:80"
+ volumes:
+ - ./inbound-gateway/nginx.conf:/etc/nginx/external.conf
+ depends_on:
+ - peerprep
+ - comms
# mongo:
# image: "mongo:latest"
# ports:
-# - "27017:27017"
\ No newline at end of file
+# - "27017:27017"
diff --git a/formatter/Dockerfile b/formatter/Dockerfile
new file mode 100644
index 0000000000..5a91905e7e
--- /dev/null
+++ b/formatter/Dockerfile
@@ -0,0 +1,11 @@
+FROM alpine:latest
+
+RUN apk add --no-cache clang clang-extra-tools python3 py3-pip nodejs npm
+RUN python3 -m venv /src/.venv
+RUN /src/.venv/bin/pip install black fastapi uvicorn
+RUN npm install -g prettier esprima
+ENV PATH="/src/.venv/bin:$PATH"
+WORKDIR /src
+COPY src /src
+EXPOSE 5000
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000"]
diff --git a/formatter/src/app.py b/formatter/src/app.py
new file mode 100644
index 0000000000..861b06a15c
--- /dev/null
+++ b/formatter/src/app.py
@@ -0,0 +1,94 @@
+import asyncio
+import logging
+import subprocess
+
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+
+from parsers import validate_python_code, validate_javascript_code, validate_cpp_code
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI()
+
+origins = [
+ "http://localhost:3000",
+ "http://localhost:80",
+ "http://localhost",
+ "*",
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+class CodeFormatRequest(BaseModel):
+ code: str
+
+
+async def run_formatter(command: list, code: str, timeout: int = 5) -> str:
+ try:
+ if command[0] == "black":
+ await validate_python_code(code)
+ elif command[0] == "prettier":
+ await validate_javascript_code(code)
+ elif command[0] == "clang-format":
+ await validate_cpp_code(code)
+
+ process = await asyncio.create_subprocess_exec(
+ *command,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ stdout, stderr = await asyncio.wait_for(process.communicate(input=code.encode()), timeout=timeout)
+
+ if process.returncode != 0:
+ error_message = stderr.decode().strip()
+ logger.error(f"Formatter error: {error_message}")
+ raise HTTPException(status_code=500, detail=f"Formatter error: {error_message}")
+
+ return stdout.decode().strip()
+
+ except HTTPException as http_err:
+ raise http_err
+ except asyncio.TimeoutError:
+ logger.error("Timed out")
+ raise HTTPException(status_code=504, detail="Timed out")
+ except Exception as e:
+ logger.error(f"Unexpected error: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
+
+
+@app.post("/format/python")
+async def format_python(request: CodeFormatRequest):
+ print(request.code)
+ command = ["black", "-q", "-"]
+ formatted_code = await run_formatter(command, request.code)
+ return {"formatted_code": formatted_code}
+
+
+@app.post("/format/cpp")
+async def format_cpp(request: CodeFormatRequest):
+ command = ["clang-format"]
+ formatted_code = await run_formatter(command, request.code)
+ return {"formatted_code": formatted_code}
+
+
+@app.post("/format/javascript")
+async def format_javascript(request: CodeFormatRequest):
+ command = ["prettier", "--stdin-filepath", "file.js"]
+ formatted_code = await run_formatter(command, request.code)
+ return {"formatted_code": formatted_code}
+
+
+@app.get("/")
+async def hello_world():
+ return {"Status": "Hello from formatter"}
diff --git a/formatter/src/js_syntax_check.js b/formatter/src/js_syntax_check.js
new file mode 100644
index 0000000000..85dada5e79
--- /dev/null
+++ b/formatter/src/js_syntax_check.js
@@ -0,0 +1,11 @@
+const esprima = require("esprima");
+
+const code = process.argv[2];
+
+try {
+ esprima.parseScript(code);
+ console.log("Parsed OK");
+} catch (error) {
+ console.error("Failed to parse code: ", error);
+ process.exit(1);
+}
diff --git a/formatter/src/parsers.py b/formatter/src/parsers.py
new file mode 100644
index 0000000000..30ee9b40db
--- /dev/null
+++ b/formatter/src/parsers.py
@@ -0,0 +1,48 @@
+import ast
+import asyncio
+import logging
+import subprocess
+
+from fastapi import HTTPException
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+async def validate_python_code(code: str):
+ try:
+ ast.parse(code)
+ except SyntaxError as e:
+ logger.error(f"Syntax error: {e}")
+ raise HTTPException(status_code=400, detail=f"Syntax error: {e}")
+
+
+async def validate_javascript_code(code: str):
+ try:
+ result = subprocess.run(
+ ["node", "js_syntax_check.js", code],
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ if result.returncode != 0:
+ raise HTTPException(status_code=400, detail="Failed to parse JavaScript code: " + result.stderr.strip())
+ except subprocess.TimeoutExpired:
+ logger.error("JavaScript syntax check timed out")
+ raise HTTPException(status_code=504, detail="JavaScript syntax check timed out")
+
+
+async def validate_cpp_code(code: str):
+ try:
+ result = subprocess.run(
+ ["clang", "-fsyntax-only", "-x", "c++", "-"],
+ input=code,
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ if result.returncode != 0:
+ raise HTTPException(status_code=400, detail="Failed to parse C++ code: " + result.stderr.strip())
+ except subprocess.TimeoutExpired:
+ logger.error("C++ syntax check timed out")
+ raise HTTPException(status_code=504, detail="C++ syntax check timed out")
diff --git a/inbound-gateway/Dockerfile b/inbound-gateway/Dockerfile
new file mode 100644
index 0000000000..5a5b07759c
--- /dev/null
+++ b/inbound-gateway/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:alpine
+COPY nginx.conf /etc/nginx/external.conf
+EXPOSE 80
+CMD ["nginx", "-c", "external.conf", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/inbound-gateway/README.md b/inbound-gateway/README.md
new file mode 100644
index 0000000000..28f1e06e06
--- /dev/null
+++ b/inbound-gateway/README.md
@@ -0,0 +1,7 @@
+nginx is dockerised, just have to run `docker compose up --build` as usual.
+
+if edits are made to the local nginx.conf file, following command must be run to see changes reflected in docker:
+
+`docker exec cs3219-ay2425s1-project-g14-nginx-1 nginx -s reload`
+
+(or just exec `nginx -s reload` in the container directly)
diff --git a/inbound-gateway/nginx.conf b/inbound-gateway/nginx.conf
new file mode 100644
index 0000000000..409205e839
--- /dev/null
+++ b/inbound-gateway/nginx.conf
@@ -0,0 +1,56 @@
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include mime.types;
+
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ upstream peerprep {
+ server peerprep:3000;
+ }
+
+ server {
+ listen 80;
+
+ location / {
+ proxy_pass http://peerprep/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host; # port 80 implicitly removes this port
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+
+ location /comms/ {
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header X-NginX-Proxy false;
+
+
+ proxy_pass http://comms:4001;
+
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+ }
+}
\ No newline at end of file
diff --git a/kubernetes/apply_k8s_configs.ps1 b/kubernetes/apply_k8s_configs.ps1
new file mode 100644
index 0000000000..2fa6321fc4
--- /dev/null
+++ b/kubernetes/apply_k8s_configs.ps1
@@ -0,0 +1,30 @@
+# Array of Kubernetes YAML files to apply
+$files = @(
+ "backend-deployment.yaml",
+ "backend-service.yaml",
+ "collab-service-deployment.yaml",
+ "collab-service-service.yaml",
+ "matching-service-api-deployment.yaml",
+ "matching-service-api-service.yaml",
+ "matching-service-deployment.yaml",
+ "nginx-deployment.yaml",
+ "nginx-service.yaml",
+ "peerprep-deployment.yaml",
+ "peerprep-service.yaml",
+ "rabbitmq-service.yaml",
+ "rabbitmq-statefulset.yaml",
+ "redis-service.yaml",
+ "redis-statefulset.yaml",
+ "storage-blob-api-deployment.yaml",
+ "storage-blob-api-service.yaml",
+ "user-service-deployment.yaml",
+ "user-service-service.yaml"
+)
+
+# Loop through each file and apply it using kubectl
+foreach ($file in $files) {
+ Write-Output "Applying $file..."
+ kubectl apply -f $file
+}
+
+Write-Output "All files applied."
diff --git a/kubernetes/backend-HPA.yaml b/kubernetes/backend-HPA.yaml
new file mode 100644
index 0000000000..ce822006f7
--- /dev/null
+++ b/kubernetes/backend-HPA.yaml
@@ -0,0 +1,18 @@
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: backend
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: backend
+ minReplicas: 1
+ maxReplicas: 5
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 80
\ No newline at end of file
diff --git a/kubernetes/backend-deployment.yaml b/kubernetes/backend-deployment.yaml
new file mode 100644
index 0000000000..84fe8f807d
--- /dev/null
+++ b/kubernetes/backend-deployment.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: backend
+spec:
+ selector:
+ matchLabels:
+ app: backend
+ template:
+ metadata:
+ labels:
+ app: backend
+ spec:
+ containers:
+ - name: backend
+ image: distractedcat/backend:1.0.0
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: MONGODB_URI
+ value: mongodb+srv://nxtms3:np0aUdwlMkISiUia@questions.bh9su.mongodb.net/?retryWrites=true&w=majority&appName=questions
+ - name: PORT
+ value: :9090
+ - name: CORS_ORIGIN
+ value: http://peerprep:3000
+ ports:
+ - containerPort: 9090
+ name: backend
+ resources:
+ requests:
+ cpu: "100m"
+ memory: "128Mi"
+ limits:
+ cpu: "200m"
+ memory: "256Mi"
\ No newline at end of file
diff --git a/kubernetes/backend-service.yaml b/kubernetes/backend-service.yaml
new file mode 100644
index 0000000000..52f340d801
--- /dev/null
+++ b/kubernetes/backend-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: backend
+spec:
+ selector:
+ app: backend
+ ports:
+ - protocol: TCP
+ port: 9090
+ targetPort: 9090
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/collab-service-deployment.yaml b/kubernetes/collab-service-deployment.yaml
new file mode 100644
index 0000000000..e9bf53b72b
--- /dev/null
+++ b/kubernetes/collab-service-deployment.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: collab
+spec:
+ selector:
+ matchLabels:
+ app: collab
+ template:
+ metadata:
+ labels:
+ app: collab
+ spec:
+ containers:
+ - name: collab
+ image: distractedcat/collab:3.0.0
+ imagePullPolicy: Always
+ env:
+ - name: PORT
+ value: :4000
+ - name: REDIS_URI
+ value: redis:6379
+ - name: USER_SERVICE_URI
+ value: http://user-service:3001
+ ports:
+ - containerPort: 4000
+ name: collab
\ No newline at end of file
diff --git a/kubernetes/collab-service-service.yaml b/kubernetes/collab-service-service.yaml
new file mode 100644
index 0000000000..4e0da5d4bd
--- /dev/null
+++ b/kubernetes/collab-service-service.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: collab
+spec:
+ selector:
+ app: collab
+ ports:
+ - protocol: TCP
+ port: 4000
+ targetPort: 4000
+ #nodePort: 31000
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/comms-deployment.yaml b/kubernetes/comms-deployment.yaml
new file mode 100644
index 0000000000..9d4727de11
--- /dev/null
+++ b/kubernetes/comms-deployment.yaml
@@ -0,0 +1,20 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: comms
+spec:
+ selector:
+ matchLabels:
+ app: comms
+ template:
+ metadata:
+ labels:
+ app: comms
+ spec:
+ containers:
+ - name: comms
+ image: distractedcat/comms:1.0.0
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 4001
+ name: comms
\ No newline at end of file
diff --git a/kubernetes/comms-service.yaml b/kubernetes/comms-service.yaml
new file mode 100644
index 0000000000..67440d5f04
--- /dev/null
+++ b/kubernetes/comms-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: comms
+spec:
+ selector:
+ app: comms
+ ports:
+ - protocol: TCP
+ port: 4001
+ targetPort: 4001
+ type: LoadBalancer
\ No newline at end of file
diff --git a/kubernetes/formatter-deployment.yaml b/kubernetes/formatter-deployment.yaml
new file mode 100644
index 0000000000..11e09a5052
--- /dev/null
+++ b/kubernetes/formatter-deployment.yaml
@@ -0,0 +1,20 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: formatter
+spec:
+ selector:
+ matchLabels:
+ app: formatter
+ template:
+ metadata:
+ labels:
+ app: formatter
+ spec:
+ containers:
+ - name: formatter
+ image: distractedcat/formatter:1.0.0
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 5000
+ name: formatter
\ No newline at end of file
diff --git a/kubernetes/formatter-service.yaml b/kubernetes/formatter-service.yaml
new file mode 100644
index 0000000000..5b834a90a4
--- /dev/null
+++ b/kubernetes/formatter-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: formatter
+spec:
+ selector:
+ app: formatter
+ ports:
+ - protocol: TCP
+ port: 5000
+ targetPort: 5000
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/matching-service-api-deployment.yaml b/kubernetes/matching-service-api-deployment.yaml
new file mode 100644
index 0000000000..db1feb25f3
--- /dev/null
+++ b/kubernetes/matching-service-api-deployment.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: matching-service-api
+spec:
+ selector:
+ matchLabels:
+ app: matching-service-api
+ template:
+ metadata:
+ labels:
+ app: matching-service-api
+ spec:
+ containers:
+ - name: matching-service-api
+ image: distractedcat/matching-service-api:3.0.0
+ imagePullPolicy: Always
+ env:
+ - name: PORT
+ value: :9200
+ - name: RABBIT_URI
+ value: amqp://grp14:grp14@rabbitmq/
+ - name: CORS_ORIGIN
+ value: http://peerprep:3000
+ ports:
+ - containerPort: 9200
+ name: matching-api
\ No newline at end of file
diff --git a/kubernetes/matching-service-api-service.yaml b/kubernetes/matching-service-api-service.yaml
new file mode 100644
index 0000000000..4abdff43e5
--- /dev/null
+++ b/kubernetes/matching-service-api-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: matching-service-api
+spec:
+ selector:
+ app: matching-service-api
+ ports:
+ - protocol: TCP
+ port: 9200
+ targetPort: 9200
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/matching-service-deployment.yaml b/kubernetes/matching-service-deployment.yaml
new file mode 100644
index 0000000000..ae6f585090
--- /dev/null
+++ b/kubernetes/matching-service-deployment.yaml
@@ -0,0 +1,24 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: matching-service
+spec:
+ selector:
+ matchLabels:
+ app: matching-service
+ template:
+ metadata:
+ labels:
+ app: matching-service
+ spec:
+ containers:
+ - name: matching-service
+ image: distractedcat/matching-service:3.0.0
+ imagePullPolicy: Always
+ env:
+ - name: RABBIT_URI
+ value: amqp://grp14:grp14@rabbitmq/
+ - name: REDIS_URI
+ value: redis:6379
+ - name: BACKEND_MATCH_URI
+ value: http://backend:9090/match
\ No newline at end of file
diff --git a/kubernetes/nginx-deployment.yaml b/kubernetes/nginx-deployment.yaml
new file mode 100644
index 0000000000..6290ad1ed1
--- /dev/null
+++ b/kubernetes/nginx-deployment.yaml
@@ -0,0 +1,20 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: distractedcat/nginx:2.0.0
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 80
\ No newline at end of file
diff --git a/kubernetes/nginx-service.yaml b/kubernetes/nginx-service.yaml
new file mode 100644
index 0000000000..753599711b
--- /dev/null
+++ b/kubernetes/nginx-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+spec:
+ selector:
+ app: nginx
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 80
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/peerprep-deployment.yaml b/kubernetes/peerprep-deployment.yaml
new file mode 100644
index 0000000000..e8172cf317
--- /dev/null
+++ b/kubernetes/peerprep-deployment.yaml
@@ -0,0 +1,37 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: peerprep
+spec:
+ selector:
+ matchLabels:
+ app: peerprep
+ template:
+ metadata:
+ labels:
+ app: peerprep
+ spec:
+ containers:
+ - name: peerprep
+ image: distractedcat/peerprep:3.0.0
+ imagePullPolicy: Always
+ env:
+ - name: NEXT_PUBLIC_QUESTION_SERVICE
+ value: backend
+ - name: NEXT_PUBLIC_USER_SERVICE
+ value: users
+ - name: NEXT_PUBLIC_MATCHING_SERVICE
+ value: matchmaking
+ - name: NEXT_PUBLIC_STORAGE_BLOB
+ value: blob
+ - name: NEXT_PUBLIC_NGINX
+ value: http://nginx:80
+ - name: NEXT_PUBLIC_COLLAB
+ value: collab
+ - name: DEV_ENV
+ value: not
+ - name: NEXT_PUBLIC_FORMATTER
+ value: formatter
+ ports:
+ - containerPort: 3000
+ name: peerprep
\ No newline at end of file
diff --git a/kubernetes/peerprep-service.yaml b/kubernetes/peerprep-service.yaml
new file mode 100644
index 0000000000..c0940c81e8
--- /dev/null
+++ b/kubernetes/peerprep-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: peerprep
+spec:
+ selector:
+ app: peerprep
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
+ type: LoadBalancer
\ No newline at end of file
diff --git a/kubernetes/rabbitmq-service.yaml b/kubernetes/rabbitmq-service.yaml
new file mode 100644
index 0000000000..0792128eb3
--- /dev/null
+++ b/kubernetes/rabbitmq-service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: rabbitmq
+spec:
+ type: ClusterIP # or ClusterIP if you don't need external access
+ ports:
+ - name: rabbitmq
+ port: 5672
+ targetPort: 5672
+ - name: management
+ port: 15672
+ targetPort: 15672
+ selector:
+ app: rabbitmq
\ No newline at end of file
diff --git a/kubernetes/rabbitmq-statefulset.yaml b/kubernetes/rabbitmq-statefulset.yaml
new file mode 100644
index 0000000000..9361107b25
--- /dev/null
+++ b/kubernetes/rabbitmq-statefulset.yaml
@@ -0,0 +1,33 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: rabbitmq
+spec:
+ serviceName: "rabbitmq"
+ replicas: 1
+ selector:
+ matchLabels:
+ app: rabbitmq
+ template:
+ metadata:
+ labels:
+ app: rabbitmq
+ spec:
+ containers:
+ - name: rabbitmq
+ image: rabbitmq:3-management
+ ports:
+ - containerPort: 5672
+ - containerPort: 15672
+ env:
+ - name: RABBITMQ_DEFAULT_USER
+ value: "grp14"
+ - name: RABBITMQ_DEFAULT_PASS
+ value: "grp14"
+ readinessProbe:
+ exec:
+ command: ["rabbitmq-diagnostics", "check_port_connectivity"]
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 10
+ failureThreshold: 10
\ No newline at end of file
diff --git a/kubernetes/redis-service.yaml b/kubernetes/redis-service.yaml
new file mode 100644
index 0000000000..b9823a58e8
--- /dev/null
+++ b/kubernetes/redis-service.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+spec:
+ type: ClusterIP # Or ClusterIP depending on your access needs
+ ports:
+ - name: redis
+ port: 6379
+ targetPort: 6379
+ # nodePort: 9190 # Optional, if you want to specify a NodePort
+ selector:
+ app: redis
\ No newline at end of file
diff --git a/kubernetes/redis-statefulset.yaml b/kubernetes/redis-statefulset.yaml
new file mode 100644
index 0000000000..1c5239be70
--- /dev/null
+++ b/kubernetes/redis-statefulset.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: redis
+spec:
+ serviceName: "redis"
+ replicas: 1 # Number of Redis instances
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ containers:
+ - name: redis
+ image: redis
+ ports:
+ - containerPort: 6379 # Redis default port
+ volumeMounts:
+ - name: redis-storage
+ mountPath: /data # Mount path for Redis data
+ volumeClaimTemplates:
+ - metadata:
+ name: redis-storage
+ spec:
+ accessModes: ["ReadWriteOnce"]
+ resources:
+ requests:
+ storage: 1Gi
\ No newline at end of file
diff --git a/kubernetes/storage-blob-api-deployment.yaml b/kubernetes/storage-blob-api-deployment.yaml
new file mode 100644
index 0000000000..5d4f890faa
--- /dev/null
+++ b/kubernetes/storage-blob-api-deployment.yaml
@@ -0,0 +1,25 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: storage-blob-api
+spec:
+ selector:
+ matchLabels:
+ app: storage-blob-api
+ template:
+ metadata:
+ labels:
+ app: storage-blob-api
+ spec:
+ containers:
+ - name: storage-blob-api
+ image: distractedcat/storage-blob-api:3.0.0
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: PORT
+ value: :9300
+ - name: REDIS_URI
+ value: redis:6379
+ ports:
+ - containerPort: 9300
+ name: storage-api
\ No newline at end of file
diff --git a/kubernetes/storage-blob-api-service.yaml b/kubernetes/storage-blob-api-service.yaml
new file mode 100644
index 0000000000..71a313a4d1
--- /dev/null
+++ b/kubernetes/storage-blob-api-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: storage-blob-api
+spec:
+ selector:
+ app: storage-blob-api
+ ports:
+ - protocol: TCP
+ port: 9300
+ targetPort: 9300
+ type: ClusterIP
\ No newline at end of file
diff --git a/kubernetes/user-service-deployment.yaml b/kubernetes/user-service-deployment.yaml
new file mode 100644
index 0000000000..d0c7f9c80d
--- /dev/null
+++ b/kubernetes/user-service-deployment.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: user-service
+spec:
+ selector:
+ matchLabels:
+ app: user-service
+ template:
+ metadata:
+ labels:
+ app: user-service
+ spec:
+ containers:
+ - name: user-service
+ image: distractedcat/user-service:1.0.0
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: DB_CLOUD_URI
+ value: mongodb+srv://modemhappy:dvVbAqwq5lEOr6WN@cluster0.1oehu.mongodb.net/peerprep?retryWrites=true&w=majority&appName=Cluster0
+ - name: DB_LOCAL_URI
+ value: mongodb://127.0.0.1:27017/peerprepUserServiceDB
+ - name: PORT
+ value: "3001"
+ - name: ENV
+ value: PROD
+ - name: JWT_SECRET
+ value: you-can-replace-this-with-your-own-secret
+ ports:
+ - containerPort: 3001
+ name: user-service
\ No newline at end of file
diff --git a/kubernetes/user-service-service.yaml b/kubernetes/user-service-service.yaml
new file mode 100644
index 0000000000..c8ca7215b3
--- /dev/null
+++ b/kubernetes/user-service-service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: user-service
+spec:
+ selector:
+ app: user-service
+ ports:
+ - protocol: TCP
+ port: 3001
+ targetPort: 3001
+ type: ClusterIP
\ No newline at end of file
diff --git a/lc/.gitignore b/lc/.gitignore
index 2eea525d88..bf1633d2ff 100644
--- a/lc/.gitignore
+++ b/lc/.gitignore
@@ -1 +1,2 @@
-.env
\ No newline at end of file
+.env
+content/
\ No newline at end of file
diff --git a/matching-service-api/.env.example b/matching-service-api/.env.example
new file mode 100644
index 0000000000..28af422e62
--- /dev/null
+++ b/matching-service-api/.env.example
@@ -0,0 +1,4 @@
+PORT=9200
+RABBIT_URI=
+CORS_ORIGIN=
+
diff --git a/matching-service-api/.gitignore b/matching-service-api/.gitignore
new file mode 100644
index 0000000000..fbf828d63a
--- /dev/null
+++ b/matching-service-api/.gitignore
@@ -0,0 +1 @@
+log
\ No newline at end of file
diff --git a/matching-service-api/Dockerfile b/matching-service-api/Dockerfile
new file mode 100644
index 0000000000..6399e54f32
--- /dev/null
+++ b/matching-service-api/Dockerfile
@@ -0,0 +1,16 @@
+FROM golang:1.20
+
+WORKDIR /matching-service-api
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# Build
+RUN CGO_ENABLED=0 GOOS=linux go build -o /matching-service-api/app
+
+EXPOSE 9200
+
+# Run
+CMD ["/matching-service-api/app"]
\ No newline at end of file
diff --git a/matching-service-api/go.mod b/matching-service-api/go.mod
new file mode 100644
index 0000000000..5bff70930f
--- /dev/null
+++ b/matching-service-api/go.mod
@@ -0,0 +1,37 @@
+module matching-service-api
+
+go 1.20
+
+require (
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/cors v1.7.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/gin-gonic/gin v1.10.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/streadway/amqp v1.1.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/matching-service-api/go.sum b/matching-service-api/go.sum
new file mode 100644
index 0000000000..d9b585090b
--- /dev/null
+++ b/matching-service-api/go.sum
@@ -0,0 +1,91 @@
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
+github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
+github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/matching-service-api/log/matching_service_api.log b/matching-service-api/log/matching_service_api.log
new file mode 100644
index 0000000000..a8bd4b4e2a
--- /dev/null
+++ b/matching-service-api/log/matching_service_api.log
@@ -0,0 +1,170 @@
+time="2024-10-13T11:40:53+08:00" level=info msg="Server started at time: 2024-10-13 11:40:53.9651404 +0800 +08 m=+0.020315501"
+time="2024-10-13T11:44:08+08:00" level=info msg="Server started at time: 2024-10-13 11:44:08.2137412 +0800 +08 m=+0.019447901"
+time="2024-10-13T11:45:18+08:00" level=info msg="Server started at time: 2024-10-13 11:45:18.8711385 +0800 +08 m=+0.018465501"
+time="2024-10-13T11:46:37+08:00" level=info msg="Server started at time: 2024-10-13 11:46:37.0114407 +0800 +08 m=+0.019357101"
+time="2024-10-13T11:46:49+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-13T11:47:24+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-13T11:52:42+08:00" level=info msg="Server started at time: 2024-10-13 11:52:42.820679 +0800 +08 m=+0.019017101"
+time="2024-10-13T11:52:49+08:00" level=error msg="error publishing message:Exception (504) Reason: \"channel/connection is not open\""
+time="2024-10-13T11:52:49+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-13T11:54:36+08:00" level=info msg="Server started at time: 2024-10-13 11:54:36.2585996 +0800 +08 m=+0.018451501"
+time="2024-10-13T11:54:39+08:00" level=error msg="error publishing message:Exception (504) Reason: \"channel/connection is not open\""
+time="2024-10-13T11:54:39+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-13T12:54:45+08:00" level=info msg="Server started at time: 2024-10-13 12:54:45.809209 +0800 +08 m=+0.018372701"
+time="2024-10-13T12:55:54+08:00" level=info msg="Server started at time: 2024-10-13 12:55:54.7209082 +0800 +08 m=+0.017350501"
+time="2024-10-13T12:55:59+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-13T12:56:13+08:00" level=info msg="request from user hello successfully published"
+time="2024-10-15T21:16:09+08:00" level=info msg="Server started at time: 2024-10-15 21:16:09.5830829 +0800 +08 m=+0.018717201"
+time="2024-10-15T21:19:11+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-15T21:24:05+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-15T21:24:19+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-15T21:27:07+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-15T21:27:16+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-15T21:34:56+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-15T21:35:04+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T11:47:58+08:00" level=info msg="Server started at time: 2024-10-16 11:47:58.9097737 +0800 +08 m=+0.018180201"
+time="2024-10-16T11:58:13+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T11:58:19+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-16T12:04:09+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T12:04:12+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-16T12:06:43+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-16T12:06:47+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T12:12:09+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T12:12:14+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-16T12:16:10+08:00" level=info msg="request from user user1 successfully published"
+time="2024-10-16T12:16:15+08:00" level=info msg="request from user user2 successfully published"
+time="2024-10-25T22:40:44+08:00" level=info msg="Server started at time: 2024-10-25 22:40:44.6771508 +0800 +08 m=+0.090065801"
+time="2024-10-25T23:25:28+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T15:25:31+08:00" level=info msg="Server started at time: 2024-10-27 15:25:31.7266589 +0800 +08 m=+0.095761501"
+time="2024-10-27T15:26:55+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T15:26:55+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T15:29:09+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T15:29:10+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:08:08+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:08:10+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:16:30+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:16:33+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:19:10+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:19:10+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:19:41+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:19:41+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:21:10+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:21:11+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:21:12+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:21:23+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:21:24+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:22:44+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:22:44+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:27:03+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:27:05+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:27:48+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:27:50+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:28:28+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:28:28+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:29:33+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:29:33+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:34:11+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:34:12+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:35:54+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-27T16:35:56+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:36:54+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-27T16:36:55+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:01:44+08:00" level=info msg="Server started at time: 2024-10-28 13:01:44.2402807 +0800 +08 m=+0.155052801"
+time="2024-10-28T13:06:50+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T13:06:50+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:06:51+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T13:06:52+08:00" level=info msg="request from user 671f1ba30b0e2619aaa4dd7a successfully published"
+time="2024-10-28T13:12:22+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T13:12:24+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:50:55+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:51:03+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T13:57:58+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:57:59+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T13:59:34+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T13:59:35+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:00:45+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:00:46+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:04:50+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:04:51+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:10:02+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:10:03+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:17:49+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T14:17:49+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:17:50+08:00" level=info msg="request from user 671f1ba30b0e2619aaa4dd7a successfully published"
+time="2024-10-28T14:17:50+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:20:11+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:20:12+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T14:31:29+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T14:31:29+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T20:45:31+08:00" level=info msg="Server started at time: 2024-10-28 20:45:31.7844893 +0800 +08 m=+0.088887001"
+time="2024-10-28T20:45:42+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T20:45:42+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T20:46:17+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T20:46:18+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T20:53:53+08:00" level=info msg="Server started at time: 2024-10-28 20:53:53.3049845 +0800 +08 m=+0.077096601"
+time="2024-10-28T20:56:49+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T20:56:49+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T20:58:05+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T20:58:06+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:00:59+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:00:59+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:01:35+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:01:35+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:09:10+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:09:11+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:32:14+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:32:15+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:33:06+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:33:07+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:33:37+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:33:37+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:34:03+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:34:05+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:34:46+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:34:46+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:36:18+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:36:19+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:39:20+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:39:21+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:40:23+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:40:25+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:41:06+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:41:07+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:43:19+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:43:21+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:43:49+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:43:50+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:45:11+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T21:45:12+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:48:21+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:48:21+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:49:30+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:49:30+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:49:46+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:49:46+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:52:11+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:52:11+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:54:06+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:54:07+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:54:48+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:54:48+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:57:05+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:57:05+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:57:21+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:57:22+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T21:57:56+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T21:57:57+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:00:23+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:00:23+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:03:12+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:03:13+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:03:49+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:03:50+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:05:59+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:05:59+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:07:05+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:07:05+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:07:25+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
+time="2024-10-28T22:07:25+08:00" level=info msg="request from user 6702c9a3f1217a1b2123575b successfully published"
+time="2024-10-28T22:09:51+08:00" level=info msg="request from user 671f1bdf0b0e2619aaa4dd85 successfully published"
+time="2024-10-28T22:09:52+08:00" level=info msg="request from user 67188e6ebe7127ec5d472f15 successfully published"
diff --git a/matching-service-api/main.go b/matching-service-api/main.go
new file mode 100644
index 0000000000..1b72b5c0b8
--- /dev/null
+++ b/matching-service-api/main.go
@@ -0,0 +1,72 @@
+package main
+
+import(
+
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ "matching-service-api/models"
+ "github.com/joho/godotenv"
+
+ "github.com/gin-gonic/gin"
+ "matching-service-api/transport"
+)
+
+func main() {
+ //initialise logger file and directory if they do not exist
+
+ err := godotenv.Load(".env")
+ if err != nil {
+ log.Fatal("Error loading environment variables: " + err.Error())
+ }
+
+ ORIGIN := os.Getenv("CORS_ORIGIN")
+ if ORIGIN == "" {
+ ORIGIN = "http://localhost:3000"
+ }
+ PORT := os.Getenv("PORT")
+ if PORT == "" {
+ PORT = ":9200"
+ }
+
+ logger := models.NewLogger()
+
+ logDirectory := "./log"
+
+ if err := os.MkdirAll(logDirectory, 0755); err != nil {
+ logger.Log.Error("Failed to create log directory: " + err.Error())
+ }
+
+ logFile, err := os.OpenFile("./log/matching_service_api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+
+ if err != nil {
+ logger.Log.Warn("Failed to log to file, using default stderr")
+ }
+
+ defer logFile.Close()
+
+ logger.Log.Out = logFile
+
+ URI := os.Getenv("RABBIT_URI")
+ if URI == "" {
+ logger.Log.Fatal("Error finding the queue URI")
+ }
+ channel, err := models.InitialiseQueue(URI)
+
+ if err != nil {
+ panic(err)
+ }
+
+ defer channel.Connection.Close()
+ defer channel.Channel.Close()
+
+ router := gin.Default()
+ transport.SetCors(router, ORIGIN)
+ transport.SetAllEndpoints(router, channel, logger)
+
+ logger.Log.Info(fmt.Sprintf("Server started at time: %s", time.Now().String()))
+
+ router.Run(PORT)
+}
\ No newline at end of file
diff --git a/matching-service-api/mappings/client_mappings.go b/matching-service-api/mappings/client_mappings.go
new file mode 100644
index 0000000000..5833f90858
--- /dev/null
+++ b/matching-service-api/mappings/client_mappings.go
@@ -0,0 +1,18 @@
+package mappings
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "io"
+)
+
+func GenerateMatchingHash() (string, error) {
+ bytes := make([]byte, 16)
+
+ if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
+ return "", errors.New("Failed to generate random matching hash" + err.Error())
+ }
+
+ return hex.EncodeToString(bytes), nil
+}
diff --git a/matching-service-api/models/logger.go b/matching-service-api/models/logger.go
new file mode 100644
index 0000000000..2149803b88
--- /dev/null
+++ b/matching-service-api/models/logger.go
@@ -0,0 +1,15 @@
+package models
+
+import (
+ "github.com/sirupsen/logrus"
+)
+
+type Logger struct {
+ Log *logrus.Logger
+}
+
+func NewLogger() *Logger {
+ return &Logger {
+ Log: logrus.New(),
+ }
+}
\ No newline at end of file
diff --git a/matching-service-api/models/producer_queue.go b/matching-service-api/models/producer_queue.go
new file mode 100644
index 0000000000..1f0435551d
--- /dev/null
+++ b/matching-service-api/models/producer_queue.go
@@ -0,0 +1,57 @@
+package models
+
+import (
+ rabbit "github.com/streadway/amqp"
+ "log"
+ "time"
+)
+
+type ProducerQueue struct {
+ Connection *rabbit.Connection
+ Channel *rabbit.Channel
+
+ Queue rabbit.Queue
+}
+
+func InitialiseQueue(URI string) (*ProducerQueue, error) {
+ var connection *rabbit.Connection
+ var err error
+
+ for i := 0; i < 10; i++ {
+ connection, err = rabbit.Dial(URI)
+ if err == nil {
+ break
+ }
+ log.Printf("Could not establish connection to RabbitMQ, retrying in 5 seconds... (%d/10)\n", i+1)
+ time.Sleep(5 * time.Second)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ channel, err := connection.Channel()
+
+ if err != nil {
+ return nil, err
+ }
+
+ queue, err := channel.QueueDeclare(
+ "match_queue", // name of the queue
+ true, // durable
+ false, // delete when unused
+ false, // exclusive
+ false, // no-wait
+ nil, // arguments
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &ProducerQueue{
+ Connection: connection,
+ Channel: channel,
+ Queue: queue,
+ }, nil
+}
\ No newline at end of file
diff --git a/matching-service-api/models/request.go b/matching-service-api/models/request.go
new file mode 100644
index 0000000000..910658e08c
--- /dev/null
+++ b/matching-service-api/models/request.go
@@ -0,0 +1,13 @@
+package models
+
+type Request struct {
+ MatchHash string
+
+ UserId string `json:"userId"`
+
+ TopicTags []string `json:"topicTags"`
+
+ Difficulty string `json:"difficulty"`
+
+ RequestTime string `json:"requestTime"`
+}
\ No newline at end of file
diff --git a/matching-service-api/transport/endpoints.go b/matching-service-api/transport/endpoints.go
new file mode 100644
index 0000000000..fc171adcb4
--- /dev/null
+++ b/matching-service-api/transport/endpoints.go
@@ -0,0 +1,25 @@
+package transport
+
+import (
+ "matching-service-api/models"
+ "time"
+
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+)
+
+func SetAllEndpoints(router *gin.Engine, producerQueue *models.ProducerQueue, logger *models.Logger) {
+ router.POST("/request", HandleRequest(producerQueue, logger))
+
+}
+
+func SetCors(router *gin.Engine, origin string) {
+ router.Use(cors.New(cors.Config{
+ AllowOrigins: []string{origin},
+ AllowMethods: []string{"POST", "OPTIONS"},
+ AllowHeaders: []string{"Origin", "Content-Type", "Content-Length", "Authorization"},
+ ExposeHeaders: []string{"Content-Length"},
+ AllowCredentials: true,
+ MaxAge: 2 * time.Minute,
+ }))
+}
diff --git a/matching-service-api/transport/request_handler.go b/matching-service-api/transport/request_handler.go
new file mode 100644
index 0000000000..ca6424be5b
--- /dev/null
+++ b/matching-service-api/transport/request_handler.go
@@ -0,0 +1,78 @@
+package transport
+
+import (
+ "encoding/json"
+ "fmt"
+ "matching-service-api/models"
+ "matching-service-api/mappings"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/streadway/amqp"
+)
+
+func HandleRequest(channel *models.ProducerQueue, logger *models.Logger) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ var req models.Request
+
+ if err := ctx.BindJSON(&req); err != nil {
+ logger.Log.Error("error receiving request: ", err.Error())
+ ctx.JSON(http.StatusBadGateway, gin.H{"error receiving request": err.Error()})
+ return
+ }
+
+ parsedTime, err := time.Parse("2006-01-02 15-04-05", req.RequestTime)
+
+ if err != nil {
+ logger.Log.Error("error parsing the time: ", err.Error())
+ ctx.JSON(http.StatusBadRequest, "error parsing time, ensure time is parsed in YYYY-MM-DD HH:mm:ss format")
+ return
+ }
+
+ // adding this matching hash to reserve user-id as a store for
+ // persistence.
+ matchHash, err := mappings.GenerateMatchingHash();
+ if err != nil {
+ logger.Log.Error("Error: " + err.Error())
+ ctx.JSON(http.StatusInternalServerError, "failed to generate query hash")
+ return
+ }
+ req.MatchHash = matchHash;
+
+ //current time is more than 30 seconds after request time, timeout
+ if time.Now().After(parsedTime.Add(30 * time.Second).Add(-8 * time.Hour)) {
+ logger.Log.Warn("request timeout")
+ ctx.JSON(http.StatusRequestTimeout, "request time is too old")
+ return
+ }
+
+ message, err := json.Marshal(req)
+
+ if err != nil {
+ logger.Log.Error("error converting request to bytes: ", err.Error())
+ ctx.JSON(http.StatusBadGateway, "error processing request")
+ return
+ }
+
+ if err := channel.Channel.Publish(
+ "",
+ channel.Queue.Name,
+ false,
+ false,
+ amqp.Publishing{
+ DeliveryMode: amqp.Persistent,
+ ContentType: "text/plain",
+ Body: []byte(message),
+ }); err != nil {
+ logger.Log.Error("error publishing message:", err.Error())
+ return
+ }
+
+ logger.Log.Info(fmt.Sprintf("request from user %s successfully published", req.UserId))
+ ctx.JSON(http.StatusOK, gin.H{
+ "match_code": matchHash,
+ "message": "processing request",
+ })
+ }
+}
diff --git a/matching-service/.env.example b/matching-service/.env.example
new file mode 100644
index 0000000000..3a9910a6fa
--- /dev/null
+++ b/matching-service/.env.example
@@ -0,0 +1,5 @@
+RABBIT_URI=
+
+REDIS_URI=
+
+BACKEND_MATCH_URI="[BACKEND_URI]/match"
\ No newline at end of file
diff --git a/matching-service/.gitignore b/matching-service/.gitignore
new file mode 100644
index 0000000000..fbf828d63a
--- /dev/null
+++ b/matching-service/.gitignore
@@ -0,0 +1 @@
+log
\ No newline at end of file
diff --git a/matching-service/Dockerfile b/matching-service/Dockerfile
new file mode 100644
index 0000000000..21b5d761e6
--- /dev/null
+++ b/matching-service/Dockerfile
@@ -0,0 +1,14 @@
+FROM golang:1.20
+
+WORKDIR /matching-service
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# Build
+RUN CGO_ENABLED=0 GOOS=linux go build -o /matching-service/app
+
+# Run
+CMD ["/matching-service/app"]
\ No newline at end of file
diff --git a/matching-service/consumer/begin_consuming.go b/matching-service/consumer/begin_consuming.go
new file mode 100644
index 0000000000..414c9994a3
--- /dev/null
+++ b/matching-service/consumer/begin_consuming.go
@@ -0,0 +1,35 @@
+package consumer
+
+import (
+ "matching-service/models"
+ db "matching-service/storage"
+)
+
+func BeginConsuming(mq *models.MessageQueue, logger *models.Logger, clientMappings *db.ClientMappings, roomMappings *db.RoomMappings) {
+ logger.Log.Info("Begin processing requests")
+
+ msgs, err := mq.Channel.Consume(
+ mq.Queue.Name, // queue
+ "", // consumer
+ true, // auto-ack
+ false, // exclusive
+ false, // no-local
+ false, // no-wait
+ nil, // args
+ )
+
+ if err != nil {
+ logger.Log.Error("Error when consuming requests:" + err.Error())
+ }
+
+ forever := make(chan bool)
+
+ go func() {
+ for req := range msgs {
+ if err := Process(req, clientMappings, roomMappings); err != nil {
+ logger.Log.Error(err.Error())
+ }
+ }
+ }()
+ <-forever //blocks forever
+}
diff --git a/matching-service/consumer/process_request.go b/matching-service/consumer/process_request.go
new file mode 100644
index 0000000000..b8a4f77a23
--- /dev/null
+++ b/matching-service/consumer/process_request.go
@@ -0,0 +1,60 @@
+//This is the logic to handle user requests.
+//Each client is internally mapped to all the possible questions that satisfy their request.
+//If another user comes where their possible questions overlap with that of another user, a random question in the intersection is selected.
+
+package consumer
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "matching-service/models"
+ db "matching-service/storage"
+
+ rabbit "github.com/streadway/amqp"
+)
+
+func Process(msg rabbit.Delivery, clientMappings *db.ClientMappings, roomMappings *db.RoomMappings) error {
+ var request models.IncomingRequests
+
+ if err := json.Unmarshal(msg.Body, &request); err != nil {
+ return fmt.Errorf("error unmarshling the request from JSON: %s", err.Error())
+ }
+
+ keys, err := clientMappings.Conn.Keys(context.Background(), "*").Result()
+
+ if err != nil {
+ fmt.Println("error getting keys")
+ } else {
+ fmt.Printf("queue before user match: %s / ",keys)
+ }
+
+ room, err := clientMappings.HandleRequest(request)
+
+ if err != nil {
+ return fmt.Errorf("error handling incoming request: %s", err.Error())
+ }
+
+ keys, err = clientMappings.Conn.Keys(context.Background(), "*").Result()
+
+ if err != nil {
+ fmt.Println("error getting keys")
+ } else {
+ fmt.Printf("queue after user match:%s / ", keys)
+ }
+
+ if err != nil {
+ return fmt.Errorf("error handling incoming request: %s", err.Error())
+ }
+
+ fmt.Println("success handling incoming request!")
+ if room != nil {
+ if err := roomMappings.SendToStorageBlob(room); err != nil {
+ return err
+ }
+
+ fmt.Println("success sending to storage blob")
+ }
+
+ return nil
+}
diff --git a/matching-service/go.mod b/matching-service/go.mod
new file mode 100644
index 0000000000..36d5dc7d8e
--- /dev/null
+++ b/matching-service/go.mod
@@ -0,0 +1,13 @@
+module matching-service
+
+go 1.20
+
+require (
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/go-redis/redis/v8 v8.11.5 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/streadway/amqp v1.1.0 // indirect
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
+)
diff --git a/matching-service/go.sum b/matching-service/go.sum
new file mode 100644
index 0000000000..58a675bd43
--- /dev/null
+++ b/matching-service/go.sum
@@ -0,0 +1,21 @@
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
+github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/matching-service/log/matching_service.log b/matching-service/log/matching_service.log
new file mode 100644
index 0000000000..3150348d5f
--- /dev/null
+++ b/matching-service/log/matching_service.log
@@ -0,0 +1,44 @@
+time="2024-10-15T21:34:37+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-15T21:34:37+08:00" level=info msg="Begin processing requests"
+time="2024-10-16T11:47:46+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-16T11:47:46+08:00" level=info msg="Begin processing requests"
+time="2024-10-16T12:06:22+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-16T12:06:22+08:00" level=info msg="Begin processing requests"
+time="2024-10-16T12:11:53+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-16T12:11:53+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:06:35+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:06:35+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:18:03+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:18:03+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:32:32+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:32:32+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:33:42+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:33:42+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:36:02+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:36:02+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:36:17+08:00" level=error msg="error parsing the time: parsing time \"2024-10-17 22-36-30\" as \"2006-01-02 15-04-05 +0800\": cannot parse \"\" as \" +0800\""
+time="2024-10-17T22:39:31+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:39:31+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:43:06+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:43:06+08:00" level=info msg="Begin processing requests"
+time="2024-10-17T22:51:23+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-17T22:51:23+08:00" level=info msg="Begin processing requests"
+time="2024-10-25T22:40:12+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-25T22:40:12+08:00" level=info msg="Begin processing requests"
+time="2024-10-25T23:25:28+08:00" level=error msg="error handling incoming request: dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:25:08+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-27T15:25:08+08:00" level=info msg="Begin processing requests"
+time="2024-10-27T15:26:55+08:00" level=error msg="error handling incoming request: dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:26:56+08:00" level=error msg="error handling incoming request: dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:28:11+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-27T15:28:11+08:00" level=info msg="Begin processing requests"
+time="2024-10-27T15:29:09+08:00" level=error msg="error handling incoming request: WRONGTYPE Operation against a key holding the wrong kind of value"
+time="2024-10-27T15:29:10+08:00" level=error msg="error handling incoming request: WRONGTYPE Operation against a key holding the wrong kind of value"
+time="2024-10-27T16:08:08+08:00" level=error msg="error handling incoming request: WRONGTYPE Operation against a key holding the wrong kind of value"
+time="2024-10-27T16:08:10+08:00" level=error msg="error handling incoming request: WRONGTYPE Operation against a key holding the wrong kind of value"
+time="2024-10-28T13:01:38+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-28T13:01:38+08:00" level=info msg="Begin processing requests"
+time="2024-10-28T20:44:26+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-28T20:44:26+08:00" level=info msg="Begin processing requests"
+time="2024-10-28T20:53:48+08:00" level=info msg="Beginning consumption from message queue"
+time="2024-10-28T20:53:48+08:00" level=info msg="Begin processing requests"
diff --git a/matching-service/main.go b/matching-service/main.go
new file mode 100644
index 0000000000..e9477c8ce3
--- /dev/null
+++ b/matching-service/main.go
@@ -0,0 +1,141 @@
+package main
+
+import (
+ "log"
+ "os"
+ "strconv"
+ "time"
+
+ "matching-service/consumer"
+ models "matching-service/models"
+ "matching-service/storage"
+
+ "github.com/joho/godotenv"
+ rabbit "github.com/streadway/amqp"
+
+ "github.com/sirupsen/logrus"
+)
+
+func initialiseLogger() (*models.Logger, *os.File) {
+ logger := models.Logger{
+ Log: logrus.New(),
+ }
+
+ logDirectory := "./log"
+
+ if err := os.MkdirAll(logDirectory, 0755); err != nil {
+ logger.Log.Error("Failed to create log directory: " + err.Error())
+ }
+
+ logFile, err := os.OpenFile("./log/matching_service.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+
+ if err != nil {
+ logger.Log.Warn("Failed to log to file, using default stderr")
+ }
+
+
+ logger.Log.Out = logFile
+
+ return &logger, logFile
+}
+
+
+func createRabbitChannel(channel *rabbit.Channel, queue rabbit.Queue) *models.MessageQueue {
+ return &models.MessageQueue{
+ Channel: channel,
+ Queue: queue,
+ }
+}
+
+func main() {
+
+ if err := godotenv.Load(".env"); err != nil {
+ log.Fatal("Error loading the environment variables" + err.Error())
+ }
+
+ URI := os.Getenv("RABBIT_URI")
+
+ if URI == "" {
+ log.Fatal("No Rabbit URI found in environment variables")
+ }
+
+ var connection *rabbit.Connection
+ var err error
+
+ for i := 0; i < 10; i++ {
+ connection, err = rabbit.Dial(URI)
+ if err == nil {
+ break
+ }
+ log.Printf("Could not establish connection to RabbitMQ, retrying in 5 seconds... (%d/10)\n", i+1)
+ time.Sleep(5 * time.Second)
+ }
+
+ if err != nil {
+ log.Fatal("Could not establish connection to RabbitMQ after 10 attempts: " + err.Error())
+ }
+
+ defer connection.Close()
+
+ channel, err := connection.Channel()
+
+ if err != nil {
+ log.Fatal("Could not open a channel" + err.Error())
+ }
+
+ defer channel.Close()
+
+ queue, err := channel.QueueDeclare(
+ "match_queue", // name
+ true, // durable
+ false, // delete when unused
+ false, // exclusive
+ false, // no-wait
+ nil, // arguments
+ )
+
+ if err != nil {
+ log.Fatal("Could not declare a queue" + err.Error())
+ }
+
+ mq := createRabbitChannel(channel, queue)
+
+ logger, logFile := initialiseLogger()
+
+ defer logFile.Close()
+
+ REDIS_URI := os.Getenv("REDIS_URI")
+
+ if REDIS_URI == "" {
+ REDIS_URI = "localhost://9190"
+ }
+
+ REDIS_CLIENT_MAPPING := 0
+ REDIS_ROOM_MAPPING := 1
+
+ if os.Getenv("REDIS_CLIENT_MAPPING") != "" {
+ num, err := strconv.Atoi(os.Getenv("REDIS_CLIENT_MAPPING"))
+ if err != nil {
+ log.Fatal("DB no of client map is badly formatted" + err.Error())
+ } else {
+ REDIS_CLIENT_MAPPING = num
+ }
+ }
+
+ if os.Getenv("REDIS_ROOM_MAPPING") != "" {
+ num, err := strconv.Atoi(os.Getenv("REDIS_ROOM_MAPPING"))
+ if err != nil {
+ log.Fatal("DB no of room map is badly formatted" + err.Error())
+ } else {
+ REDIS_ROOM_MAPPING = num
+ }
+ }
+
+ clientMappings := storage.InitialiseClientMappings(REDIS_URI, REDIS_CLIENT_MAPPING)
+ roomMappings := storage.InitialiseRoomMappings(REDIS_URI, REDIS_ROOM_MAPPING)
+
+
+ logger.Log.Info("Beginning consumption from message queue")
+ consumer.BeginConsuming(mq, logger, clientMappings, roomMappings)
+
+}
diff --git a/matching-service/mappings/client_mappings.go b/matching-service/mappings/client_mappings.go
new file mode 100644
index 0000000000..80b9eb5868
--- /dev/null
+++ b/matching-service/mappings/client_mappings.go
@@ -0,0 +1,98 @@
+//this file is deprecated
+package mappings
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "matching-service/models"
+)
+
+// TODO: consider using redis to store this information instead
+type Mappings struct {
+ Topics map[string][]string
+ Difficulty map[string]string
+}
+
+func CreateMappings() *Mappings {
+ return &Mappings{
+ Topics: make(map[string][]string),
+ Difficulty: make(map[string]string),
+ }
+}
+
+// TODO: implement logic to implement TTL for values
+// logic to find matching categories and generates a room id for the 2 users
+func (db *Mappings) HandleRequest(request models.IncomingRequests) (*models.Room, error) {
+ for user1, topics := range db.Topics {
+ if difficulty, exists := db.Difficulty[user1]; !exists {
+ return nil, fmt.Errorf("user %s only exists in topics store and not in difficulty store", user1)
+ } else if difficulty != request.Difficulty {
+ continue
+ }
+
+ overlapping := findOverLap(topics, request.TopicTags)
+
+ // user1 does not match with this user
+ if len(overlapping) == 0 {
+ continue
+ }
+
+ //match found, generate room Id and return the room
+ if roomId, err := generateRoomId(); err != nil {
+ return nil, err
+ } else {
+ //match found! delete user1 from store
+ delete(db.Topics, user1)
+ delete(db.Difficulty, user1)
+
+ return &models.Room{
+ RoomId: roomId,
+ User1: user1,
+ User2: request.UserId,
+ TopicTags: overlapping,
+ Difficulty: request.Difficulty,
+ }, nil
+ }
+ }
+
+ //no match found
+ //add user2 to the mappings
+ db.Topics[request.UserId] = request.TopicTags
+ db.Difficulty[request.UserId] = request.Difficulty
+
+ return nil, nil
+}
+
+func generateRoomId() (string, error) {
+ bytes := make([]byte, 16)
+
+ if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
+ return "", errors.New("Failed to generate random room Id" + err.Error())
+ }
+
+ return hex.EncodeToString(bytes), nil
+}
+
+func findOverLap(user1 []string, user2 []string) []string {
+
+ stringMap := make(map[string]bool)
+ var commonStrings []string
+
+ // Store each string from slice1 in the map
+ for _, topic := range user1 {
+ stringMap[topic] = true
+ }
+
+ // Iterate over slice2 and check for common strings
+ for _, topic := range user2 {
+ if stringMap[topic] {
+ commonStrings = append(commonStrings, topic)
+ delete(stringMap, topic) // Remove to avoid duplicates in result
+ }
+ }
+
+ return commonStrings
+}
diff --git a/matching-service/models/logger.go b/matching-service/models/logger.go
new file mode 100644
index 0000000000..be2a439d1d
--- /dev/null
+++ b/matching-service/models/logger.go
@@ -0,0 +1,10 @@
+package models
+
+import (
+
+ "github.com/sirupsen/logrus"
+)
+
+type Logger struct {
+ Log *logrus.Logger
+}
\ No newline at end of file
diff --git a/matching-service/models/matching_request.go b/matching-service/models/matching_request.go
new file mode 100644
index 0000000000..329021ee5d
--- /dev/null
+++ b/matching-service/models/matching_request.go
@@ -0,0 +1,14 @@
+package models
+
+type IncomingRequests struct {
+ MatchHash string `json:"matchHash"`
+ UserId string `json:"userId"`
+ TopicTags []string `json:"topicTags"`
+ Difficulty string `json:"difficulty"`
+ RequestTime string `json:"requestTime"`
+}
+
+type OutGoingRequests struct {
+ TopicTags []string `json:"topicTags"`
+ Difficulty string `json:"difficulty"`
+}
\ No newline at end of file
diff --git a/matching-service/models/message_queue.go b/matching-service/models/message_queue.go
new file mode 100644
index 0000000000..47cdc6ea65
--- /dev/null
+++ b/matching-service/models/message_queue.go
@@ -0,0 +1,11 @@
+package models
+
+import (
+ rabbit "github.com/streadway/amqp"
+)
+
+type MessageQueue struct {
+ Channel *rabbit.Channel
+ Queue rabbit.Queue
+}
+
diff --git a/matching-service/models/room.go b/matching-service/models/room.go
new file mode 100644
index 0000000000..d8c25f188d
--- /dev/null
+++ b/matching-service/models/room.go
@@ -0,0 +1,23 @@
+package models
+
+type Room struct {
+ // stores what key to ship the resulting blob to
+ MatchHash1 string `json:"matchHash1"`
+ MatchHash2 string `json:"matchHash2"`
+
+ // user information
+ RoomId string `json:"roomId"`
+ User1 string `json:"user1"`
+ User2 string `json:"user2"`
+ RequestTime string `json:"requestTime"` //takes user1's requestTime since this is older
+
+ //contains question Data
+ Title string `json:"title"`
+ TitleSlug string `json:"titleSlug"`
+ Difficulty string `json:"difficulty"`
+ TopicTags []string `json:"topicTags"`
+ Content string `json:"content"`
+ Schemas []string `json:"schemas"`
+ QuestionId int `json:"id"`
+
+}
diff --git a/matching-service/storage/client_mappings.go b/matching-service/storage/client_mappings.go
new file mode 100644
index 0000000000..5622044e23
--- /dev/null
+++ b/matching-service/storage/client_mappings.go
@@ -0,0 +1,175 @@
+package storage
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "io"
+ "matching-service/models"
+ "matching-service/transport"
+ "time"
+
+ redis "github.com/go-redis/redis/v8"
+)
+
+
+type ClientMappings struct {
+ Conn *redis.Client
+}
+
+func InitialiseClientMappings(addr string, db_num int) *ClientMappings {
+ conn := redis.NewClient(&redis.Options{
+ Addr:addr,
+ DB: db_num,
+ })
+
+ return &ClientMappings{
+ Conn: conn,
+ }
+
+}
+
+func (db *ClientMappings) HandleRequest(request models.IncomingRequests) (*models.Room, error){
+ ctx := context.Background()
+ user2, user2_difficulty, user2_topics := request.UserId, request.Difficulty, request.TopicTags
+ user2_requestTime, user2_matchHash := request.RequestTime, request.MatchHash
+
+ currMappings, err := db.Conn.Keys(ctx, "*").Result()
+
+ if err != nil {
+ return nil, err
+ }
+
+ for _, user1 := range currMappings {
+
+ if user1 == user2 {
+ //users cannot match with themselves
+ continue
+ }
+
+ result, err := db.Conn.HGetAll(ctx, user1).Result()
+
+ if err == redis.Nil {
+ continue //key expired
+ } else if err != nil {
+ return nil, err
+ }
+
+ var user1_topics []string
+ if err := json.Unmarshal([]byte(result["topicTags"]), &user1_topics); err != nil {
+ return nil, err
+ }
+
+ user1_difficulty := result["difficulty"]
+ user1_requestTime := result["requestTime"]
+
+ if user1_difficulty != user2_difficulty {
+ continue
+ }
+
+ overlappingTopics := findOverlap(user1_topics, user2_topics)
+
+ if len(overlappingTopics) == 0 {
+ continue
+ }
+
+ roomId, err := generateRoomId()
+
+ if err != nil {
+ return nil, err
+ }
+
+ user1_matchHash := result["matchHash"]
+
+ db.Conn.Del(ctx, user1)
+
+ room := models.Room{
+ MatchHash1: user1_matchHash,
+ MatchHash2: user2_matchHash,
+ RoomId: roomId,
+ User1: user1,
+ User2: user2,
+ RequestTime: user1_requestTime,
+ }
+
+ err = transport.FindSuitableQuestionId(overlappingTopics, user1_difficulty, &room)
+
+ if err != nil {
+ return nil, err
+ } else if room.QuestionId == 0 {
+ //no matching question
+ continue
+ }
+
+ return &room, nil
+
+ }
+
+ //no match found
+
+ user2_topics_json, err := json.Marshal(user2_topics)
+
+ if err != nil {
+ return nil, err
+ }
+
+ err = db.Conn.HSet(ctx, user2, map[string]interface{}{
+ "matchHash": user2_matchHash,
+ "topicTags": user2_topics_json,
+ "difficulty": user2_difficulty,
+ "requestTime": user2_requestTime,
+ }).Err()
+
+ if err != nil {
+ return nil, err
+ }
+
+ requestTime, err := time.Parse("2006-01-02 15-04-05", user2_requestTime)
+
+ if err != nil {
+ return nil, err
+ }
+
+ expiryTime := requestTime.Add(30 * time.Second).Add(-8 * time.Hour)
+ diff := int(time.Until(expiryTime).Seconds())
+ err = db.Conn.Expire(ctx, user2, time.Duration(diff) * time.Second).Err()
+
+ if err != nil {
+ return nil, err
+ }
+
+ return nil, nil
+}
+
+func findOverlap(user1 []string, user2 []string) []string {
+
+ stringMap := make(map[string]bool)
+ var commonStrings []string
+
+ // Store each string from slice1 in the map
+ for _, topic := range user1 {
+ stringMap[topic] = true
+ }
+
+ // Iterate over slice2 and check for common strings
+ for _, topic := range user2 {
+ if stringMap[topic] {
+ commonStrings = append(commonStrings, topic)
+ delete(stringMap, topic) // Remove to avoid duplicates in result
+ }
+ }
+
+ return commonStrings
+}
+
+func generateRoomId() (string, error) {
+ bytes := make([]byte, 16)
+
+ if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
+ return "", errors.New("Failed to generate random room Id" + err.Error())
+ }
+
+ return hex.EncodeToString(bytes), nil
+}
\ No newline at end of file
diff --git a/matching-service/storage/room_mappings.go b/matching-service/storage/room_mappings.go
new file mode 100644
index 0000000000..47826e2d15
--- /dev/null
+++ b/matching-service/storage/room_mappings.go
@@ -0,0 +1,102 @@
+package storage
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "matching-service/models"
+ "time"
+
+ redis "github.com/go-redis/redis/v8"
+)
+
+// same as client mappings, but separated for type safety
+type RoomMappings struct {
+ Conn *redis.Client
+}
+
+func InitialiseRoomMappings(addr string, db_num int) *RoomMappings {
+ conn := redis.NewClient(&redis.Options{
+ Addr: addr,
+ DB: db_num,
+ })
+
+ return &RoomMappings{
+ Conn: conn,
+ }
+}
+
+func (db *RoomMappings) SendToStorageBlob(room *models.Room) error {
+ ctx := context.Background()
+ topics_json, err := json.Marshal(room.TopicTags)
+
+ if err != nil {
+ return fmt.Errorf("error marshling topics: %s", err.Error())
+ }
+
+ schema_json, err := json.Marshal(room.Schemas)
+
+ if err != nil {
+ return fmt.Errorf("error marshling topics: %s", err.Error())
+ }
+
+ // this is where the value is being set
+ user1_info := map[string]interface{}{
+ "roomId": room.RoomId,
+ "thisUser": room.User1,
+ "otherUser": room.User2,
+ "requestTime": room.RequestTime,
+
+ "title": room.Title,
+ "titleSlug": room.TitleSlug,
+ "difficulty": room.Difficulty,
+ "topicTags": topics_json,
+ "content": room.Content,
+ "schemas": schema_json,
+ "id": room.QuestionId,
+ }
+
+ user2_info := map[string]interface{}{
+ "roomId": room.RoomId,
+ "thisUser": room.User2,
+ "otherUser": room.User1,
+ "requestTime": room.RequestTime,
+
+ "title": room.Title,
+ "titleSlug": room.TitleSlug,
+ "difficulty": room.Difficulty,
+ "topicTags": topics_json,
+ "content": room.Content,
+ "schemas": schema_json,
+ "id": room.QuestionId,
+ }
+
+ // TODO: Modify this - this is where the key-value is being set
+ if err1 := db.Conn.HSet(ctx, room.MatchHash1, user1_info).Err(); err1 != nil {
+ return fmt.Errorf("error setting user1's room to storage: %s", err1.Error())
+ }
+
+ if err2 := db.Conn.HSet(ctx, room.MatchHash2, user2_info).Err(); err2 != nil {
+ return fmt.Errorf("error setting user2's room to storage: %s", err2.Error())
+ }
+
+ requestTime, err := time.Parse("2006-01-02 15-04-05", room.RequestTime)
+
+ if err != nil {
+ return fmt.Errorf("error parsing the time: %s", err.Error())
+ }
+
+ expiryTime := requestTime.Add(30 * time.Second).Add(-8 * time.Hour)
+
+ diff := int(time.Until(expiryTime).Seconds())
+
+ if err1 := db.Conn.Expire(ctx, room.MatchHash1, time.Duration(diff)*time.Second).Err(); err1 != nil {
+ return fmt.Errorf("error setting expiry time on room data: %s", err1.Error())
+ }
+
+ if err2 := db.Conn.Expire(ctx, room.MatchHash2, time.Duration(diff)*time.Second).Err(); err2 != nil {
+ return fmt.Errorf("error setting expiry time on room data: %s", err2.Error())
+ }
+
+ return nil
+}
diff --git a/matching-service/transport/request_questions.go b/matching-service/transport/request_questions.go
new file mode 100644
index 0000000000..8237499968
--- /dev/null
+++ b/matching-service/transport/request_questions.go
@@ -0,0 +1,70 @@
+//handles the request to the question-service to find a suitable question for the 2 users to match on
+
+package transport
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "io"
+ "matching-service/models"
+ "net/http"
+)
+
+func FindSuitableQuestionId(topicTags []string, difficulty string, target *models.Room) (error) {
+ data := models.OutGoingRequests{
+ TopicTags: topicTags,
+ Difficulty: difficulty,
+ }
+
+ reqBody, err := json.Marshal(data)
+
+ if err != nil {
+ return fmt.Errorf("failed to convert outgoing req to JSON: %s", err.Error())
+ }
+
+ URI := os.Getenv("BACKEND_MATCH_URI")
+
+ if URI == "" {
+ URI = "http://localhost:9090/match"
+ }
+
+ req, err := http.NewRequest("POST", URI, bytes.NewBuffer(reqBody))
+
+ if err != nil {
+ return fmt.Errorf("failed to make request: %s", err.Error())
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ client := http.DefaultClient
+
+ resp, err := client.Do(req)
+
+ if err != nil {
+ return fmt.Errorf("error sending request: %s", err.Error())
+ } else if resp.StatusCode == http.StatusNotFound {
+ //no matching questions
+ return nil
+ } else if resp.StatusCode >= 300 {
+ return fmt.Errorf("question service encountered error when processing request")
+ }
+
+
+ body, err := io.ReadAll(resp.Body)
+
+ if err != nil {
+ return fmt.Errorf("error reading response body: %s", err.Error())
+ }
+
+ //unmarshal the data into the target room struct
+ err = json.Unmarshal(body, target)
+
+ if err != nil {
+ return fmt.Errorf("error unmarshalling JSON to question: %s", err.Error())
+ }
+
+
+ return nil
+}
\ No newline at end of file
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
new file mode 100644
index 0000000000..4bc7df8168
--- /dev/null
+++ b/nginx/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:alpine
+COPY nginx.conf /etc/nginx/internal.conf
+EXPOSE 70
+CMD ["nginx", "-c", "internal.conf", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/nginx/README.md b/nginx/README.md
new file mode 100644
index 0000000000..28f1e06e06
--- /dev/null
+++ b/nginx/README.md
@@ -0,0 +1,7 @@
+nginx is dockerised, just have to run `docker compose up --build` as usual.
+
+if edits are made to the local nginx.conf file, following command must be run to see changes reflected in docker:
+
+`docker exec cs3219-ay2425s1-project-g14-nginx-1 nginx -s reload`
+
+(or just exec `nginx -s reload` in the container directly)
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 0000000000..e4ee87b73f
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,124 @@
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include mime.types;
+
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+
+
+ upstream peerprep {
+ server peerprep:3000;
+ }
+
+ upstream user_service {
+ server user-service:3001;
+ }
+
+ upstream backend {
+ server backend:9090;
+ }
+
+ upstream matching_service_api {
+ server matching-service-api:9200;
+ }
+
+ upstream storage_blob_api {
+ server storage-blob-api:9300;
+ }
+
+ upstream collab {
+ server collab:4000;
+ }
+
+ upstream formatter {
+ server formatter:5000;
+ }
+
+ # upstream comms {
+ # server comms:4001;
+ # }
+
+ server {
+ listen 70;
+ location / {
+ proxy_pass http://peerprep/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+
+ location /users/ {
+ proxy_pass http://user_service/;
+
+ }
+
+ location /backend/ {
+ proxy_pass http://backend/;
+ }
+
+ location /matchmaking/ {
+ proxy_pass http://matching_service_api/;
+ }
+
+ location /blob/ {
+ proxy_pass http://storage_blob_api/;
+ }
+
+ location /collab/ {
+ proxy_pass http://collab/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 86400;
+ }
+
+ location /formatter/ {
+ proxy_pass http://formatter/;
+ }
+
+ # location /comms/ {
+ # proxy_pass http://comms/;
+ # proxy_set_header Host $host;
+ # proxy_set_header X-Real-IP $remote_addr;
+ # proxy_set_header X-Forwarded-Proto $scheme;
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # proxy_http_version 1.1;
+ # proxy_read_timeout 86400;
+ # }
+
+ # location /socket.io/ {
+ # proxy_pass http://comms/socket.io/;
+ # proxy_set_header Host $host;
+ # proxy_set_header X-Real-IP $remote_addr;
+ # proxy_set_header X-Forwarded-Proto $scheme;
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # proxy_http_version 1.1;
+ # proxy_set_header Upgrade $http_upgrade;
+ # proxy_set_header Connection "upgrade";
+ # proxy_read_timeout 86400;
+ # }
+ }
+}
\ No newline at end of file
diff --git a/peerprep/.dockerignore b/peerprep/.dockerignore
new file mode 100644
index 0000000000..a38a8f3a16
--- /dev/null
+++ b/peerprep/.dockerignore
@@ -0,0 +1,4 @@
+ /node_modules
+# /.next
+# .DS_Store
+# .env
\ No newline at end of file
diff --git a/peerprep/.env.example b/peerprep/.env.example
index 5325201b9a..d0878f2289 100644
--- a/peerprep/.env.example
+++ b/peerprep/.env.example
@@ -1,5 +1,8 @@
-NEXT_PUBLIC_QUESTION_SERVICE=
-NEXT_PUBLIC_USER_SERVICE=
-# note NGINX will currently point to itself.
-NEXT_PUBLIC_NGINX=
-DEV_ENV=
\ No newline at end of file
+#NEXT_PUBLIC_BASE_URL=http://host.docker.internal:80
+NEXT_PUBLIC_QUESTION_SERVICE=backend
+NEXT_PUBLIC_USER_SERVICE=users
+
+NEXT_PUBLIC_NGINX=http://host.docker.internal:80
+DEV_ENV=not
+NEXT_PUBLIC_MATCHING_SERVICE=matchmaking
+NEXT_PUBLIC_STORAGE_BLOB=blob
\ No newline at end of file
diff --git a/peerprep/.env.sample b/peerprep/.env.sample
new file mode 100644
index 0000000000..0db3be0468
--- /dev/null
+++ b/peerprep/.env.sample
@@ -0,0 +1,7 @@
+# THIS CANNOT BE RIGHT, TEMP FIX
+NEXT_PUBLIC_BASE_URL=http://localhost
+# location of question service
+NEXT_PUBLIC_QUESTION_SERVICE=api
+NEXT_PUBLIC_USER_SERVICE=users
+# dev flag, originally used when services were not up
+DEV_ENV=not
\ No newline at end of file
diff --git a/peerprep/.eslintrc.json b/peerprep/.eslintrc.json
index bffb357a71..c3bd7e6aef 100644
--- a/peerprep/.eslintrc.json
+++ b/peerprep/.eslintrc.json
@@ -1,3 +1,11 @@
{
- "extends": "next/core-web-vitals"
+ "extends": [
+ "next/core-web-vitals",
+ "next/typescript",
+ "prettier"
+ ],
+ "ignorePatterns": [
+ "**/*.css",
+ "**/*.scss"
+ ]
}
diff --git a/peerprep/.gitignore b/peerprep/.gitignore
index 56f49ba2be..b8e093b167 100644
--- a/peerprep/.gitignore
+++ b/peerprep/.gitignore
@@ -2,6 +2,9 @@
# environment file
.env
+.dockerignore
+
+.eslintcache
# dependencies
/node_modules
diff --git a/peerprep/.husky/.gitignore b/peerprep/.husky/.gitignore
new file mode 100644
index 0000000000..31354ec138
--- /dev/null
+++ b/peerprep/.husky/.gitignore
@@ -0,0 +1 @@
+_
diff --git a/peerprep/.husky/pre-commit b/peerprep/.husky/pre-commit
new file mode 100644
index 0000000000..b28c372e66
--- /dev/null
+++ b/peerprep/.husky/pre-commit
@@ -0,0 +1 @@
+npm run lint-staged
\ No newline at end of file
diff --git a/peerprep/.prettierignore b/peerprep/.prettierignore
new file mode 100644
index 0000000000..1b8ac8894b
--- /dev/null
+++ b/peerprep/.prettierignore
@@ -0,0 +1,3 @@
+# Ignore artifacts:
+build
+coverage
diff --git a/peerprep/.prettierrc b/peerprep/.prettierrc
new file mode 100644
index 0000000000..b4bfed3579
--- /dev/null
+++ b/peerprep/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/peerprep/Dockerfile b/peerprep/Dockerfile
index aa707df1a2..48dfc02924 100644
--- a/peerprep/Dockerfile
+++ b/peerprep/Dockerfile
@@ -4,7 +4,8 @@ WORKDIR /frontend
# TODO: don't include the .env file in the COPY
# TODO: multistage build
COPY package*.json ./
-RUN npm install
+RUN npm install --force
COPY . .
+RUN npm run build
EXPOSE 3000
-CMD ["npm", "run", "dev"]
\ No newline at end of file
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/peerprep/README.md b/peerprep/README.md
index f4da3c4c1c..5aec39a728 100644
--- a/peerprep/README.md
+++ b/peerprep/README.md
@@ -1,4 +1,13 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+Icon attribution:
+
+- Code icons created by Royyan Wijaya -
+ Flaticon
+- Code icons created by Freepik - Flaticon
+- Person icons created by spaceman.design -
+ Flaticon
+
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [
+`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
@@ -14,9 +23,10 @@ pnpm dev
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and
+load Inter, a custom Google Font.
## Learn More
@@ -25,10 +35,13 @@ To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions
+are welcome!
## Deploy on Vercel
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+The easiest way to deploy your Next.js app is to use
+the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
+from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/peerprep/api/gateway.ts b/peerprep/api/gateway.ts
index b80565f63b..1c80c44c32 100644
--- a/peerprep/api/gateway.ts
+++ b/peerprep/api/gateway.ts
@@ -1,13 +1,21 @@
import { cookies } from "next/headers";
-import { LoginResponse, Question, SigninResponse, StatusBody } from "./structs";
-import DOMPurify from "isomorphic-dompurify";
+import { LoginResponse, StatusBody, UserServiceResponse } from "./structs";
+import { CookieNames } from "@/app/actions/session";
export function generateAuthHeaders() {
return {
- Authorization: `Bearer ${cookies().get("session")}`,
+ Authorization: `Bearer ${cookies().get(CookieNames.SESSION.valueOf())?.value}`,
};
}
+export function getSessionToken() {
+ return cookies().get(CookieNames.SESSION.valueOf())?.value;
+}
+
+export function getUserData() {
+ return cookies().get(CookieNames.USER_DATA.valueOf())?.value;
+}
+
export function generateJSONHeaders() {
return {
...generateAuthHeaders(),
@@ -15,49 +23,20 @@ export function generateJSONHeaders() {
};
}
-export async function fetchQuestion(
- questionId: string,
-): Promise {
- try {
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/solve/${questionId}`,
- {
- method: "GET",
- headers: generateAuthHeaders(),
- },
- );
- if (!response.ok) {
- return {
- error: await response.text(),
- status: response.status,
- };
- }
-
- // NOTE: this may cause the following: "Can't resolve canvas"
- // https://github.com/kkomelin/isomorphic-dompurify/issues/54
- const question = (await response.json()) as Question;
- question.content = DOMPurify.sanitize(question.content);
- return question;
- } catch (err: any) {
- return { error: err.message, status: 400 };
- }
-}
+export const userServiceUrl = `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_USER_SERVICE}`;
export async function getSessionLogin(validatedFields: {
email: string;
password: string;
}): Promise {
try {
- const res = await fetch(
- `${process.env.NEXT_PUBLIC_USER_SERVICE}/auth/login`,
- {
- method: "POST",
- body: JSON.stringify(validatedFields),
- headers: {
- "Content-type": "application/json; charset=UTF-8",
- },
+ const res = await fetch(`${userServiceUrl}/auth/login`, {
+ method: "POST",
+ body: JSON.stringify(validatedFields),
+ headers: {
+ "Content-type": "application/json; charset=UTF-8",
},
- );
+ });
const json = await res.json();
if (!res.ok) {
@@ -75,10 +54,10 @@ export async function postSignupUser(validatedFields: {
username: string;
email: string;
password: string;
-}): Promise {
+}): Promise {
try {
console.log(JSON.stringify(validatedFields));
- const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/users`, {
+ const res = await fetch(`${userServiceUrl}/users`, {
method: "POST",
body: JSON.stringify(validatedFields),
headers: {
@@ -97,3 +76,20 @@ export async function postSignupUser(validatedFields: {
return { error: err.message, status: 400 };
}
}
+
+export async function verifyUser(): Promise {
+ try {
+ const res = await fetch(`${userServiceUrl}/auth/verify-token`, {
+ method: "GET",
+ headers: generateAuthHeaders(),
+ });
+ const json = (await res.json()) as UserServiceResponse;
+
+ if (!res.ok) {
+ return { error: json.message, status: res.status };
+ }
+ return json;
+ } catch (err: any) {
+ return { error: err.message, status: 400 };
+ }
+}
diff --git a/peerprep/api/structs.ts b/peerprep/api/structs.ts
index 059d187556..53e5ef4945 100644
--- a/peerprep/api/structs.ts
+++ b/peerprep/api/structs.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z, ZodType } from "zod";
export enum Difficulty {
All = "All",
@@ -15,6 +15,7 @@ export interface QuestionBody {
}
// TODO remove this (unused)
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface QuestionFullBody extends QuestionBody {}
export interface Question extends QuestionFullBody {
@@ -43,11 +44,34 @@ export interface LoginResponse {
data: UserDataAccessToken;
}
-export interface SigninResponse {
+export interface UserServiceResponse {
message: string;
data: UserData;
}
+export interface MatchRequest {
+ userId: string;
+ topicTags: string[];
+ difficulty: string;
+ requestTime: string;
+}
+
+export interface MatchReqInitRes {
+ match_code: string;
+}
+
+export interface MatchData {
+ roomId: string;
+ user1: string;
+ user2: string;
+ questionId: string;
+}
+
+export interface MatchResponse {
+ isMatchFound: boolean;
+ data: MatchData;
+}
+
// credit - taken from Next.JS Auth tutorial
export type FormState =
| {
@@ -82,14 +106,35 @@ export const LoginFormSchema = z.object({
password: z
.string()
.min(8, { message: "Be at least 8 characters long" })
- .regex(/[a-zA-Z]/, { message: "Contain at least one letter." })
- .regex(/[0-9]/, { message: "Contain at least one number." })
- .regex(/[^a-zA-Z0-9]/, {
- message: "Contain at least one special character.",
- })
+ // .regex(/[a-zA-Z]/, { message: "Contain at least one letter." })
+ // .regex(/[0-9]/, { message: "Contain at least one number." })
+ // .regex(/[^a-zA-Z0-9]/, {
+ // message: "Contain at least one special character.",
+ // })
.trim(),
});
+// TODO: remove `any`
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isError(obj: any | StatusBody): obj is StatusBody {
return (obj as StatusBody).status !== undefined;
}
+
+export type Language = "javascript" | "python" | "c_cpp";
+// maybe this shud be in structs
+export type FormatResponse = {
+ formatted_code: string;
+};
+
+export const QuestionSchema = z.object({
+ difficulty: z.nativeEnum(Difficulty),
+ title: z.string().min(2, {
+ message: "Please input a title of at least length 2.",
+ }),
+ content: z.string().min(2, {
+ message: "Please input content.",
+ }),
+ topicTags: z.array(z.string()).min(1, {
+ message: "Please input at least one topic tag. Press enter to add a tag.",
+ }),
+}) satisfies ZodType;
diff --git a/peerprep/app/actions/server_actions.ts b/peerprep/app/actions/server_actions.ts
index 074b3f7651..40893ab135 100644
--- a/peerprep/app/actions/server_actions.ts
+++ b/peerprep/app/actions/server_actions.ts
@@ -1,14 +1,17 @@
"use server";
-import { getSessionLogin, postSignupUser } from "@/api/gateway";
+import { getSessionLogin, postSignupUser, verifyUser } from "@/api/gateway";
// defines the server-sided login action.
import {
- SignupFormSchema,
- LoginFormSchema,
FormState,
isError,
+ LoginFormSchema,
+ SignupFormSchema,
+ UserData,
+ UserServiceResponse,
} from "@/api/structs";
import { createSession } from "@/app/actions/session";
import { redirect } from "next/navigation";
+import { cookies } from "next/headers"; // credit - taken from Next.JS Auth tutorial
// credit - taken from Next.JS Auth tutorial
export async function signup(state: FormState, formData: FormData) {
@@ -33,7 +36,13 @@ export async function signup(state: FormState, formData: FormData) {
redirect("/auth/login");
} else {
// TODO: handle failure codes: 400, 409, 500.
- console.log(`${json.status}: ${json.error}`);
+ console.log(`Error in signup: ${json.status}: ${json.error}`);
+ return {
+ errors: {
+ username: ["Username is already in use."],
+ email: ["Email is already in use."],
+ },
+ };
}
}
@@ -53,9 +62,42 @@ export async function login(state: FormState, formData: FormData) {
const json = await getSessionLogin(validatedFields.data);
if (!isError(json)) {
- await createSession(json.data.accessToken);
+ await createSession(json.data);
redirect("/questions");
} else {
- console.log(json.error);
+ if (json.status === 401) {
+ return {
+ errors: {
+ email: ["Invalid email or password."],
+ },
+ };
+ } else if (json.status === 500) {
+ console.log(
+ "Get session login error: " + json.error + " : " + json.status,
+ );
+
+ return {
+ errors: {
+ email: ["Please try again."],
+ },
+ };
+ }
+ }
+}
+
+export async function hydrateUid(): Promise {
+ if (!cookies().has("session")) {
+ // TODO: this should not be required because of middleware
+ console.log("No session found - triggering switch back to login page.");
+ // redirect("/auth/login");
+ }
+ const json = await verifyUser();
+ if (isError(json)) {
+ console.log("Failed to fetch user ID.");
+ console.log(`Error ${json.status}: ${json.error}`);
+ // redirect("/auth/logout");
}
+ // TODO: handle error handling
+ const response = json as UserServiceResponse;
+ return response.data;
}
diff --git a/peerprep/app/actions/session.ts b/peerprep/app/actions/session.ts
index c4d592552a..0b5f685fef 100644
--- a/peerprep/app/actions/session.ts
+++ b/peerprep/app/actions/session.ts
@@ -1,13 +1,48 @@
import "server-only";
import { cookies } from "next/headers";
+import { UserData, UserDataAccessToken } from "@/api/structs";
-export async function createSession(accessToken: string) {
+export enum CookieNames {
+ SESSION = "session",
+ USER_DATA = "userdata",
+}
+
+export async function createSession(userDataAccessToken: UserDataAccessToken) {
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
- cookies().set("session", accessToken, {
- httpOnly: true,
- secure: true,
- expires: expiresAt,
- sameSite: "lax",
- path: "/",
- });
+ try {
+ console.log("Setting cookie...");
+
+ cookies().set(
+ CookieNames.SESSION.valueOf(),
+ userDataAccessToken.accessToken,
+ {
+ httpOnly: true,
+ // TODO: set this to true
+ secure: false,
+ expires: expiresAt,
+ sameSite: "lax",
+ path: "/",
+ },
+ );
+
+ const userData: UserData = {
+ email: userDataAccessToken.email,
+ username: userDataAccessToken.username,
+ id: userDataAccessToken.id,
+ isAdmin: userDataAccessToken.isAdmin,
+ createdAt: userDataAccessToken.createdAt,
+ };
+
+ cookies().set(CookieNames.USER_DATA.valueOf(), JSON.stringify(userData), {
+ httpOnly: true,
+ secure: false,
+ expires: expiresAt,
+ sameSite: "lax",
+ path: "/",
+ });
+
+ console.log("Cookies set successfully.");
+ } catch (error) {
+ console.error("Error setting cookie:", error);
+ }
}
diff --git a/peerprep/app/api/internal/formatter/helper.ts b/peerprep/app/api/internal/formatter/helper.ts
new file mode 100644
index 0000000000..bd17bd76ce
--- /dev/null
+++ b/peerprep/app/api/internal/formatter/helper.ts
@@ -0,0 +1,28 @@
+import { FormatResponse, Language, StatusBody } from "@/api/structs";
+
+export async function callFormatter(
+ code: string,
+ language: Language,
+): Promise {
+ try {
+ const response = await fetch("/api/internal/formatter", {
+ method: "POST",
+ body: JSON.stringify({ code, language }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as StatusBody;
+ console.error("FormatterHelper: ", errorData);
+ throw new Error(`${errorData.error}`);
+ }
+
+ const formattedCode = (await response.json()) as FormatResponse;
+ console.log(formattedCode);
+ return formattedCode;
+ } catch (err: any) {
+ throw new Error(`${err.message}`);
+ }
+}
diff --git a/peerprep/app/api/internal/formatter/route.ts b/peerprep/app/api/internal/formatter/route.ts
new file mode 100644
index 0000000000..8747daccd9
--- /dev/null
+++ b/peerprep/app/api/internal/formatter/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest, NextResponse } from "next/server";
+import { FormatResponse, Language } from "@/api/structs";
+
+export async function POST(req: NextRequest) {
+ const { code, language }: { code: string; language: Language } =
+ await req.json();
+
+ let endpoint: string;
+
+ switch (language) {
+ case "javascript":
+ endpoint = "javascript";
+ break;
+ case "python":
+ endpoint = "python";
+ break;
+ case "c_cpp":
+ endpoint = "cpp";
+ break;
+ default:
+ return NextResponse.json(
+ { error: "Unsupported language type" },
+ { status: 400 },
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_FORMATTER}/format/${endpoint}`,
+ {
+ method: "POST",
+ body: JSON.stringify({ code }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ },
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ console.log("Formatter: ", errorData);
+ throw new Error(`Failed to format code: ${errorData.detail}`);
+ }
+
+ const formattedCode = (await response.json()) as FormatResponse;
+ return NextResponse.json(formattedCode);
+ } catch (error: any) {
+ return NextResponse.json({ error: `${error.message}` }, { status: 500 });
+ }
+}
diff --git a/peerprep/app/api/internal/matching/helper.ts b/peerprep/app/api/internal/matching/helper.ts
new file mode 100644
index 0000000000..d351cc1fc2
--- /dev/null
+++ b/peerprep/app/api/internal/matching/helper.ts
@@ -0,0 +1,54 @@
+import {
+ MatchData,
+ MatchRequest,
+ MatchResponse,
+ StatusBody,
+ MatchReqInitRes
+} from "@/api/structs";
+
+// helper to be called from client to check storage blob
+export async function checkMatchStatus(
+ matchHash: string
+): Promise {
+ console.debug("In matching helper, checking storage blob:", matchHash);
+ const res = await fetch(
+ `/api/internal/matching?matchHash=${matchHash}`,
+ {
+ method: "GET",
+ }
+ );
+ if (!res.ok) {
+ return {
+ error: await res.text(),
+ status: res.status,
+ };
+ }
+ const json = (await res.json()) as MatchData;
+ const isMatchFound = true; // TODO differntiate??
+
+ return {
+ isMatchFound,
+ data: json,
+ } as MatchResponse;
+}
+
+export async function findMatch(
+ matchRequest: MatchRequest
+): Promise {
+ console.debug(
+ "In matching helper, posting match request",
+ JSON.stringify(matchRequest)
+ );
+ const res = await fetch(`/api/internal/matching`, {
+ method: "POST",
+ body: JSON.stringify(matchRequest),
+ });
+ if (!res.ok) {
+ return {
+ error: await res.text(),
+ status: res.status,
+ } as StatusBody;
+ }
+ const json = await res.json();
+ return json as MatchReqInitRes;
+}
diff --git a/peerprep/app/api/internal/questions/route.ts b/peerprep/app/api/internal/matching/route.ts
similarity index 53%
rename from peerprep/app/api/internal/questions/route.ts
rename to peerprep/app/api/internal/matching/route.ts
index d8f85bd78a..d043241f5a 100644
--- a/peerprep/app/api/internal/questions/route.ts
+++ b/peerprep/app/api/internal/matching/route.ts
@@ -2,10 +2,17 @@ import { generateAuthHeaders, generateJSONHeaders } from "@/api/gateway";
import { QuestionFullBody } from "@/api/structs";
import { NextRequest, NextResponse } from "next/server";
-export async function GET() {
+// all get request interpreted as getting from storage blob
+export async function GET(request: NextRequest) {
+ const matchHash = request.nextUrl.searchParams.get("matchHash"); // Assuming you're passing the userId as a query parameter
+ console.log("in route,", matchHash);
+ if (!matchHash) {
+ return NextResponse.json({ error: "MatchHash is required" }, { status: 400 });
+ }
+
try {
const response = await fetch(
- `${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions`,
+ `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_STORAGE_BLOB}/request/${matchHash}`,
{
method: "GET",
headers: generateAuthHeaders(),
@@ -29,20 +36,20 @@ export async function GET() {
}
}
+// for matching stuff all post requests interpreted as posting matchmaking request
export async function POST(request: NextRequest) {
const body = await request.json();
try {
const response = await fetch(
- `${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions`,
+ `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_MATCHING_SERVICE}/request`,
{
method: "POST",
body: JSON.stringify(body),
- headers: generateJSONHeaders(),
}
);
if (response.ok) {
return NextResponse.json(
- { status: response.status },
+ { match_code: (await response.json()).match_code },
{ status: response.status }
);
}
@@ -60,38 +67,3 @@ export async function POST(request: NextRequest) {
);
}
}
-
-export async function DELETE(request: NextRequest) {
- const body = await request.json();
- if (body.qid === undefined) {
- return NextResponse.json(
- { error: "No ID specified.", status: 400 },
- { status: 400 }
- );
- }
- try {
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/delete/${body.qid}`,
- {
- method: "DELETE",
- headers: generateAuthHeaders(),
- }
- );
- if (response.ok) {
- // NextResponse doesn't support 204.
- return new Response(null, { status: response.status });
- }
- return NextResponse.json(
- {
- error: (await response.json())["Error deleting question: "],
- status: response.status,
- },
- { status: response.status }
- );
- } catch (err: any) {
- return NextResponse.json(
- { error: `Bad request: ${err.message}`, status: 400 },
- { status: 400 }
- );
- }
-}
diff --git a/peerprep/app/api/internal/questions/helper.ts b/peerprep/app/api/internal/questions/helper.ts
deleted file mode 100644
index f13855b2bc..0000000000
--- a/peerprep/app/api/internal/questions/helper.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { QuestionFullBody, StatusBody } from "@/api/structs";
-
-export async function deleteQuestion(id: number): Promise {
- const res = await fetch(
- `${process.env.NEXT_PUBLIC_NGINX}/api/internal/questions`,
- {
- method: "DELETE",
- body: JSON.stringify({ qid: id }),
- },
- );
- if (res.ok) {
- return { status: res.status };
- }
- const json = await res.json();
- return json as StatusBody;
-}
-
-export async function addQuestion(
- question: QuestionFullBody,
-): Promise {
- // TODO: this is not desired
- question.content = "" + question.content + "
";
- const res = await fetch(
- `${process.env.NEXT_PUBLIC_NGINX}/api/internal/questions`,
- {
- method: "POST",
- body: JSON.stringify(question),
- },
- );
- if (!res.ok) {
- return { status: res.status };
- }
- const json = await res.json();
- return json as StatusBody;
-}
diff --git a/peerprep/app/auth/login/page.tsx b/peerprep/app/auth/login/page.tsx
index 3ef3cd0bd0..887fd49c16 100644
--- a/peerprep/app/auth/login/page.tsx
+++ b/peerprep/app/auth/login/page.tsx
@@ -3,38 +3,58 @@ import React from "react";
import style from "@/style/form.module.css";
import { useFormState, useFormStatus } from "react-dom";
import FormTextInput from "@/components/shared/form/FormTextInput";
-import FormPasswordInput from "@/components/shared/form/FormPasswordInput";
import { login } from "@/app/actions/server_actions";
import Link from "next/link";
-type Props = {};
-
-function LoginPage({}: Props) {
+function LoginPage() {
const [state, action] = useFormState(login, undefined);
+ // we can actually use server actions to auth the user... maybe we can
+ // change our AddQn action too.
return (
- // we can actually use server actions to auth the user... maybe we can
- // change our AddQn action too.
);
}
@@ -43,7 +63,12 @@ function SubmitButton() {
const { pending } = useFormStatus();
return (
-
+
Login
);
diff --git a/peerprep/app/auth/register/page.tsx b/peerprep/app/auth/register/page.tsx
index b11131f8ab..b537226844 100644
--- a/peerprep/app/auth/register/page.tsx
+++ b/peerprep/app/auth/register/page.tsx
@@ -3,13 +3,10 @@ import React from "react";
import style from "@/style/form.module.css";
import { useFormState, useFormStatus } from "react-dom";
import FormTextInput from "@/components/shared/form/FormTextInput";
-import FormPasswordInput from "@/components/shared/form/FormPasswordInput";
import { signup } from "@/app/actions/server_actions";
import Link from "next/link";
-type Props = {};
-
-function RegisterPage({}: Props) {
+function RegisterPage() {
const [state, action] = useFormState(signup, undefined);
return (
// we can actually use server actions to auth the user... maybe we can
@@ -18,12 +15,25 @@ function RegisterPage({}: Props) {
Sign up for an account
- {state?.errors?.username && {state.errors.username}
}
+ {state?.errors?.username && (
+ {state.errors.username}
+ )}
- {state?.errors?.email && {state.errors.email}
}
-
+ {state?.errors?.email && (
+
+ {state.errors.email.map((item) => (
+
{item}
+ ))}
+
+ )}
+
{state?.errors?.password && (
-
+
@@ -45,7 +58,11 @@ function SubmitButton() {
const { pending } = useFormStatus();
return (
-
+
Sign up
);
diff --git a/peerprep/app/globals.css b/peerprep/app/globals.css
index e0288337df..35c504e5b1 100644
--- a/peerprep/app/globals.css
+++ b/peerprep/app/globals.css
@@ -7,6 +7,8 @@ body {
@apply overflow-hidden;
background-color: #121212;
height: 100%;
+ display: flex;
+ flex-direction: column;
}
body,
@@ -15,3 +17,68 @@ h1,
h2 {
@apply text-text-2;
}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/peerprep/app/icon.png b/peerprep/app/icon.png
new file mode 100644
index 0000000000..d5e519a196
Binary files /dev/null and b/peerprep/app/icon.png differ
diff --git a/peerprep/app/layout.tsx b/peerprep/app/layout.tsx
index 515f79b22b..2c30ad14fd 100644
--- a/peerprep/app/layout.tsx
+++ b/peerprep/app/layout.tsx
@@ -1,27 +1,25 @@
-import TitleBar from "@/components/shared/TitleBar";
-import "./globals.css";
import type { Metadata } from "next";
-import { Inter } from "next/font/google";
-import Container from "@/components/shared/Container";
-import styles from "@/style/layout.module.css";
-
-const inter = Inter({ subsets: ["latin"] });
+import "./globals.css";
+import Navbar from "@/components/navbar/Navbar";
+import ThemeProvider from "./theme-provider";
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "PeerPrep - One Stop Technical Interview Preparation",
+ description: "Your choice for Technical Interview Preparation",
};
export default function RootLayout({
children,
-}: {
+}: Readonly<{
children: React.ReactNode;
-}) {
+}>) {
return (
-
-
- {children}
+
+
+
+ {children}
+
);
diff --git a/peerprep/app/page.tsx b/peerprep/app/page.tsx
index d20fd8d3ce..ac4c592ed9 100644
--- a/peerprep/app/page.tsx
+++ b/peerprep/app/page.tsx
@@ -1,6 +1,57 @@
-import { redirect } from "next/navigation";
+import FadeUpAnimation from "@/components/animations/FadeUp";
+import Link from "next/link";
+import BoringSlideShow from "@/components/home/BoringSlideShow";
export default function Home() {
- redirect(`/questions`);
- // return Questions;
+ return (
+ <>
+
+
+ Welcome to PeerPrep!
+
+
+
+
+
+
+
+
Boring code platforms 😢
+
+
+
+
Tired of solving interview questions by yourself?
+
Code with a friend 👥 instead! 😊
+
PeerPrep is a platform for technical interview preparation
+
+
+ Features:
+
+
+ - Online coding
+ - A collaborative code editor
+ - Camera and audio support
+
+ - Syntax highlighting and formatting for three languages!
+
+
+
+
+
+
+
+
+ Try it now!
+
+
+
+ >
+ );
}
diff --git a/peerprep/app/questions/QuestionForm.tsx b/peerprep/app/questions/QuestionForm.tsx
new file mode 100644
index 0000000000..983451011e
--- /dev/null
+++ b/peerprep/app/questions/QuestionForm.tsx
@@ -0,0 +1,138 @@
+"use client";
+
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Difficulty, QuestionSchema } from "@/api/structs";
+import { Controller, UseFormReturn } from "react-hook-form";
+import { InputTags } from "@/components/ui/tags";
+import Tiptap from "@/components/modifyQuestion/Tiptap";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+
+type FormType = UseFormReturn<
+ {
+ difficulty: Difficulty;
+ title: string;
+ content: string;
+ topicTags: string[];
+ },
+ any,
+ undefined
+>;
+
+type QuestionFormProps = {
+ form: FormType;
+ onSubmit: (values: z.infer) => Promise;
+};
+
+const QuestionForm = ({ form, onSubmit }: QuestionFormProps) => {
+ return (
+
+
+ (
+
+ Title
+
+
+
+ Please input a title.
+
+
+ )}
+ />
+ (
+
+ Difficulty
+
+
+
+
+
+
+
+ Easy
+ Medium
+ Hard
+
+
+ Please select a difficulty.
+
+
+ )}
+ />
+ (
+
+ Topics
+
+ (
+
+ )}
+ />
+
+
+ Please input at least one topic tag.
+
+
+
+ )}
+ />
+ (
+
+ Content
+
+
+
+ Please input content.
+
+
+ )}
+ />
+
+ Submit
+
+
+
+ );
+};
+
+export default QuestionForm;
diff --git a/peerprep/app/questions/[question]/[roomID]/page.tsx b/peerprep/app/questions/[question]/[roomID]/page.tsx
new file mode 100644
index 0000000000..c6c2b3ca8c
--- /dev/null
+++ b/peerprep/app/questions/[question]/[roomID]/page.tsx
@@ -0,0 +1,47 @@
+import { getSessionToken, getUserData } from "@/api/gateway";
+import { isError, Question as QnType, StatusBody } from "@/api/structs";
+import styles from "@/style/question.module.css";
+import ErrorBlock from "@/components/shared/ErrorBlock";
+import React from "react";
+import QuestionBlock from "./question";
+import { fetchQuestion } from "@/app/questions/helper";
+
+type Props = {
+ searchParams: {
+ match?: string;
+ };
+ params: {
+ question: number;
+ roomID: string;
+ };
+};
+
+async function Question({ params, searchParams }: Props) {
+ const question = await fetchQuestion(params.question);
+ const authToken = getSessionToken();
+ const userData = getUserData();
+ let userId;
+ try {
+ userId = JSON.parse(userData as string)?.id;
+ } catch (err) {
+ console.log("Failed to parse userid");
+ }
+
+ return (
+
+ {isError(question) ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default Question;
diff --git a/peerprep/app/questions/[question]/[roomID]/question.tsx b/peerprep/app/questions/[question]/[roomID]/question.tsx
new file mode 100644
index 0000000000..f7c6638b6c
--- /dev/null
+++ b/peerprep/app/questions/[question]/[roomID]/question.tsx
@@ -0,0 +1,82 @@
+"use client";
+import React from "react";
+import { Difficulty, Question } from "@/api/structs";
+import Chip from "@/components/shared/Chip";
+import styles from "@/style/question.module.css";
+import CollabEditor from "@/components/questionpage/CollabEditor";
+import DOMPurify from "isomorphic-dompurify";
+
+interface Props {
+ question: Question;
+ roomID?: string;
+ authToken?: string;
+ userId?: string;
+ matchHash?: string;
+}
+
+interface DifficultyChipProps {
+ diff: Difficulty;
+}
+
+function DifficultyChip({ diff }: DifficultyChipProps) {
+ return diff === Difficulty.Easy ? (
+ Easy
+ ) : diff === Difficulty.Medium ? (
+ Med
+ ) : (
+ Hard
+ );
+}
+
+function QuestionBlock({
+ question,
+ roomID,
+ authToken,
+ userId,
+ matchHash,
+}: Props) {
+ return (
+ <>
+
+
+
+
+ Q{question.id}: {question.title}
+
+
+
+
+
+
Topics:
+ {question.topicTags.length == 0 ? (
+
No topics listed.
+ ) : (
+ question.topicTags.map((elem, idx) => (
+
+ {elem}
+
+ ))
+ )}
+
+ {
+
+ }
+
+
+
+
+ >
+ );
+}
+
+export default QuestionBlock;
diff --git a/peerprep/app/questions/[question]/page.tsx b/peerprep/app/questions/[question]/page.tsx
index 73c230106b..f5f075b882 100644
--- a/peerprep/app/questions/[question]/page.tsx
+++ b/peerprep/app/questions/[question]/page.tsx
@@ -1,13 +1,14 @@
-import { fetchQuestion } from "@/api/gateway";
-import { Question as QnType, StatusBody, isError } from "@/api/structs";
+import { isError, Question as QnType, StatusBody } from "@/api/structs";
import styles from "@/style/question.module.css";
import ErrorBlock from "@/components/shared/ErrorBlock";
import React from "react";
import QuestionBlock from "./question";
+import { fetchQuestion } from "@/app/questions/helper";
+
type Props = {
params: {
- question: string;
+ question: number;
};
};
diff --git a/peerprep/app/questions/[question]/question.tsx b/peerprep/app/questions/[question]/question.tsx
index 98cc27e2fd..ea0404a734 100644
--- a/peerprep/app/questions/[question]/question.tsx
+++ b/peerprep/app/questions/[question]/question.tsx
@@ -1,13 +1,13 @@
"use client";
+
import React from "react";
import { Difficulty, Question } from "@/api/structs";
import Chip from "@/components/shared/Chip";
-import PeerprepButton from "@/components/shared/PeerprepButton";
import styles from "@/style/question.module.css";
import { useRouter } from "next/navigation";
-import { deleteQuestion } from "@/app/api/internal/questions/helper";
+import { deleteQuestion } from "@/app/questions/helper";
import CollabEditor from "@/components/questionpage/CollabEditor";
-import DOMPurify from "dompurify";
+import DOMPurify from "isomorphic-dompurify";
interface Props {
question: Question;
@@ -45,6 +45,7 @@ function QuestionBlock({ question }: Props) {
}
console.log(`Successfully deleted the question.`);
router.push("/questions");
+ router.refresh();
} else {
console.log("Deletion cancelled.");
}
@@ -60,12 +61,6 @@ function QuestionBlock({ question }: Props) {
-
- Delete
-
Topics:
@@ -89,7 +84,7 @@ function QuestionBlock({ question }: Props) {
}
-
+
>
);
diff --git a/peerprep/app/questions/edit/[question]/EditQuestion.tsx b/peerprep/app/questions/edit/[question]/EditQuestion.tsx
new file mode 100644
index 0000000000..9e50c75dd8
--- /dev/null
+++ b/peerprep/app/questions/edit/[question]/EditQuestion.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Question, QuestionSchema } from "@/api/structs";
+import { editQuestion } from "@/app/questions/helper";
+import QuestionForm from "@/app/questions/QuestionForm";
+import { useRouter } from "next/navigation";
+
+const EditQuestion = ({ question }: { question: Question }) => {
+ const router = useRouter();
+
+ const form = useForm>({
+ resolver: zodResolver(QuestionSchema),
+ defaultValues: {
+ title: question.title,
+ difficulty: question.difficulty,
+ content: question.content,
+ topicTags: question.topicTags,
+ },
+ });
+
+ const onSubmit = async (values: z.infer) => {
+ console.log(values);
+ const qn: Question = {
+ id: question.id,
+ ...values,
+ };
+ const status = await editQuestion(qn);
+ console.log(status);
+ if (status.error) {
+ console.log("Failed to add question.");
+ console.log(`Code ${status.status}: ${status.error}`);
+ alert(`Failed to add question. Code ${status.status}: ${status.error}`);
+ return;
+ }
+ console.log(`Successfully modified the question.`);
+ router.push("/questions");
+ };
+
+ return (
+
+ );
+};
+
+export default EditQuestion;
diff --git a/peerprep/app/questions/edit/[question]/page.tsx b/peerprep/app/questions/edit/[question]/page.tsx
new file mode 100644
index 0000000000..9b13e5b8a4
--- /dev/null
+++ b/peerprep/app/questions/edit/[question]/page.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import EditQuestion from "@/app/questions/edit/[question]/EditQuestion";
+import { Question } from "@/api/structs";
+import { revalidatePath } from "next/cache";
+
+import { fetchQuestion } from "@/app/questions/helper";
+
+type Props = {
+ params: {
+ question: number;
+ };
+};
+
+const EditQuestionPage = async ({ params }: Props) => {
+ const question = (await fetchQuestion(params.question)) as Question;
+ console.log("Fetching question");
+ revalidatePath("/questions");
+ revalidatePath(`/questions/${params.question}`);
+ revalidatePath(`/questions/edit/${params.question}`);
+
+ return ;
+};
+
+export default EditQuestionPage;
diff --git a/peerprep/app/questions/helper.ts b/peerprep/app/questions/helper.ts
new file mode 100644
index 0000000000..a1987be235
--- /dev/null
+++ b/peerprep/app/questions/helper.ts
@@ -0,0 +1,117 @@
+"use server";
+
+import { isError, Question, QuestionFullBody, StatusBody } from "@/api/structs";
+import { revalidatePath } from "next/cache";
+import {
+ generateAuthHeaders,
+ generateJSONHeaders,
+ verifyUser,
+} from "@/api/gateway";
+import DOMPurify from "isomorphic-dompurify";
+
+const questionServiceUrl = `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_QUESTION_SERVICE}`;
+
+export async function deleteQuestion(id: number): Promise {
+ const verify = await verifyUser();
+ if (isError(verify) || verify?.data.isAdmin === false) {
+ return verify as StatusBody;
+ }
+
+ const res = await fetch(`${questionServiceUrl}/questions/delete/${id}`, {
+ method: "DELETE",
+ headers: generateAuthHeaders(),
+ });
+ if (res.ok) {
+ return { status: res.status };
+ }
+ revalidatePath("/questions");
+ const json = await res.json();
+ return json as StatusBody;
+}
+
+export async function fetchAllQuestions(): Promise {
+ console.log("Fetching all questions...");
+ const res = await fetch(`${questionServiceUrl}/questions`, {
+ method: "GET",
+ headers: generateAuthHeaders(),
+ cache: "no-store",
+ });
+ if (!res.ok) {
+ return { status: res.status };
+ }
+ const json = await res.json();
+ return json as Question[];
+}
+
+export async function editQuestion(question: Question): Promise {
+ const verify = await verifyUser();
+ if (isError(verify) || verify?.data.isAdmin === false) {
+ return verify as StatusBody;
+ }
+
+ console.log("editing question", question.id);
+ const res = await fetch(
+ `${questionServiceUrl}/questions/replace/${question.id}`,
+ {
+ method: "PUT",
+ body: JSON.stringify(question),
+ headers: generateJSONHeaders(),
+ },
+ );
+ if (!res.ok) {
+ return { status: res.status, error: await res.text() };
+ }
+ revalidatePath("/questions");
+ revalidatePath("/questions/edit/" + question.id);
+ const json = await res.json();
+ return json as StatusBody;
+}
+
+export async function addQuestion(
+ question: QuestionFullBody,
+): Promise {
+ const verify = await verifyUser();
+ if (isError(verify) || verify?.data.isAdmin === false) {
+ return verify as StatusBody;
+ }
+ console.log("Adding question", question.title);
+ const res = await fetch(`${questionServiceUrl}/questions`, {
+ method: "POST",
+ body: JSON.stringify(question),
+ headers: generateJSONHeaders(),
+ });
+ if (!res.ok) {
+ return { status: res.status, error: await res.text() };
+ }
+ revalidatePath("/questions");
+ const json = await res.json();
+ return json as StatusBody;
+}
+
+export async function fetchQuestion(
+ questionId: number,
+): Promise {
+ try {
+ const response = await fetch(
+ `${questionServiceUrl}/questions/solve/${questionId}`,
+ {
+ method: "GET",
+ headers: generateAuthHeaders(),
+ cache: "no-store",
+ },
+ );
+ if (!response.ok) {
+ return {
+ error: await response.text(),
+ status: response.status,
+ };
+ }
+
+ const question = (await response.json()) as Question;
+ question.content = DOMPurify.sanitize(question.content);
+ revalidatePath(`/questions/edit/${questionId}`);
+ return question;
+ } catch (err: any) {
+ return { error: err.message, status: 400 };
+ }
+}
\ No newline at end of file
diff --git a/peerprep/app/questions/loading.tsx b/peerprep/app/questions/loading.tsx
new file mode 100644
index 0000000000..c878ad5664
--- /dev/null
+++ b/peerprep/app/questions/loading.tsx
@@ -0,0 +1,10 @@
+const LoadingPage = () => {
+ return (
+
+
Loading...
+
Please wait...
+
+ );
+};
+
+export default LoadingPage;
diff --git a/peerprep/app/questions/new/ExampleQuestion.ts b/peerprep/app/questions/new/ExampleQuestion.ts
new file mode 100644
index 0000000000..b579493a72
--- /dev/null
+++ b/peerprep/app/questions/new/ExampleQuestion.ts
@@ -0,0 +1,61 @@
+export const exampleQuestion = `
+<!-- This is an example question, replace with your own. Remove this line! -->
+Given an integer array nums, return all the triplets
+\t[nums[i], nums[j], nums[k]] such that
+\ti != j,
+\ti != k, and
+\tj != k, and
+\tnums[i] + nums[j] + nums[k] == 0.
+
\n\n
+Notice that the solution set must not contain duplicate triplets.
\n\n
+
\n
+
+
+\tExample 1:
+
\n\n
+
+\n
+
+\tInput: nums = [-1,0,1,2,-1,-4]\n\n
+
+\tOutput: [[-1,-1,2],[-1,0,1]]\n
+\tExplanation: \nnums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.\nnums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.\nnums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.\nThe distinct triplets are [-1,0,1] and [-1,-1,2].\nNotice that the order of the output and the order of the triplets does not matter.\n
+\n\n
+
+
+
+\tExample 2:
+
\n\n
+
+\n
+\tInput: nums = [0,1,1]\n
+\tOutput: []\n
+\tExplanation: The only possible triplet does not sum up to 0.\n
+ \n\n
+
+
+\tExample 3:
+
\n\n
+
+\n
+\tInput: nums = [0,0,0]\n
+\tOutput: [[0,0,0]]\n
+\tExplanation: The only possible triplet sums up to 0.\n
+ \n\n
+
+
\n
+
+\tConstraints:
+
\n\n
+\n\t
+\t
+\t\t3 <= nums.length <= 3000
+\t \n\t
+\t
+\t\t-10
+\t\t\t5 <= nums[i] <= 10
+\t\t\t5
+\t\t
+\t \n
+ \n",
+`;
diff --git a/peerprep/app/questions/new/page.tsx b/peerprep/app/questions/new/page.tsx
index 054ad71508..b47fff5ffa 100644
--- a/peerprep/app/questions/new/page.tsx
+++ b/peerprep/app/questions/new/page.tsx
@@ -1,98 +1,34 @@
"use client";
-import { ChangeEvent, FormEvent, MouseEvent, useState } from "react";
-import { Difficulty, QuestionBody, QuestionFullBody } from "@/api/structs";
-import style from "@/style/form.module.css";
-import FormTextInput from "@/components/shared/form/FormTextInput";
-import RadioButtonGroup from "@/components/shared/form/RadioButtonGroup";
-import FormTextAreaInput from "@/components/shared/form/FormTextAreaInput";
-import { useRouter } from "next/navigation";
-import { addQuestion } from "@/app/api/internal/questions/helper";
-type Props = {};
+import { useForm } from "react-hook-form";
+import { z } from "zod";
-interface Mapping {
- key: string;
- value: string;
-}
+import { Difficulty, QuestionSchema } from "@/api/structs";
+import { useRouter } from "next/navigation";
+import { exampleQuestion } from "@/app/questions/new/ExampleQuestion";
+import { zodResolver } from "@hookform/resolvers/zod";
+import QuestionForm from "@/app/questions/QuestionForm";
+import { addQuestion } from "@/app/questions/helper";
-function NewQuestion({}: Props) {
+const NewQuestion = () => {
const router = useRouter();
- // Form Data is handled as a single submission
- const [formData, setFormData] = useState({
- title: "",
- difficulty: Difficulty.Easy,
- content: "",
- topicTags: [],
- });
- // Choice 1: Test cases handled separately to allow modification of multiple fields
- const [testCases, setTestCases] = useState([]);
- // TODO: Resolve this mess of hooks to combine the form data
- const [mapping, setMapping] = useState({
- key: "",
- value: "",
- });
- // Choice 2: Topics handled in a separate state, inject into formData on confirm
- const [topic, setTopic] = useState("");
- const [loading, setLoading] = useState(false);
-
- const handleTopicsInput = (e: ChangeEvent) =>
- setTopic(e.target.value);
- const handleTopicAdd = (e: MouseEvent) => {
- if (topic.length == 0) return;
- setFormData({
- ...formData,
- topicTags: [...formData.topicTags, topic],
- });
- setTopic("");
- };
- const handleTopicDel = (e: MouseEvent, idx: number) => {
- if (loading) return;
- const values = [...formData.topicTags];
- values.splice(idx, 1);
- setFormData({
- ...formData,
- topicTags: values,
- });
- };
- const handleFormTextInput = (
- e: ChangeEvent,
- ) =>
- setFormData({
- ...formData,
- [e.target.name]: e.target.value,
- });
-
- const handleMappingInput = (e: ChangeEvent) =>
- setMapping({
- ...mapping,
- [e.target.name]: e.target.value,
- });
-
- const handleMappingAdd = (e: MouseEvent) => {
- if (mapping.key.length == 0 || mapping.value.length == 0) return;
- setTestCases([...testCases, mapping]);
- setMapping({ key: "", value: "" });
- };
-
- const handleMappingDel = (e: MouseEvent, idx: number) => {
- if (loading) return;
- const values = [...testCases];
- values.splice(idx, 1);
- setTestCases(values);
- };
+ const form = useForm>({
+ resolver: zodResolver(QuestionSchema),
+ defaultValues: {
+ title: "",
+ difficulty: Difficulty.Easy,
+ content: exampleQuestion,
+ topicTags: [],
+ },
+ });
- const handleSubmission = async (e: FormEvent) => {
- e.preventDefault();
- setLoading(true);
- const question: QuestionFullBody = {
- ...formData,
- };
- const status = await addQuestion(question);
+ const onSubmit = async (values: z.infer) => {
+ console.log(values);
+ const status = await addQuestion(values);
if (status.error) {
console.log("Failed to add question.");
console.log(`Code ${status.status}: ${status.error}`);
- setLoading(false);
return;
}
console.log(`Successfully added the question.`);
@@ -100,110 +36,12 @@ function NewQuestion({}: Props) {
};
return (
-
-
- Create a new Question
-
-
-
-
-
-
-
- {formData.topicTags.length == 0 ? (
-
No Topics added.
- ) : (
- formData.topicTags.map((elem, idx) => (
-
handleTopicDel(e, idx)}
- >
- {elem}
-
- ))
- )}
-
-
- {testCases.length == 0 ? (
- No Test Cases added.
- ) : (
- testCases.map((elem, idx) => (
- handleMappingDel(e, idx)}
- >
- {elem.key}/{elem.value}
-
- ))
- )}
-
- Submit
-
-
+
);
-}
+};
export default NewQuestion;
diff --git a/peerprep/app/questions/page.tsx b/peerprep/app/questions/page.tsx
index 79f4b080b9..48c169e0b2 100644
--- a/peerprep/app/questions/page.tsx
+++ b/peerprep/app/questions/page.tsx
@@ -1,14 +1,42 @@
import React from "react";
import QuestionList from "@/components/questionpage/QuestionList";
import Matchmaking from "@/components/questionpage/Matchmaking";
+import { QuestionFilterProvider } from "@/contexts/QuestionFilterContext";
+import { hydrateUid } from "../actions/server_actions";
+import { isError, Question, StatusBody, UserData } from "@/api/structs";
+import { UserInfoProvider } from "@/contexts/UserInfoContext";
+import { fetchAllQuestions } from "@/app/questions/helper";
+import { redirect } from "next/navigation";
+
+async function QuestionsPage() {
+ const userData = (await hydrateUid()) as UserData;
+
+ if (!userData) {
+ redirect("/auth/login");
+ }
+
+ const questions: Question[] | StatusBody = await fetchAllQuestions();
+
+ if (isError(questions)) {
+ return (
+
+ );
+ }
-const QuestionsPage = () => {
return (
-
-
-
-
+
+
+
+
+
);
-};
+}
export default QuestionsPage;
diff --git a/peerprep/app/theme-provider.tsx b/peerprep/app/theme-provider.tsx
new file mode 100644
index 0000000000..ebc04774af
--- /dev/null
+++ b/peerprep/app/theme-provider.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import { createContext } from "react";
+
+export const ThemeContext = createContext({});
+
+export default function ThemeProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return
{children} ;
+}
diff --git a/peerprep/components.json b/peerprep/components.json
new file mode 100644
index 0000000000..bcec1f91f9
--- /dev/null
+++ b/peerprep/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
\ No newline at end of file
diff --git a/peerprep/components/animations/FadeUp.tsx b/peerprep/components/animations/FadeUp.tsx
new file mode 100644
index 0000000000..0c3af43e22
--- /dev/null
+++ b/peerprep/components/animations/FadeUp.tsx
@@ -0,0 +1,27 @@
+"use client";
+import { motion } from "framer-motion";
+import { ReactNode } from "react";
+
+// Code adapted from https://staticmania.com/blog/how-to-use-framer-motion-for-animations-in-next-js
+
+export const fadeUpVariant = {
+ initial: { opacity: 0, y: 10 },
+ animate: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.2,
+ },
+ },
+ exit: { opacity: 0 },
+};
+
+const FadeUpAnimation = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default FadeUpAnimation;
diff --git a/peerprep/components/home/BoringSlideShow.tsx b/peerprep/components/home/BoringSlideShow.tsx
new file mode 100644
index 0000000000..07a7cadcf8
--- /dev/null
+++ b/peerprep/components/home/BoringSlideShow.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import Carousel, { ImageProps } from "@/components/home/Carousel";
+
+const images: ImageProps[] = [
+ {
+ url: "/boring1.png",
+ alt: "L**tc*de",
+ },
+ {
+ url: "/boring2.png",
+ alt: "H*ckerr*nk",
+ },
+ {
+ url: "/boring3.png",
+ alt: "K*ttis",
+ },
+];
+
+const BoringSlideShow = () => {
+ return
;
+};
+
+export default BoringSlideShow;
diff --git a/peerprep/components/home/Carousel.tsx b/peerprep/components/home/Carousel.tsx
new file mode 100644
index 0000000000..59080e425c
--- /dev/null
+++ b/peerprep/components/home/Carousel.tsx
@@ -0,0 +1,96 @@
+import React, { useEffect, useRef, useState } from "react";
+import { AnimatePresence, motion, MotionConfig } from "framer-motion";
+import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+
+export type ImageProps = {
+ url: string;
+ alt: string;
+};
+
+const Carousel = ({ images }: { images: ImageProps[] }) => {
+ const [index, setIndex] = useState(0);
+ const intervalRef = useRef
(null);
+
+ const clearExistingInterval = () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ };
+
+ const startInterval = () => {
+ clearExistingInterval();
+ intervalRef.current = setInterval(() => {
+ setIndex((prevIndex) => (prevIndex + 1) % images.length);
+ }, 5000);
+ };
+
+ const prevSlide = () => {
+ setIndex((prevIndex) => (prevIndex - 1 + images.length) % images.length);
+ clearExistingInterval();
+ startInterval();
+ };
+
+ const nextSlide = () => {
+ setIndex((prevIndex) => (prevIndex + 1) % images.length);
+ clearExistingInterval();
+ startInterval();
+ };
+
+ useEffect(() => {
+ startInterval();
+
+ return () => clearExistingInterval();
+ }, [images.length]);
+
+ return (
+
+
+
+
+
+ {[...images, ...images].map((image, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Carousel;
\ No newline at end of file
diff --git a/peerprep/components/modifyQuestion/MenuBar.tsx b/peerprep/components/modifyQuestion/MenuBar.tsx
new file mode 100644
index 0000000000..1286ab4a9d
--- /dev/null
+++ b/peerprep/components/modifyQuestion/MenuBar.tsx
@@ -0,0 +1,167 @@
+import styles from "@/style/addquestion.module.css";
+
+import React from "react";
+import { Editor } from "@tiptap/core";
+import {
+ Bold,
+ Code,
+ Italic,
+ List,
+ ListOrdered,
+ Redo,
+ RemoveFormatting,
+ Strikethrough,
+ Underline,
+ Undo,
+} from "lucide-react";
+import Tooltip from "./Tooltip";
+import clsx from "clsx";
+
+type Props = {
+ editor: Editor | null;
+};
+
+export const MenuBar = ({ editor }: Props) => {
+ if (!editor) {
+ return null;
+ }
+
+ return (
+
+
+ editor.commands.unsetAllMarks()}
+ type={"button"}
+ className={styles.button}
+ >
+
+
+
+
+ editor.chain().focus().toggleBold().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("bold") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleItalic().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("italic") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleUnderline().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("underline") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleStrike().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("strike") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleCode().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("code") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleCodeBlock().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("codeBlock") && "is-active",
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleOrderedList().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("orderedList") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().toggleBulletList().run()}
+ type={"button"}
+ className={clsx(
+ styles.button,
+ editor.isActive("bulletList") && styles.isActive,
+ )}
+ >
+
+
+
+
+
+ editor.chain().focus().undo().run()}
+ type={"button"}
+ disabled={!editor.can().undo()}
+ className={styles.button}
+ >
+
+
+
+
+
+ editor.chain().focus().redo().run()}
+ type={"button"}
+ disabled={!editor.can().redo()}
+ className={styles.button}
+ >
+
+
+
+
+ );
+};
diff --git a/peerprep/components/modifyQuestion/Tiptap.tsx b/peerprep/components/modifyQuestion/Tiptap.tsx
new file mode 100644
index 0000000000..6cb6d7c5d7
--- /dev/null
+++ b/peerprep/components/modifyQuestion/Tiptap.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import "@/style/tiptap.css";
+import { EditorContent, useEditor } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import React from "react";
+import { Placeholder } from "@tiptap/extension-placeholder";
+import { Subscript } from "@tiptap/extension-subscript";
+import { Superscript } from "@tiptap/extension-superscript";
+import { Link } from "@tiptap/extension-link";
+import { Underline } from "@tiptap/extension-underline";
+import { MenuBar } from "@/components/modifyQuestion/MenuBar";
+
+type TipTapProps = {
+ defaultContent: string;
+ onChange: (richText: string) => void;
+};
+
+const Tiptap = ({ defaultContent, onChange }: TipTapProps) => {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Subscript,
+ Superscript,
+ Underline,
+ Link,
+ Placeholder.configure({
+ placeholder: "Add your question here",
+ }),
+ ],
+ content: defaultContent,
+ immediatelyRender: false,
+ onUpdate({ editor }) {
+ onChange(editor.getHTML());
+ },
+ });
+
+ return (
+
+ );
+};
+
+export default Tiptap;
diff --git a/peerprep/components/modifyQuestion/Tooltip.tsx b/peerprep/components/modifyQuestion/Tooltip.tsx
new file mode 100644
index 0000000000..0b5d9f3e80
--- /dev/null
+++ b/peerprep/components/modifyQuestion/Tooltip.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import styles from "@/style/addquestion.module.css";
+
+type TooltipProps = {
+ text: string;
+ children: React.ReactNode;
+};
+
+const Tooltip = ({ text, children }: TooltipProps) => {
+ return (
+
+ {children}
+ {text}
+
+ );
+};
+
+export default Tooltip;
diff --git a/peerprep/components/navbar/Navbar.tsx b/peerprep/components/navbar/Navbar.tsx
new file mode 100644
index 0000000000..f231185128
--- /dev/null
+++ b/peerprep/components/navbar/Navbar.tsx
@@ -0,0 +1,114 @@
+import Link from "next/link";
+import React from "react";
+import {
+ Disclosure,
+ DisclosureButton,
+ DisclosurePanel,
+} from "@headlessui/react";
+import { Menu, X } from "lucide-react";
+import Image from "next/image";
+import { ProfileDropdown } from "@/components/navbar/ProfileDropdown";
+
+// Navbar adapted from https://tailwindui.com/components/application-ui/navigation/navbars
+
+interface NavbarItemProps {
+ href: string;
+ name: string;
+}
+
+const navigation: NavbarItemProps[] = [
+ { href: "/", name: "Home" },
+ { href: "/questions", name: "Questions" },
+ // { href: "/auth/login", name: "Login" },
+];
+
+const MobileMenu = () => {
+ return (
+
+ {/* Mobile menu button*/}
+
+
+ Open main menu
+
+
+
+
+ );
+};
+
+const MobileDropdown = () => {
+ return (
+
+
+ {navigation.map((item) => (
+
+ {item.name}
+
+ ))}
+
+
+ );
+};
+
+const NavigationList = () => {
+ return (
+
+
+ {navigation.map((item) => (
+
+ {item.name}
+
+ ))}
+
+
+ );
+};
+
+const Navbar = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default Navbar;
diff --git a/peerprep/components/navbar/ProfileDropdown.tsx b/peerprep/components/navbar/ProfileDropdown.tsx
new file mode 100644
index 0000000000..b0b9693b99
--- /dev/null
+++ b/peerprep/components/navbar/ProfileDropdown.tsx
@@ -0,0 +1,70 @@
+import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
+import Image from "next/image";
+import React from "react";
+import Link from "next/link";
+import { UserData } from "@/api/structs";
+import { hydrateUid } from "@/app/actions/server_actions";
+
+export const ProfileDropdown = async () => {
+ let userData;
+ try {
+ userData = (await hydrateUid()) as UserData;
+ } catch {
+ userData = null;
+ console.error("Error hydrating user data.");
+ }
+
+ return (
+
+ {/* Profile dropdown */}
+
+
+
+
+ Open user menu
+
+
+
+
+ {userData ? (
+ <>
+
+
+ Hello, {userData.username}!
+
+
+
+
+ Sign out
+
+
+ >
+ ) : (
+ <>
+
+
+ Login
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/peerprep/components/questionpage/CollabEditor.tsx b/peerprep/components/questionpage/CollabEditor.tsx
index 9e27bcacd6..5ba6f4c13a 100644
--- a/peerprep/components/questionpage/CollabEditor.tsx
+++ b/peerprep/components/questionpage/CollabEditor.tsx
@@ -1,4 +1,5 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-python";
@@ -11,16 +12,16 @@ import "ace-builds/src-min-noconflict/ext-searchbox";
import "ace-builds/src-min-noconflict/ext-language_tools";
import PeerprepDropdown from "@/components/shared/PeerprepDropdown";
-import { Question } from "@/api/structs";
+import { FormatResponse, Language } from "@/api/structs";
+import PeerprepButton from "../shared/PeerprepButton";
+import CommsPanel from "./CommsPanel";
-const languages = [
- "javascript",
- "java",
- "python",
- "mysql",
- "golang",
- "typescript",
-];
+// import { diff_match_patch } from "diff-match-patch";
+import { callFormatter } from "@/app/api/internal/formatter/helper";
+import { Ace } from "ace-builds";
+
+const PING_INTERVAL_MILLISECONDS = 5000;
+const languages: Language[] = ["javascript", "python", "c_cpp"];
const themes = [
"monokai",
@@ -36,42 +37,273 @@ const themes = [
];
languages.forEach((lang) => {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require(`ace-builds/src-noconflict/mode-${lang}`);
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
require(`ace-builds/src-noconflict/snippets/${lang}`);
});
+// eslint-disable-next-line @typescript-eslint/no-require-imports
themes.forEach((theme) => require(`ace-builds/src-noconflict/theme-${theme}`));
interface Props {
- question: Question;
+ roomID?: string;
+ authToken?: string;
+ userId?: string | undefined;
+ matchHash?: string;
+}
+
+interface Message {
+ type: string;
+ roomId?: string;
+ data?: string;
+ userId?: string | undefined;
+ token?: string;
+ matchHash?: string;
+}
+
+enum MessageTypes {
+ AUTH = "auth",
+ AUTH_SUCCESS = "auth_success",
+ AUTH_FAIL = "auth_fail",
+ CLOSE_SESSION = "close_session",
+ CONTENT_CHANGE = "content_change",
+ PING = "ping",
}
-export default function CollabEditor({ question }: Props) {
+// const dmp = new diff_match_patch();
+// const questionSeed = "def foo():\n pass";
+
+export default function CollabEditor({
+ roomID,
+ authToken,
+ userId,
+ matchHash,
+}: Props) {
const [theme, setTheme] = useState("terminal");
const [fontSize, setFontSize] = useState(18);
- const [language, setLanguage] = useState("python");
+ const [language, setLanguage] = useState("python");
+ const [value, setValue] = useState("");
+ const [socket, setSocket] = useState(null);
+ const [connected, setConnected] = useState(false);
+ const [authenticated, setAuthenticated] = useState(false);
+ const [otherUserConnected, setOtherUserConnected] = useState(false);
+ const [lastPingReceived, setLastPingReceived] = useState(null);
+ const router = useRouter();
+
+ async function formatCode(value: string, language: Language) {
+ try {
+ const res = await callFormatter(value, language);
+ const formatResponse = res as FormatResponse;
+ const formatted_code = formatResponse.formatted_code;
+
+ setValue(formatted_code);
+ if (
+ socket &&
+ formatted_code !== value &&
+ socket?.readyState === WebSocket.OPEN
+ ) {
+ // const patches = generatePatch(value, formatted_code);
+ const msg: Message = {
+ type: MessageTypes.CONTENT_CHANGE.valueOf(),
+ data: formatted_code,
+ userId: userId,
+ };
+ socket.send(JSON.stringify(msg));
+ }
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ alert(e.message);
+ console.error(e.message);
+ } else {
+ console.error("An unknown error occurred");
+ }
+ }
+ }
const handleOnChange = (newValue: string) => {
- console.log("Content changed:", newValue);
+ // const patches = generatePatch(value, newValue);
+
+ setValue(newValue);
+ console.log("Content changed:", userId, newValue);
+
+ if (socket) {
+ const msg: Message = {
+ type: MessageTypes.CONTENT_CHANGE.valueOf(),
+ data: newValue,
+ userId: userId,
+ };
+ console.log("Sending message", msg);
+ socket.send(JSON.stringify(msg));
+ }
};
- const handleOnLoad = (editor: any) => {
+ const handleOnLoad = (editor: Ace.Editor) => {
editor.container.style.resize = "both";
};
- // TODO: to be taken from question props instead
- // const value = question[language] ?? "// Comment"
- const value = `def foo:
- pass`;
+ useEffect(() => {
+ if (!roomID) return;
+
+ console.log("Testing http");
+
+ const newSocket = new WebSocket(`/api/proxy?roomID=${roomID}`);
+
+ newSocket.onopen = () => {
+ console.log("WebSocket connection established");
+ setConnected(true);
+
+ const authMessage: Message = {
+ type: MessageTypes.AUTH.valueOf(),
+ token: authToken,
+ matchHash: matchHash, // omitted if undefined
+ };
+ newSocket.send(JSON.stringify(authMessage));
+ };
+
+ newSocket.onmessage = (event) => {
+ console.log("Event is", event);
+ // console.error(event.data);
+
+ const message: Message = JSON.parse(event.data);
+
+ const msgType = message.type as MessageTypes;
+
+ console.log("Received a message of type", msgType);
+ console.log("Received a message", message.type);
+
+ switch (msgType) {
+ case MessageTypes.AUTH:
+ // This should only be sent, never received by the client
+ throw new Error("Received unexpected auth message");
+ case MessageTypes.AUTH_SUCCESS:
+ setAuthenticated(true);
+ console.log("Auth success", message.data);
+ setValue(message.data as string);
+ break;
+ case MessageTypes.AUTH_FAIL:
+ window.alert("Authentication failed");
+ newSocket.close();
+ router.push("/questions");
+ break;
+ case MessageTypes.CLOSE_SESSION:
+ window.alert(
+ "Session has ended. If you leave the room now, this data will be lost.",
+ );
+ newSocket.close();
+ setAuthenticated(false);
+ setConnected(false);
+ break;
+ case MessageTypes.CONTENT_CHANGE:
+ if (message.userId !== userId) {
+ console.log(
+ "Received message from user: ",
+ message.userId,
+ "I am",
+ userId,
+ "We are the same: ",
+ message.userId === userId,
+ );
+ setValue(message.data as string);
+ }
+ break;
+ case MessageTypes.PING:
+ if (message.userId !== userId) {
+ console.log("other user connected!");
+ setOtherUserConnected(true);
+ setLastPingReceived(Date.now());
+ }
+ break;
+ default:
+ const exhaustiveCheck: never = msgType;
+ console.error("Unknown message type:", exhaustiveCheck);
+ console.log("Message data:", message);
+ }
+ };
+
+ newSocket.onerror = (event) => {
+ console.error("WebSocket error observed:", event);
+ console.error("WebSocket readyState:", newSocket.readyState);
+ console.error("WebSocket URL:", newSocket.url);
+ };
+
+ newSocket.onclose = () => {
+ console.log("WebSocket connection closed");
+ // TODO: should setConnected be false here?
+ // setConnected(false);
+ // router.push("/questions");
+ };
+
+ setSocket(newSocket);
+
+ return () => {
+ newSocket.close();
+ };
+ }, [authToken, matchHash, roomID, router, userId]);
+
+ // ping ws
+ const notifyRoomOfConnection = async () => {
+ // send message over ws
+ if (socket) {
+ console.log("PINGING WS FROM " + userId);
+ const msg: Message = {
+ type: MessageTypes.PING.valueOf(),
+ userId: userId,
+ };
+ socket.send(JSON.stringify(msg));
+ }
+ };
+
+ useEffect(() => {
+ if (!connected || !socket) return;
+
+ const interval = setInterval(
+ notifyRoomOfConnection,
+ PING_INTERVAL_MILLISECONDS,
+ );
+
+ const disconnectCheckInterval = setInterval(() => {
+ if (
+ lastPingReceived &&
+ Date.now() - lastPingReceived > 3 * PING_INTERVAL_MILLISECONDS
+ ) {
+ setOtherUserConnected(false);
+ clearInterval(disconnectCheckInterval);
+ }
+ }, PING_INTERVAL_MILLISECONDS);
+
+ return () => {
+ clearInterval(interval);
+ clearInterval(disconnectCheckInterval);
+ };
+ }, [notifyRoomOfConnection, connected, socket]);
+
+ const handleCloseConnection = () => {
+ const confirmClose = confirm(
+ "Are you sure you are finished? This will close the room for all users.",
+ );
+
+ if (confirmClose && socket) {
+ console.log("Sent!");
+ const msg: Message = {
+ type: MessageTypes.CLOSE_SESSION.valueOf(),
+ userId: userId,
+ };
+ socket.send(JSON.stringify(msg));
+ }
+ };
return (
<>
-
+ {authenticated && (
+
+ )}
+
-
Font Size
+
Font Size
setFontSize(Number(e.target.value))}
/>
@@ -83,21 +315,59 @@ export default function CollabEditor({ question }: Props) {
onChange={(e) => setTheme(e.target.value)}
options={themes}
className={
- "border border-gray-600 bg-gray-800 text-white p-2 rounded"
+ "rounded border border-gray-600 bg-gray-800 p-2 text-white"
}
/>
setLanguage(e.target.value)}
+ onChange={(e) => setLanguage(e.target.value as Language)}
options={languages}
className={
- "border border-gray-600 bg-gray-800 text-white p-2 rounded"
+ "rounded border border-gray-600 bg-gray-800 p-2 text-white"
}
/>
-
+
formatCode(value, language)}>
+ Format code
+
+
+ {roomID &&
+ (connected ? (
+
+ ) : (
+
+
+ Disconnected. Check logs.
+
+
+ ))}
+
+ {roomID &&
+ (connected ? (
+
+
+
+ {otherUserConnected
+ ? "Other user connected"
+ : "Other user disconnected"}
+
+
+ ) : (
+
Disconnected. Check logs.
+ ))}
();
+ const [callStarts, setCallStarts] = useState(false);
+
+ const myVideo = useRef(null);
+ const userVideo = useRef(null);
+ const connectionRef = useRef();
+
+ useEffect(() => {
+ socket.removeAllListeners();
+ socket.open();
+ return () => {
+ console.log("socket cleanup called");
+ if (socket) {
+ console.log("destroying socket");
+ socket.close();
+ }
+ if (connectionRef.current) {
+ connectionRef.current.destroy();
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ // capture the stream within the cleanup function itself.
+ let videoElement: MediaStream | undefined;
+
+ navigator.mediaDevices
+ .getUserMedia({ video: true, audio: true })
+ .then((newStream) => {
+ console.log("new stream's status is " + newStream.active);
+ newStream.getTracks().forEach((track: MediaStreamTrack) => {
+ console.log(
+ "media track status (ready/enabled): " +
+ track.readyState +
+ "/" +
+ track.enabled,
+ );
+ });
+ if (myVideo.current) {
+ console.log("can set myVideo.current");
+ myVideo.current.srcObject = newStream;
+ }
+ setStream(newStream);
+ videoElement = newStream;
+ })
+ .catch((err) => console.log("failed to get stream", err));
+
+ return () => {
+ console.log("cleaning up media");
+ if (videoElement) {
+ console.log("destroying stream");
+ videoElement.getTracks().forEach((track) => track.stop());
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!roomId || !stream || !socket.connected) {
+ console.log("stream status: " + stream);
+ console.log("connection status: " + socket.connected);
+ return;
+ }
+ console.log("in hook");
+
+ // clear all listeners if we are reinitializing this.
+ socket.removeAllListeners();
+ console.log("removed all listeners");
+
+ // when we receive the first peer connection, we immediately send out
+ // a peer connection request.
+ attachSocketInitiator(stream, roomId, userVideo, setCallStarts, connectionRef);
+
+ // as the receiver, I will propagate my data outwards now.
+ attachSocketReceiver(stream, roomId, userVideo, setCallStarts, connectionRef);
+
+ socket.on("endCall", () => {
+ // immediately destroy the socket listeners
+ destroyCallListeners(roomId);
+ if (userVideo.current) {
+ (userVideo.current.srcObject as MediaStream)
+ .getTracks()
+ .forEach((tracks: MediaStreamTrack) => {
+ tracks.stop();
+ });
+ userVideo.current.srcObject = null;
+ setCallStarts(false);
+ }
+ if (connectionRef.current && !connectionRef.current.destroyed) {
+ connectionRef.current.destroy();
+ }
+ // reattach the sockets
+ attachSocketInitiator(stream, roomId, userVideo, setCallStarts, connectionRef);
+ attachSocketReceiver(stream, roomId, userVideo, setCallStarts, connectionRef);
+ // rejoin the room
+ socket.emit("joinRoom", {
+ target: roomId,
+ });
+ });
+
+ socket.emit("joinRoom", {
+ target: roomId,
+ });
+ console.log("applied all hooks");
+ }, [stream, socket.connected]);
+
+ return (
+
+
+
+
+
+ {!callStarts &&
+
+ No signal from other user.
+ (Don't worry - the call doesn't start until we get a signal from both users!)
+
}
+
+
+
+ );
+}
+
+function destroyCallListeners(roomId: string) {
+ socket.removeAllListeners("startCall");
+ socket.removeAllListeners("peerConnected");
+ socket.removeAllListeners("handshakeCall");
+}
+
+function attachSocketReceiver(
+ stream: MediaStream,
+ roomId: string,
+ userVideo: React.RefObject,
+ setCallStarts: React.Dispatch>,
+ connectionRef: React.MutableRefObject,
+) {
+ socket.on("startCall", (data) => {
+ console.log("received start call signal");
+ const peerReceive = new Peer({
+ initiator: false,
+ trickle: false,
+ stream: stream,
+ });
+
+ peerReceive.on("signal", (data) => {
+ console.log("sending handshake");
+ socket.emit("handshakeCall", { signal: data, target: roomId });
+ });
+
+ peerReceive.on("stream", (stream) => {
+ console.log("setting stream of first user");
+ if (userVideo.current) {
+ console.log("user video exists");
+ userVideo.current.srcObject = stream;
+ setCallStarts(true);
+ }
+ });
+
+ connectionRef.current = peerReceive;
+ console.log("signalling receiver");
+ peerReceive.signal(data.signal);
+ });
+}
+
+function attachSocketInitiator(
+ stream: MediaStream,
+ roomId: string,
+ userVideo: React.RefObject,
+ setCallStarts: React.Dispatch>,
+ connectionRef: React.MutableRefObject,
+) {
+ socket.on("peerConnected", () => {
+ console.log("peer connected, starting call");
+ const peerInit = new Peer({
+ initiator: true,
+ trickle: false,
+ stream: stream,
+ });
+
+ peerInit.on("signal", (data) => {
+ console.log("signal to start call received");
+ socket.emit("startCall", { signalData: data, target: roomId });
+ });
+
+ peerInit.on("stream", (stream) => {
+ if (userVideo.current) {
+ console.log("setting stream for handshake");
+ userVideo.current.srcObject = stream;
+ setCallStarts(true);
+ }
+ });
+
+ connectionRef.current = peerInit;
+
+ socket.on("handshakeCall", (data) => {
+ console.log("received handshake");
+ peerInit.signal(data.signal);
+ });
+ });
+}
+
+export default CommsPanel;
diff --git a/peerprep/components/questionpage/Matchmaking.tsx b/peerprep/components/questionpage/Matchmaking.tsx
index 2de1aa5944..c168763088 100644
--- a/peerprep/components/questionpage/Matchmaking.tsx
+++ b/peerprep/components/questionpage/Matchmaking.tsx
@@ -1,15 +1,190 @@
"use client";
-import React from "react";
+import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import PeerprepButton from "../shared/PeerprepButton";
+import { useQuestionFilter } from "@/contexts/QuestionFilterContext";
+import { useUserInfo } from "@/contexts/UserInfoContext";
+import {
+ Difficulty,
+ isError,
+ MatchRequest,
+ MatchResponse,
+} from "@/api/structs";
+import {
+ checkMatchStatus,
+ findMatch,
+} from "@/app/api/internal/matching/helper";
+import ResettingStopwatch from "../shared/ResettingStopwatch";
+import PeerprepDropdown from "../shared/PeerprepDropdown";
+
+const QUERY_INTERVAL_MILLISECONDS = 5000;
+const TIMEOUT_MILLISECONDS = 30000;
+
+const getMatchRequestTime = (): string => {
+ const now = new Date();
+ const options: Intl.DateTimeFormatOptions = {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ };
+
+ const formattedDate = now.toLocaleString("en-CA", options); // gives YYYY-MM-DD, HH:mm:ss
+ const finalDate = formattedDate.replace(",", "").replace(/:/g, "-");
+
+ return finalDate;
+};
+
+const usePeriodicCallback = (
+ callback: () => void,
+ intervalTime: number,
+ isActive: boolean,
+) => {
+ useEffect(() => {
+ if (!isActive) return;
+
+ const interval = setInterval(callback, intervalTime);
+
+ return () => clearInterval(interval);
+ }, [callback, intervalTime, isActive]);
+};
const Matchmaking = () => {
const router = useRouter();
+ const [isMatching, setIsMatching] = useState(false);
+ const [matchHash, setMatchHash] = useState("");
+ const { difficulties, topicList } = useQuestionFilter();
+ const [difficultyFilter, setDifficultyFilter] = useState(
+ Difficulty.Easy,
+ );
+ const [topicFilter, setTopicFilter] = useState(topicList);
+ const userData = useUserInfo();
+ const userid = userData.id;
+ const timeout = useRef();
+
+ useEffect(() => {
+ setTopicFilter(topicList);
+ }, [topicList]);
+
+ const stopTimer = () => {
+ // if user manually stopped it clear timeout
+ if (timeout.current) {
+ console.debug("Match request timeout stopped");
+ clearTimeout(timeout.current);
+ }
+ };
+
+ const getMatchMakingRequest = (): MatchRequest => {
+ const matchRequest: MatchRequest = {
+ userId: userid,
+ difficulty: difficultyFilter,
+ topicTags: topicFilter,
+ requestTime: getMatchRequestTime(),
+ };
+
+ return matchRequest;
+ };
+
+ // TODO: Canceling the match should propagate a cancellation signal
+ // Currently, we can actually match yourself rn due to this change
+ // This indicates to me that 1 users can match on a cancellation
+ const handleMatch = async () => {
+ if (!isMatching) {
+ setIsMatching(true);
+
+ // start 30s timeout
+ timeout.current = setTimeout(() => {
+ setMatchHash("");
+ setIsMatching(false);
+ console.log("Match request timed out after 30s");
+ alert("Failed to find match. Request timed out.");
+ }, TIMEOUT_MILLISECONDS);
+
+ // assemble the match request
+ const matchRequest = getMatchMakingRequest();
+ console.log("Match attempted");
+ console.debug(matchRequest);
+
+ // send match request
+ const status = await findMatch(matchRequest);
+ if (isError(status)) {
+ stopTimer();
+ console.log("Failed to find match. Cancel matching.");
+ setMatchHash("");
+ setIsMatching(false);
+ return;
+ }
+ setMatchHash(status.match_code);
+ console.log(`Started finding match.`);
+ } else {
+ setMatchHash("");
+ stopTimer();
+ setIsMatching(false);
+ console.log("User stopped matching");
+ }
+ };
+
+ const queryResource = async () => {
+ const res = await checkMatchStatus(matchHash);
+ if (isError(res)) {
+ // for now 404 means no match found so dont stop matching on error, let request timeout
+ return;
+ }
+ stopTimer();
+ setIsMatching(false);
+ // TODO: iron out what is in a match response and sync up with collab service rooms
+ const matchRes: MatchResponse = res as MatchResponse;
+ console.log("Match found!");
+ router.push(
+ `/questions/${matchRes.data.questionId}/${matchRes.data.roomId}?match=${matchHash}`,
+ );
+ };
+
+ usePeriodicCallback(queryResource, QUERY_INTERVAL_MILLISECONDS, isMatching);
+
return (
-
+ // TODO: move this to some admin panel or something
+
router.push(`questions/new`)}>
Add Question
+
+
+ {isMatching
+ ? "Cancel Match"
+ : matchHash === ""
+ ? "Find Match"
+ : "Redirecting..."}
+
+ {!isMatching && (
+
setDifficultyFilter(e.target.value)}
+ // truthfully we don't need this difficulties list, but we are temporarily including it
+ options={difficulties}
+ />
+ )}
+ {!isMatching && (
+
+ setTopicFilter(
+ e.target.value === "all" ? topicList : [e.target.value],
+ )
+ }
+ options={topicList}
+ />
+ )}
+ {isMatching && }
+
);
};
diff --git a/peerprep/components/questionpage/QuestionCard.tsx b/peerprep/components/questionpage/QuestionCard.tsx
index b878b76487..79c4e4a8a8 100644
--- a/peerprep/components/questionpage/QuestionCard.tsx
+++ b/peerprep/components/questionpage/QuestionCard.tsx
@@ -4,8 +4,9 @@ import { Difficulty, Question } from "@/api/structs";
import PeerprepButton from "../shared/PeerprepButton";
import { useRouter } from "next/navigation";
import styles from "@/style/questionCard.module.css";
-import { deleteQuestion } from "@/app/api/internal/questions/helper";
-import DOMPurify from "dompurify";
+import { deleteQuestion } from "@/app/questions/helper";
+import DOMPurify from "isomorphic-dompurify";
+import { useUserInfo } from "@/contexts/UserInfoContext";
type QuestionCardProps = {
question: Question;
@@ -13,6 +14,10 @@ type QuestionCardProps = {
const QuestionCard: React.FC
= ({ question }) => {
const router = useRouter();
+ // Note that this is purely UI, there are additional checks in the API call
+ const userData = useUserInfo();
+ const isAdmin = userData.isAdmin;
+
const handleDelete = async () => {
if (
confirm(
@@ -20,6 +25,7 @@ const QuestionCard: React.FC = ({ question }) => {
)
) {
const status = await deleteQuestion(question.id);
+ router.refresh();
if (status.error) {
console.log("Failed to delete question.");
console.log(`Code ${status.status}: ${status.error}`);
@@ -30,6 +36,9 @@ const QuestionCard: React.FC = ({ question }) => {
console.log("Deletion cancelled.");
}
};
+ const handleEdit = () => {
+ router.push(`questions/edit/${question.id}`);
+ };
const getDifficultyColor = (difficulty: Difficulty) => {
switch (difficulty) {
@@ -52,12 +61,12 @@ const QuestionCard: React.FC = ({ question }) => {
return (
-
+
{question.title}
Difficulty:{" "}
@@ -72,7 +81,7 @@ const QuestionCard: React.FC = ({ question }) => {
-
+
{
= ({ question }) => {
-
router.push(`questions/${question.id}`)}>
+ router.push(`/questions/${question.id}`)}
+ >
View
- Delete
+ {isAdmin && Edit }
+ {isAdmin && (
+ Delete
+ )}
);
diff --git a/peerprep/components/questionpage/QuestionList.tsx b/peerprep/components/questionpage/QuestionList.tsx
index 62a0396706..5901e00da6 100644
--- a/peerprep/components/questionpage/QuestionList.tsx
+++ b/peerprep/components/questionpage/QuestionList.tsx
@@ -1,43 +1,33 @@
"use client";
import React, { useEffect, useState } from "react";
import QuestionCard from "./QuestionCard";
-import { Question, StatusBody, Difficulty, isError } from "@/api/structs";
+import { Difficulty, Question } from "@/api/structs";
import PeerprepDropdown from "../shared/PeerprepDropdown";
import PeerprepSearchBar from "../shared/PeerprepSearchBar";
+import { useQuestionFilter } from "@/contexts/QuestionFilterContext";
-const QuestionList: React.FC = () => {
- const [questions, setQuestions] = useState
([]);
- const [loading, setLoading] = useState(true);
- const [difficultyFilter, setDifficultyFilter] = useState(
- Difficulty.All
- );
- const [topicFilter, setTopicFilter] = useState("all");
- const [searchFilter, setSearchFilter] = useState("");
- const [topics, setTopics] = useState(["all"]);
-
- useEffect(() => {
- const fetchQuestions = async () => {
- const payload = await fetch(
- `${process.env.NEXT_PUBLIC_NGINX}/api/internal/questions`
- ).then((res) => res.json());
- // uh
- if (isError(payload)) {
- // should also reflect the error
- return;
- }
- const data: Question[] = payload;
+type Props = {
+ questions: Question[];
+};
- setLoading(false);
- setQuestions(data);
+// TODO make multiple select for topics at least
+const QuestionList = ({ questions }: Props) => {
+ const [searchFilter, setSearchFilter] = useState("");
- // get all present topics in all qns
- const uniqueTopics = Array.from(
- new Set(data.flatMap((question) => question.topicTags))
- );
- setTopics(["all", ...uniqueTopics]);
- };
+ const {
+ topicList,
+ setTopicList,
+ difficultyFilter,
+ setDifficultyFilter,
+ topicFilter,
+ setTopicFilter,
+ } = useQuestionFilter();
- fetchQuestions();
+ useEffect(() => {
+ const uniqueTopics = Array.from(
+ new Set(questions.flatMap((question) => question.topicTags)),
+ );
+ setTopicList(["all", ...uniqueTopics]);
}, []);
const filteredQuestions = questions.filter((question) => {
@@ -45,8 +35,7 @@ const QuestionList: React.FC = () => {
difficultyFilter === Difficulty.All ||
Difficulty[question.difficulty] === difficultyFilter;
const matchesTopic =
- topicFilter === topics[0] ||
- (question.topicTags ?? []).includes(topicFilter);
+ topicFilter === "all" || (question.topicTags ?? []).includes(topicFilter);
const matchesSearch =
searchFilter === "" ||
(question.title ?? "").toLowerCase().includes(searchFilter.toLowerCase());
@@ -56,9 +45,19 @@ const QuestionList: React.FC = () => {
const sortedQuestions = filteredQuestions.sort((a, b) => a.id - b.id);
+ const handleSetDifficulty = (e: React.ChangeEvent) => {
+ const diff = e.target.value;
+ setDifficultyFilter(diff);
+ };
+
+ const handleSetTopics = (e: React.ChangeEvent) => {
+ const topic = e.target.value;
+ setTopicFilter(topic);
+ };
+
return (
-
-
+
+
{
setDifficultyFilter(e.target.value)}
+ onChange={handleSetDifficulty}
options={Object.keys(Difficulty).filter((key) => isNaN(Number(key)))}
/>
setTopicFilter(e.target.value)}
- options={topics}
+ onChange={handleSetTopics}
+ options={topicList}
/>
- {loading ? (
-
Loading questions...
- ) : (
-
- {sortedQuestions.map((question) => (
-
- ))}
-
- )}
+
+ {sortedQuestions.map((question) => (
+
+ ))}
+
);
};
diff --git a/peerprep/components/shared/PeerprepButton.tsx b/peerprep/components/shared/PeerprepButton.tsx
index c0436a76ad..c2e70d3ec6 100644
--- a/peerprep/components/shared/PeerprepButton.tsx
+++ b/peerprep/components/shared/PeerprepButton.tsx
@@ -3,18 +3,27 @@ import React from "react";
import styles from "@/style/elements.module.css";
type PeerprepButtonProps = {
- onClick: () => void;
+ onClick?: () => void;
children: React.ReactNode;
className?: string;
+ disabled?: boolean;
+ type?: "button" | "submit" | "reset";
};
const PeerprepButton: React.FC
= ({
onClick,
children,
className,
+ disabled,
+ type,
}) => {
return (
-
+
{children}
);
diff --git a/peerprep/components/shared/PeerprepDropdown.tsx b/peerprep/components/shared/PeerprepDropdown.tsx
index 19911eb835..8662029ceb 100644
--- a/peerprep/components/shared/PeerprepDropdown.tsx
+++ b/peerprep/components/shared/PeerprepDropdown.tsx
@@ -17,8 +17,8 @@ const PeerprepDropdown = ({
className,
}: PeerprepDropdownProps): JSX.Element => {
return (
-
-
{label}
+
+
{label}
= ({
onChange,
}) => {
return (
-
+
+
= ({
+ isActive,
+}) => {
+ const [elapsedTime, setElapsedTime] = useState
(0);
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout | null = null;
+
+ if (isActive) {
+ interval = setInterval(() => {
+ setElapsedTime((prevTime) => prevTime + 1);
+ }, 1000);
+ }
+
+ return () => {
+ if (interval) clearInterval(interval);
+ setElapsedTime(0);
+ };
+ }, [isActive]);
+
+ const formatTime = (time: number) => {
+ const minutes = Math.floor(time / 60);
+ const seconds = time % 60;
+ return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
+ };
+
+ return {formatTime(elapsedTime)}
;
+};
+
+export default ResettingStopwatch;
diff --git a/peerprep/components/shared/TitleBar.tsx b/peerprep/components/shared/TitleBar.tsx
deleted file mode 100644
index 5092e43946..0000000000
--- a/peerprep/components/shared/TitleBar.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import styles from "@/style/layout.module.css";
-import Link from "next/link";
-
-function TitleBar() {
- return (
- // TODO: We probably need an AuthContext to wrap this in - we retrieve
- // user info to populate the values here.
- // I think NextJS handles the auth differently due to the server
- // context thing though - maybe can access from a cookie?
- // The thing is this component should only load in a few times,
- // so theoretically retrieval doesn't actually happen more than once or twice
- // in a lifetime
-
- );
-}
-
-export default TitleBar;
diff --git a/peerprep/components/shared/form/FormPasswordInput.tsx b/peerprep/components/shared/form/FormPasswordInput.tsx
deleted file mode 100644
index 5329ad1757..0000000000
--- a/peerprep/components/shared/form/FormPasswordInput.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ChangeEvent, ReactNode } from "react";
-import style from "@/style/form.module.css";
-
-type Props = {
- name: string;
- label: string;
- value?: string;
- children?: ReactNode;
- className?: string;
- required?: boolean;
- disabled?: boolean;
- id?: string;
- onChange?: (e: ChangeEvent) => void;
-};
-
-function PasswordInput({
- name,
- label,
- value,
- className,
- required,
- id,
- children,
- disabled,
- onChange,
-}: Props) {
- return (
-
-
{label}
-
- {children}
-
- );
-}
-
-export default PasswordInput;
diff --git a/peerprep/components/shared/form/FormTextInput.tsx b/peerprep/components/shared/form/FormTextInput.tsx
index 685423a225..a010c073b0 100644
--- a/peerprep/components/shared/form/FormTextInput.tsx
+++ b/peerprep/components/shared/form/FormTextInput.tsx
@@ -1,5 +1,8 @@
-import { ChangeEvent, ReactNode } from "react";
+"use client";
+
+import { ChangeEvent, ReactNode, useState } from "react";
import style from "@/style/form.module.css";
+import { Eye, EyeOff } from "lucide-react";
type Props = {
name: string;
@@ -11,6 +14,8 @@ type Props = {
disabled?: boolean;
id?: string;
onChange?: (e: ChangeEvent) => void;
+ isPassword?: boolean;
+ tooltip?: string;
};
function TextInput({
@@ -23,20 +28,48 @@ function TextInput({
children,
disabled,
onChange,
+ isPassword,
+ tooltip,
}: Props) {
+ const [isVisible, setIsVisible] = useState(false);
+
+ const toggleVisibility = () => {
+ setIsVisible(!isVisible);
+ };
return (
{label}
+
+ {isPassword && (
+
+ {isVisible ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {tooltip &&
{tooltip}
}
+
{children}
);
diff --git a/peerprep/components/shared/form/RadioButtonGroup.tsx b/peerprep/components/shared/form/RadioButtonGroup.tsx
index f516215e24..c357ea5981 100644
--- a/peerprep/components/shared/form/RadioButtonGroup.tsx
+++ b/peerprep/components/shared/form/RadioButtonGroup.tsx
@@ -5,7 +5,7 @@ interface Props {
label: string;
group: string;
options: {
- [label: string]: number;
+ [label: string]: string;
};
required?: boolean;
disabled?: boolean;
diff --git a/peerprep/components/ui/badge.tsx b/peerprep/components/ui/badge.tsx
new file mode 100644
index 0000000000..e87d62bf1a
--- /dev/null
+++ b/peerprep/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/peerprep/components/ui/button.tsx b/peerprep/components/ui/button.tsx
new file mode 100644
index 0000000000..65d4fcd9ca
--- /dev/null
+++ b/peerprep/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/peerprep/components/ui/form.tsx b/peerprep/components/ui/form.tsx
new file mode 100644
index 0000000000..0ae4001d1a
--- /dev/null
+++ b/peerprep/components/ui/form.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form";
+
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue,
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ");
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue,
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = "FormItem";
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = "FormLabel";
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = "FormControl";
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = "FormDescription";
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = "FormMessage";
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+};
diff --git a/peerprep/components/ui/input.tsx b/peerprep/components/ui/input.tsx
new file mode 100644
index 0000000000..5af26b2c1a
--- /dev/null
+++ b/peerprep/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/peerprep/components/ui/label.tsx b/peerprep/components/ui/label.tsx
new file mode 100644
index 0000000000..534182176b
--- /dev/null
+++ b/peerprep/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/peerprep/components/ui/select.tsx b/peerprep/components/ui/select.tsx
new file mode 100644
index 0000000000..ac2a8f2b9c
--- /dev/null
+++ b/peerprep/components/ui/select.tsx
@@ -0,0 +1,164 @@
+"use client"
+
+import * as React from "react"
+import {
+ CaretSortIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+} from "@radix-ui/react-icons"
+import * as SelectPrimitive from "@radix-ui/react-select"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/peerprep/components/ui/tags.tsx b/peerprep/components/ui/tags.tsx
new file mode 100644
index 0000000000..8408b69054
--- /dev/null
+++ b/peerprep/components/ui/tags.tsx
@@ -0,0 +1,91 @@
+// input-tags.tsx
+// adapted from https://github.com/shadcn-ui/ui/issues/3647#issue-2276203618
+
+"use client";
+
+import * as React from "react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { XIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { type InputProps } from "./input";
+
+type InputTagsProps = Omit & {
+ value: string[];
+ onChange: React.Dispatch>;
+};
+
+const InputTags = React.forwardRef(
+ ({ className, value, onChange, ...props }, ref) => {
+ const [pendingDataPoint, setPendingDataPoint] = React.useState("");
+
+ React.useEffect(() => {
+ if (pendingDataPoint.includes(",")) {
+ const newDataPoints = new Set([
+ ...value,
+ ...pendingDataPoint.split(",").map((chunk) => chunk.trim()),
+ ]);
+ onChange(Array.from(newDataPoints));
+ setPendingDataPoint("");
+ }
+ }, [pendingDataPoint, onChange, value]);
+
+ const addPendingDataPoint = () => {
+ if (pendingDataPoint) {
+ const newDataPoints = new Set([...value, pendingDataPoint]);
+ onChange(Array.from(newDataPoints));
+ setPendingDataPoint("");
+ }
+ };
+
+ return (
+
+ {value.map((item) => (
+
+ {item}
+ {
+ onChange(value.filter((i) => i !== item));
+ }}
+ >
+
+
+
+ ))}
+ setPendingDataPoint(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === ",") {
+ e.preventDefault();
+ addPendingDataPoint();
+ } else if (
+ e.key === "Backspace" &&
+ pendingDataPoint.length === 0 &&
+ value.length > 0
+ ) {
+ e.preventDefault();
+ onChange(value.slice(0, -1));
+ }
+ }}
+ {...props}
+ ref={ref}
+ />
+
+ );
+ },
+);
+
+InputTags.displayName = "InputTags";
+
+export { InputTags };
diff --git a/peerprep/contexts/QuestionFilterContext.tsx b/peerprep/contexts/QuestionFilterContext.tsx
new file mode 100644
index 0000000000..a8ff8f8110
--- /dev/null
+++ b/peerprep/contexts/QuestionFilterContext.tsx
@@ -0,0 +1,54 @@
+"use client";
+import { Difficulty } from "@/api/structs";
+import React, { createContext, ReactNode, useContext, useState } from "react";
+
+interface QuestionFilterContextType {
+ difficulties: Difficulty[];
+ difficultyFilter: string;
+ setDifficultyFilter: (difficulty: string) => void;
+ topicList: string[];
+ setTopicList: (topics: string[]) => void;
+ topicFilter: string;
+ setTopicFilter: (topic: string) => void;
+}
+
+const QuestionFilterContext = createContext<
+ QuestionFilterContextType | undefined
+>(undefined);
+
+export const QuestionFilterProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const difficulties = Object.values(Difficulty);
+ const [difficultyFilter, setDifficultyFilter] = useState(
+ Difficulty.All,
+ );
+ const [topicList, setTopicList] = useState([]);
+ const [topicFilter, setTopicFilter] = useState("all");
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useQuestionFilter = (): QuestionFilterContextType => {
+ const context = useContext(QuestionFilterContext);
+ if (!context) {
+ throw new Error(
+ "useQuestionFilter must be used within a QuestionFilterProvider",
+ );
+ }
+ return context;
+};
diff --git a/peerprep/contexts/UserInfoContext.tsx b/peerprep/contexts/UserInfoContext.tsx
new file mode 100644
index 0000000000..8e03b0a5c0
--- /dev/null
+++ b/peerprep/contexts/UserInfoContext.tsx
@@ -0,0 +1,33 @@
+// maybe store SAFE user info and wrap the relevant client components in it (like titlebar? matchmaking?)
+"use client";
+import React, { createContext, ReactNode, useContext } from "react";
+import { UserData } from "@/api/structs";
+
+interface UserInfoProviderProps {
+ userData: UserData;
+ children: ReactNode;
+}
+
+const UserInfoContext = createContext(undefined);
+
+export function UserInfoProvider({
+ userData,
+ children,
+}: UserInfoProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * This should not be used to validate data!
+ */
+export const useUserInfo = (): UserData => {
+ const context = useContext(UserInfoContext);
+ if (!context) {
+ throw new Error("useUserInfo must be used within a UserInfoProvider");
+ }
+ return context;
+};
diff --git a/peerprep/lib/utils.ts b/peerprep/lib/utils.ts
new file mode 100644
index 0000000000..bd0c391ddd
--- /dev/null
+++ b/peerprep/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/peerprep/middleware.ts b/peerprep/middleware.ts
index cc5d32f1a7..f2d6b7c33b 100644
--- a/peerprep/middleware.ts
+++ b/peerprep/middleware.ts
@@ -1,23 +1,38 @@
-import { cookies } from 'next/headers'
-import { NextResponse } from 'next/server'
-import type { NextRequest } from 'next/server'
+import { cookies } from "next/headers";
+import { NextRequest, NextResponse } from "next/server";
+import { CookieNames } from "@/app/actions/session";
-function isNoSession(request: NextRequest): boolean {
- return request.nextUrl.pathname.startsWith('/auth') && cookies().has("session");
-}
+const protectedRoutes = ["/questions", "/user"];
-function isSession(request: NextRequest): boolean {
- return !request.nextUrl.pathname.startsWith('/auth') && !cookies().has("session");
-}
+const isValidSession = () => {
+ return cookies().has("session");
+};
+
+export async function middleware(request: NextRequest) {
+ const path = request.nextUrl.pathname;
+ const isProtectedRoute = protectedRoutes.some((route) => path.startsWith(route));
-export function middleware(request: NextRequest) {
- if (isNoSession(request)) {
- return NextResponse.redirect(new URL("/questions", request.url));
+ // UNCOMMENT AND ADD TO ENV IF JUST TESTING FRONTEND STUFF
+ if (process.env.NEXT_BYPASS_LOGIN === "yesplease") {
+ return NextResponse.next();
}
-
- if (isSession(request)) {
+
+ if (!isValidSession() && isProtectedRoute) {
return NextResponse.redirect(new URL("/auth/login", request.url));
}
+
+ if (path === "/auth/logout" || path === "/auth/logout/") {
+ const response = NextResponse.redirect(new URL("/auth/login", request.url));
+
+ for (const cookieName of Object.values(CookieNames)) {
+ response.cookies.delete(cookieName.valueOf());
+ }
+ // response.cookies.delete("session");
+
+ return response;
+ }
+
+ return NextResponse.next();
}
// taken from Next.JS's Middleware tutorial
@@ -30,6 +45,6 @@ export const config = {
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
- '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
+ "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.png$).*)",
],
-}
+};
diff --git a/peerprep/next.config.js b/peerprep/next.config.js
index 767719fc4f..fcbfd5b15f 100644
--- a/peerprep/next.config.js
+++ b/peerprep/next.config.js
@@ -1,4 +1,22 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {}
+const nextConfig = {};
-module.exports = nextConfig
+module.exports = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ reactStrictMode: false,
+ async rewrites() {
+ return [
+ {
+ source: "/api/proxy", // client connects to api/proxy
+ // by default we expect NEXT_PUBLIC_COLLAB to be http://collab:4000.
+ // double check this before using this.
+ destination: `${process.env.NEXT_PUBLIC_NGINX}/${process.env.NEXT_PUBLIC_COLLAB}/ws`,
+ }
+ ];
+ },
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+};
diff --git a/peerprep/package-lock.json b/peerprep/package-lock.json
index 4c000b1a9e..e6707b4f4c 100644
--- a/peerprep/package-lock.json
+++ b/peerprep/package-lock.json
@@ -8,26 +8,58 @@
"name": "peerprep",
"version": "0.1.0",
"dependencies": {
+ "@headlessui/react": "^2.2.0",
+ "@hookform/resolvers": "^3.9.1",
+ "@radix-ui/react-icons": "^1.3.1",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@tiptap/extension-link": "^2.9.1",
+ "@tiptap/extension-placeholder": "^2.9.1",
+ "@tiptap/extension-subscript": "^2.9.1",
+ "@tiptap/extension-superscript": "^2.9.1",
+ "@tiptap/extension-underline": "^2.9.1",
+ "@tiptap/pm": "^2.9.1",
+ "@tiptap/react": "^2.9.1",
+ "@tiptap/starter-kit": "^2.9.1",
+ "@types/diff-match-patch": "^1.0.36",
"@types/node": "22.5.5",
"@types/react": "18.3.8",
"@types/react-dom": "18.3.0",
"ace-builds": "^1.36.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "diff-match-patch": "^1.0.5",
"dompurify": "^3.1.7",
"eslint": "8.57.1",
"eslint-config-next": "14.2.13",
+ "framer-motion": "^11.11.10",
"isomorphic-dompurify": "^2.16.0",
+ "lucide-react": "^0.453.0",
"next": "^14.2.13",
"react": "18.3.1",
"react-ace": "^12.0.0",
"react-dom": "18.3.1",
+ "react-hook-form": "^7.53.1",
+ "simple-peer": "^9.11.1",
+ "socket.io-client": "^4.8.1",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7",
"typescript": "5.6.2",
"zod": "^3.23.8"
},
"devDependencies": {
+ "@types/ace": "^0.0.52",
"@types/dompurify": "^3.0.5",
+ "@types/simple-peer": "^9.11.8",
"autoprefixer": "^10.4.20",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.2.1",
+ "husky": "^9.1.6",
+ "lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
+ "prettier-plugin-tailwindcss": "^0.6.8",
"tailwindcss": "^3.4.12"
}
},
@@ -35,7 +67,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -100,6 +131,87 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
+ "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.26.25",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.25.tgz",
+ "integrity": "sha512-hZOmgN0NTOzOuZxI1oIrDu3Gcl8WViIkvPMpB4xdd4QD6xAMtwgwr3VPoiyH/bLtRcS1cDnhxLSD1NsMJmwh/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.8",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
+ "license": "MIT"
+ },
+ "node_modules/@headlessui/react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
+ "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.26.16",
+ "@react-aria/focus": "^3.17.1",
+ "@react-aria/interactions": "^3.21.3",
+ "@tanstack/react-virtual": "^3.8.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
+ "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -183,7 +295,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@@ -198,7 +309,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -208,7 +318,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -218,14 +327,12 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -445,193 +552,1383 @@
"node": ">=14"
}
},
- "node_modules/@rtsao/scc": {
+ "node_modules/@pkgr/core": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@radix-ui/number": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
- "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
+ "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
"license": "MIT"
},
- "node_modules/@rushstack/eslint-patch": {
- "version": "1.10.4",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz",
- "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
+ "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT"
},
- "node_modules/@swc/counter": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
- "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "license": "Apache-2.0"
- },
- "node_modules/@swc/helpers": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
- "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
- "license": "Apache-2.0",
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
+ "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
+ "license": "MIT",
"dependencies": {
- "@swc/counter": "^0.1.3",
- "tslib": "^2.4.0"
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@types/dompurify": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
- "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
+ "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
+ "license": "MIT",
"dependencies": {
- "@types/trusted-types": "*"
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "22.5.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
- "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"license": "MIT",
- "dependencies": {
- "undici-types": "~6.19.2"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@types/prop-types": {
- "version": "15.7.13",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
- "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "18.3.8",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
- "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
- "dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@types/react-dom": {
- "version": "18.3.0",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
- "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
- "dependencies": {
- "@types/react": "*"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@types/trusted-types": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
- "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
"license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.6.0",
- "@typescript-eslint/type-utils": "8.6.0",
- "@typescript-eslint/utils": "8.6.0",
- "@typescript-eslint/visitor-keys": "8.6.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
"peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
- "eslint": "^8.57.0 || ^9.0.0"
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
- "typescript": {
+ "@types/react": {
"optional": true
}
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
- "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
- "license": "BSD-2-Clause",
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
+ "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.6.0",
- "@typescript-eslint/types": "8.6.0",
- "@typescript-eslint/typescript-estree": "8.6.0",
- "@typescript-eslint/visitor-keys": "8.6.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "license": "MIT",
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0"
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
- "typescript": {
+ "@types/react": {
"optional": true
}
}
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
- "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
+ "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.6.0",
- "@typescript-eslint/visitor-keys": "8.6.0"
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
},
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
- "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.1.tgz",
+ "integrity": "sha512-QvYompk0X+8Yjlo/Fv4McrzxohDdM5GgLHyQcPpcsPvlOSXCGFjdbuyGL5dzRbg0GpknAjQJJZzdiRK7iWVuFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.x || ^17.x || ^18.x || ^19.x"
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.6.0",
- "@typescript-eslint/utils": "8.6.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "@radix-ui/react-use-layout-effect": "1.1.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
- "typescript": {
+ "@types/react": {
"optional": true
}
}
},
- "node_modules/@typescript-eslint/types": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
- "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
+ "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
+ "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
+ "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
+ "integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.0",
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
+ "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
+ "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+ "license": "MIT"
+ },
+ "node_modules/@react-aria/focus": {
+ "version": "3.18.4",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
+ "integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/interactions": "^3.22.4",
+ "@react-aria/utils": "^3.25.3",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/interactions": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
+ "integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.6",
+ "@react-aria/utils": "^3.25.3",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.6.tgz",
+ "integrity": "sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/utils": {
+ "version": "3.25.3",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.3.tgz",
+ "integrity": "sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.6",
+ "@react-stately/utils": "^3.10.4",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/utils": {
+ "version": "3.10.4",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.4.tgz",
+ "integrity": "sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-types/shared": {
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.25.0.tgz",
+ "integrity": "sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@remirror/core-constants": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
+ "license": "MIT"
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "license": "MIT"
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.10.4",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz",
+ "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
+ "license": "MIT"
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
+ "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.10.8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
+ "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tiptap/core": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.9.1.tgz",
+ "integrity": "sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.9.1.tgz",
+ "integrity": "sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.9.1.tgz",
+ "integrity": "sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.9.1.tgz",
+ "integrity": "sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.9.1.tgz",
+ "integrity": "sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-code": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.9.1.tgz",
+ "integrity": "sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-code-block": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.9.1.tgz",
+ "integrity": "sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.9.1.tgz",
+ "integrity": "sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.9.1.tgz",
+ "integrity": "sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.9.1.tgz",
+ "integrity": "sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.9.1.tgz",
+ "integrity": "sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-hard-break": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.9.1.tgz",
+ "integrity": "sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.9.1.tgz",
+ "integrity": "sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-history": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.9.1.tgz",
+ "integrity": "sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-horizontal-rule": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.9.1.tgz",
+ "integrity": "sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.9.1.tgz",
+ "integrity": "sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-link": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.9.1.tgz",
+ "integrity": "sha512-yG+e3e8cCCN9dZjX4ttEe3e2xhh58ryi3REJV4MdiEkOT9QF75Bl5pUbMIS4tQ8HkOr04QBFMHKM12kbSxg1BA==",
+ "license": "MIT",
+ "dependencies": {
+ "linkifyjs": "^4.1.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.9.1.tgz",
+ "integrity": "sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.9.1.tgz",
+ "integrity": "sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.9.1.tgz",
+ "integrity": "sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-placeholder": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.9.1.tgz",
+ "integrity": "sha512-Q/w3OOg/C6jGBf4QKEWKF9k+iaCQCgPoaIg2IDTPx8QmaxRfgoVE5Csd+oTOY/brdmSNXOxykZWEci6OJP+MbA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.9.1.tgz",
+ "integrity": "sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-subscript": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.9.1.tgz",
+ "integrity": "sha512-jjfuHmF2dCUAtHmJH2K/7HhOCleM3aPVOI/UsBBYa8xM4mDU4xuW1O5sLAr2JWcB1xxyk9YKcBWwyRq+b1ENFA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-superscript": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.9.1.tgz",
+ "integrity": "sha512-7cgAPpUNgO/3QdvCN9/6dWP6JQC641o8dSgkyv0XzVv0nxISck4SU+2eADRYQLyP2s4M3xuSEFhCCiKZleK2yA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.9.1.tgz",
+ "integrity": "sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz",
+ "integrity": "sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-underline": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.9.1.tgz",
+ "integrity": "sha512-IrUsIqKPgD7GcAjr4D+RC0WvLHUDBTMkD8uPNEoeD1uH9t9zFyDfMRPnx/z3/6Gf6fTh3HzLcHGibiW2HiMi2A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/pm": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.9.1.tgz",
+ "integrity": "sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-changeset": "^2.2.1",
+ "prosemirror-collab": "^1.3.1",
+ "prosemirror-commands": "^1.6.0",
+ "prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
+ "prosemirror-history": "^1.4.1",
+ "prosemirror-inputrules": "^1.4.0",
+ "prosemirror-keymap": "^1.2.2",
+ "prosemirror-markdown": "^1.13.0",
+ "prosemirror-menu": "^1.2.4",
+ "prosemirror-model": "^1.22.3",
+ "prosemirror-schema-basic": "^1.2.3",
+ "prosemirror-schema-list": "^1.4.1",
+ "prosemirror-state": "^1.4.3",
+ "prosemirror-tables": "^1.4.0",
+ "prosemirror-trailing-node": "^3.0.0",
+ "prosemirror-transform": "^1.10.0",
+ "prosemirror-view": "^1.34.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tiptap/react": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.9.1.tgz",
+ "integrity": "sha512-LQJ34ZPfXtJF36SZdcn4Fiwsl2WxZ9YRJI87OLnsjJ45O+gV/PfBzz/4ap+LF8LOS0AbbGhTTjBOelPoNm+aYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/extension-bubble-menu": "^2.9.1",
+ "@tiptap/extension-floating-menu": "^2.9.1",
+ "@types/use-sync-external-store": "^0.0.6",
+ "fast-deep-equal": "^3",
+ "use-sync-external-store": "^1.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@tiptap/starter-kit": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.9.1.tgz",
+ "integrity": "sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^2.9.1",
+ "@tiptap/extension-blockquote": "^2.9.1",
+ "@tiptap/extension-bold": "^2.9.1",
+ "@tiptap/extension-bullet-list": "^2.9.1",
+ "@tiptap/extension-code": "^2.9.1",
+ "@tiptap/extension-code-block": "^2.9.1",
+ "@tiptap/extension-document": "^2.9.1",
+ "@tiptap/extension-dropcursor": "^2.9.1",
+ "@tiptap/extension-gapcursor": "^2.9.1",
+ "@tiptap/extension-hard-break": "^2.9.1",
+ "@tiptap/extension-heading": "^2.9.1",
+ "@tiptap/extension-history": "^2.9.1",
+ "@tiptap/extension-horizontal-rule": "^2.9.1",
+ "@tiptap/extension-italic": "^2.9.1",
+ "@tiptap/extension-list-item": "^2.9.1",
+ "@tiptap/extension-ordered-list": "^2.9.1",
+ "@tiptap/extension-paragraph": "^2.9.1",
+ "@tiptap/extension-strike": "^2.9.1",
+ "@tiptap/extension-text": "^2.9.1",
+ "@tiptap/extension-text-style": "^2.9.1",
+ "@tiptap/pm": "^2.9.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@types/ace": {
+ "version": "0.0.52",
+ "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.52.tgz",
+ "integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/diff-match-patch": {
+ "version": "1.0.36",
+ "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
+ "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/dompurify": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+ "dependencies": {
+ "@types/trusted-types": "*"
+ }
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.5.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
+ "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.13",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
+ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
+ "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
+ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/simple-peer": {
+ "version": "9.11.8",
+ "resolved": "https://registry.npmjs.org/@types/simple-peer/-/simple-peer-9.11.8.tgz",
+ "integrity": "sha512-rvqefdp2rvIA6wiomMgKWd2UZNPe6LM2EV5AuY3CPQJF+8TbdrL5TjYdMf0VAjGczzlkH4l1NjDkihwbj3Xodw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
+ "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.6.0",
+ "@typescript-eslint/type-utils": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
+ "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.6.0",
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
+ "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
+ "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
+ "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -791,6 +2088,22 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -819,14 +2132,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -840,7 +2151,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -849,6 +2159,18 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+ "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@@ -1096,11 +2418,30 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -1164,6 +2505,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -1207,7 +2572,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1233,58 +2597,164 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz",
+ "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "2.0.0"
+ },
+ "funding": {
+ "url": "https://joebell.co.uk"
+ }
+ },
+ "node_modules/class-variance-authority/node_modules/clsx": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "node_modules/cli-truncate/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">= 8.10.0"
+ "node": ">=18"
},
"funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/chokidar/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "node_modules/cli-truncate/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
"dependencies": {
- "is-glob": "^4.0.1"
+ "ansi-regex": "^6.0.1"
},
"engines": {
- "node": ">= 6"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/client-only": {
@@ -1293,6 +2763,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1311,6 +2790,13 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1326,7 +2812,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1338,6 +2823,12 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1356,7 +2847,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -1553,23 +3043,28 @@
"node": ">=0.4.0"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
- "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
+ "license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true,
"license": "MIT"
},
"node_modules/doctrine": {
@@ -1608,6 +3103,49 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
+ "node_modules/engine.io-client": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
+ "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@@ -1632,6 +3170,25 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz",
+ "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==",
+ "license": "MIT"
+ },
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -1914,6 +3471,19 @@
}
}
},
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
"node_modules/eslint-import-resolver-node": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -2087,6 +3657,37 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
}
},
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
+ "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.9.1"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": "*",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-plugin-react": {
"version": "7.36.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
@@ -2256,12 +3857,50 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -2423,6 +4062,31 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "11.11.10",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz",
+ "integrity": "sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2433,7 +4097,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2480,6 +4143,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-browser-rtc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz",
+ "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==",
+ "license": "MIT"
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -2499,6 +4181,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -2757,6 +4461,32 @@
"node": ">= 14"
}
},
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/husky": {
+ "version": "9.1.6",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
+ "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -2768,6 +4498,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2833,6 +4583,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -2896,7 +4655,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -3146,7 +4904,20 @@
"node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-string": {
@@ -3250,6 +5021,7 @@
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.16.0.tgz",
"integrity": "sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==",
+ "license": "MIT",
"dependencies": {
"@types/dompurify": "^3.0.5",
"dompurify": "^3.1.7",
@@ -3294,7 +5066,6 @@
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -3446,7 +5217,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3456,9 +5226,190 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/linkifyjs": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
+ "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==",
+ "license": "MIT"
+ },
+ "node_modules/lint-staged": {
+ "version": "15.2.10",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz",
+ "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "~5.3.0",
+ "commander": "~12.1.0",
+ "debug": "~4.3.6",
+ "execa": "~8.0.1",
+ "lilconfig": "~3.1.2",
+ "listr2": "~8.2.4",
+ "micromatch": "~4.0.8",
+ "pidtree": "~0.6.0",
+ "string-argv": "~0.3.2",
+ "yaml": "~2.5.0"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lint-staged/node_modules/lilconfig": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
+ "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "8.2.5",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz",
+ "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3490,6 +5441,144 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3508,6 +5597,45 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
+ "node_modules/lucide-react": {
+ "version": "0.453.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz",
+ "integrity": "sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3549,6 +5677,32 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3589,7 +5743,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -3709,7 +5862,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3725,6 +5877,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.13",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz",
@@ -3743,7 +5924,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3876,6 +6056,22 @@
"wrappy": "1"
}
},
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3893,6 +6089,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/orderedmap": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
+ "license": "MIT"
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -4013,11 +6215,23 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4027,7 +6241,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -4046,7 +6259,6 @@
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -4075,7 +6287,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -4093,7 +6304,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
@@ -4113,7 +6323,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -4149,7 +6358,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -4162,7 +6370,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -4188,7 +6395,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -4202,7 +6408,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@@ -4230,6 +6435,98 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz",
+ "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig-melody": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-import-sort": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-style-order": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig-melody": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-import-sort": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-style-order": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4241,6 +6538,201 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/prosemirror-changeset": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz",
+ "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-collab": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-commands": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
+ "integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.10.2"
+ }
+ },
+ "node_modules/prosemirror-dropcursor": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz",
+ "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0",
+ "prosemirror-view": "^1.1.0"
+ }
+ },
+ "node_modules/prosemirror-gapcursor": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
+ "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.0.0",
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-view": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-history": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
+ "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.2.2",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.31.0",
+ "rope-sequence": "^1.3.0"
+ }
+ },
+ "node_modules/prosemirror-inputrules": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
+ "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-keymap": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz",
+ "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "w3c-keyname": "^2.2.0"
+ }
+ },
+ "node_modules/prosemirror-markdown": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
+ "integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": "^14.0.0",
+ "markdown-it": "^14.0.0",
+ "prosemirror-model": "^1.20.0"
+ }
+ },
+ "node_modules/prosemirror-menu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
+ "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==",
+ "license": "MIT",
+ "dependencies": {
+ "crelt": "^1.0.0",
+ "prosemirror-commands": "^1.0.0",
+ "prosemirror-history": "^1.0.0",
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-model": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
+ "integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
+ "license": "MIT",
+ "dependencies": {
+ "orderedmap": "^2.0.0"
+ }
+ },
+ "node_modules/prosemirror-schema-basic": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz",
+ "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.19.0"
+ }
+ },
+ "node_modules/prosemirror-schema-list": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
+ "integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.7.3"
+ }
+ },
+ "node_modules/prosemirror-state": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
+ "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.27.0"
+ }
+ },
+ "node_modules/prosemirror-tables": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.0.tgz",
+ "integrity": "sha512-eirSS2fwVYzKhvM2qeXSn9ix/SBn7QOLDftPQ4ImEQIevFDiSKAB6Lbrmm/WEgrbTDbCm+xhSq4gOD9w7wT59Q==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.1.2",
+ "prosemirror-model": "^1.8.1",
+ "prosemirror-state": "^1.3.1",
+ "prosemirror-transform": "^1.2.1",
+ "prosemirror-view": "^1.13.3"
+ }
+ },
+ "node_modules/prosemirror-trailing-node": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remirror/core-constants": "3.0.0",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.22.1",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-view": "^1.33.8"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
+ "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.34.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.3.tgz",
+ "integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.20.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4250,6 +6742,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4270,6 +6771,15 @@
],
"license": "MIT"
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -4287,51 +6797,149 @@
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-12.0.0.tgz",
"integrity": "sha512-PstU6CSMfYIJknb4su2Fa0WgLXzq2ufQgR6fjcSWuGT1hGTHkBzuKw+SncV8PuLCdSJBJc1VehPhyeXlWByG/g==",
"dependencies": {
- "ace-builds": "^1.32.8",
- "diff-match-patch": "^1.0.5",
- "lodash.get": "^4.4.2",
- "lodash.isequal": "^4.5.0",
- "prop-types": "^15.8.1"
+ "ace-builds": "^1.32.8",
+ "diff-match-patch": "^1.0.5",
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.53.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz",
+ "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
+ "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.6",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
+ "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
},
"peerDependencies": {
- "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
},
"peerDependencies": {
- "react": "^18.3.1"
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -4414,6 +7022,39 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -4424,6 +7065,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -4461,6 +7109,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rope-sequence": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
+ "license": "MIT"
+ },
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -4507,6 +7161,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safe-regex-test": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
@@ -4644,6 +7318,106 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-peer": {
+ "version": "9.11.1",
+ "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz",
+ "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "debug": "^4.3.2",
+ "err-code": "^3.0.1",
+ "get-browser-rtc": "^1.1.0",
+ "queue-microtask": "^1.2.3",
+ "randombytes": "^2.1.0",
+ "readable-stream": "^3.6.0"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4673,6 +7447,25 @@
"node": ">=10.0.0"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -4867,6 +7660,19 @@
"node": ">=4"
}
},
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -4906,7 +7712,6 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -4954,11 +7759,43 @@
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
+ "node_modules/synckit": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
+ "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
+ "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -4992,6 +7829,15 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -5011,7 +7857,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -5021,7 +7866,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -5030,6 +7874,15 @@
"node": ">=0.8"
}
},
+ "node_modules/tippy.js": {
+ "version": "6.3.7",
+ "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
+ "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@popperjs/core": "^2.9.0"
+ }
+ },
"node_modules/tldts": {
"version": "6.1.50",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.50.tgz",
@@ -5096,7 +7949,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/tsconfig-paths": {
@@ -5227,6 +8079,12 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -5288,11 +8146,68 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
+ "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
@@ -5581,11 +8496,18 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
- "dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/peerprep/package.json b/peerprep/package.json
index 044d9f7b43..b17e4be365 100644
--- a/peerprep/package.json
+++ b/peerprep/package.json
@@ -3,33 +3,73 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
+ "dev": "next dev --turbo",
"prebuild": "next telemetry disable",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "prepare": "husky",
+ "lint-staged": "lint-staged"
+ },
+ "lint-staged": {
+ "**/*.{tsx,ts,css}": [
+ "eslint --cache --fix",
+ "prettier --write --ignore-unknown"
+ ]
},
"dependencies": {
+ "@types/diff-match-patch": "^1.0.36",
+ "@headlessui/react": "^2.2.0",
+ "@hookform/resolvers": "^3.9.1",
+ "@radix-ui/react-icons": "^1.3.1",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-select": "^2.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@tiptap/extension-link": "^2.9.1",
+ "@tiptap/extension-placeholder": "^2.9.1",
+ "@tiptap/extension-subscript": "^2.9.1",
+ "@tiptap/extension-superscript": "^2.9.1",
+ "@tiptap/extension-underline": "^2.9.1",
+ "@tiptap/pm": "^2.9.1",
+ "@tiptap/react": "^2.9.1",
+ "@tiptap/starter-kit": "^2.9.1",
"@types/node": "22.5.5",
"@types/react": "18.3.8",
"@types/react-dom": "18.3.0",
"ace-builds": "^1.36.2",
+ "diff-match-patch": "^1.0.5",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
"dompurify": "^3.1.7",
"eslint": "8.57.1",
"eslint-config-next": "14.2.13",
+ "framer-motion": "^11.11.10",
"isomorphic-dompurify": "^2.16.0",
+ "lucide-react": "^0.453.0",
"next": "^14.2.13",
"react": "18.3.1",
"react-ace": "^12.0.0",
"react-dom": "18.3.1",
+ "simple-peer": "^9.11.1",
+ "socket.io-client": "^4.8.1",
+ "react-hook-form": "^7.53.1",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7",
"typescript": "5.6.2",
"zod": "^3.23.8"
},
"devDependencies": {
+ "@types/ace": "^0.0.52",
"@types/dompurify": "^3.0.5",
+ "@types/simple-peer": "^9.11.8",
"autoprefixer": "^10.4.20",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.2.1",
+ "husky": "^9.1.6",
+ "lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
+ "prettier-plugin-tailwindcss": "^0.6.8",
"tailwindcss": "^3.4.12"
}
}
diff --git a/peerprep/public/boring1.png b/peerprep/public/boring1.png
new file mode 100644
index 0000000000..69a334296b
Binary files /dev/null and b/peerprep/public/boring1.png differ
diff --git a/peerprep/public/boring2.png b/peerprep/public/boring2.png
new file mode 100644
index 0000000000..5fccbea86c
Binary files /dev/null and b/peerprep/public/boring2.png differ
diff --git a/peerprep/public/boring3.png b/peerprep/public/boring3.png
new file mode 100644
index 0000000000..84835894e5
Binary files /dev/null and b/peerprep/public/boring3.png differ
diff --git a/peerprep/public/person.png b/peerprep/public/person.png
new file mode 100644
index 0000000000..12be719539
Binary files /dev/null and b/peerprep/public/person.png differ
diff --git a/peerprep/style/addquestion.module.css b/peerprep/style/addquestion.module.css
new file mode 100644
index 0000000000..a74b7b3d8a
--- /dev/null
+++ b/peerprep/style/addquestion.module.css
@@ -0,0 +1,41 @@
+.controlGroup {
+ @apply flex flex-col;
+}
+
+.buttonGroup {
+ @apply flex flex-row gap-2 items-center flex-wrap my-1;
+}
+
+.button {
+ @apply flex items-center px-2 py-1 rounded-md bg-gray-700 text-gray-300 transition-colors duration-200;
+}
+
+.button:hover {
+ @apply bg-gray-600;
+}
+
+.button.isActive {
+ @apply bg-blue-600 text-white;
+}
+
+.button:disabled {
+ @apply bg-gray-800 text-gray-400 cursor-not-allowed opacity-50;
+}
+
+
+.tooltipWrapper {
+ @apply relative flex items-center;
+}
+
+.tooltip {
+ @apply absolute bottom-full mb-2 p-1 text-xs text-white bg-black rounded opacity-0 transition-opacity duration-200;
+ transform: translate(-50%, 0%);
+ left: 50%;
+ white-space: nowrap;
+}
+
+.tooltipWrapper:hover .tooltip {
+ @apply opacity-100 bg-black text-white;
+}
+
+
diff --git a/peerprep/style/elements.module.css b/peerprep/style/elements.module.css
index 6d19b17b2c..42af7f35c4 100644
--- a/peerprep/style/elements.module.css
+++ b/peerprep/style/elements.module.css
@@ -1,14 +1,14 @@
/*dropdown*/
.select {
- @apply text-text-2 bg-gray-1 rounded p-2 focus:outline-none focus:ring-2 focus:ring-highlight focus:fill-highlight;
+ @apply justify-start text-text-2 bg-gray-1 rounded p-2 focus:outline-none focus:ring-2 focus:ring-highlight focus:fill-highlight;
}
/*button*/
.button {
- @apply px-4 py-2 text-text-1 bg-gray-2 rounded hover:bg-gray-3 focus:outline-none focus:ring focus:ring-highlight;
+ @apply px-4 py-2 text-text-1 bg-gray-2 rounded hover:bg-gray-3 focus:outline-none focus:ring focus:ring-highlight;
}
/*search*/
.input {
- @apply bg-gray-1 rounded-md p-2 w-full text-text-2;
+ @apply bg-gray-1 rounded-md pl-10 pr-2 py-2 w-full text-text-2;
}
diff --git a/peerprep/style/form.module.css b/peerprep/style/form.module.css
index c726c5beb3..b3c2e1f5bf 100644
--- a/peerprep/style/form.module.css
+++ b/peerprep/style/form.module.css
@@ -12,28 +12,37 @@
}
.form_container {
- background-color: rgb(36, 36, 36);
color: white;
font-family: Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
- justify-content: flex-start;
+ justify-content: left;
gap: 10px;
- padding: 15px 10px;
- border-radius: 10px;
- width: 90%;
+ padding: 2rem;
+ border-radius: 12px;
+ max-width: 800px;
+ width: 100%;
+ text-align: left;
}
.input_container {
+ padding-top: 5px;
+ /*margin: 5px;*/
display: flex;
flex-direction: row;
align-items: center;
+ /*align-items: center;*/
gap: 10px;
+ position: relative;
}
.label {
text-wrap: nowrap;
+ width: 100px;
+ text-align: left;
+ margin-right: 10px;
+ font-weight: bold;
}
.deletableText {
@@ -55,10 +64,19 @@
}
.text_input {
- background-color: rgb(30, 31, 31);
+ /*background-color: rgb(30, 31, 31);*/
border: 1px solid white;
width: 80%;
- border-radius: 10%;
+ border-radius: 3%;
+ margin: 2px;
+ padding: 2px;
+ position: relative;
+ color: black;
+}
+
+.error {
+ color: red;
+ font-size: small;
}
.radio_container {
diff --git a/peerprep/style/question.module.css b/peerprep/style/question.module.css
index 6b2df882bd..cbe0293eb9 100644
--- a/peerprep/style/question.module.css
+++ b/peerprep/style/question.module.css
@@ -88,6 +88,29 @@
overflow-y: auto;
}
+.editorHTML ul {
+ padding: 0 1rem;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+ list-style-type: disc;
+
+ .editorHTML li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+}
+
+.editorHTML ol {
+ padding: 0 1rem;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+ list-style-type: decimal;
+
+ .editorHTML li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+}
+
+
.editorHTML code {
font-family: 'Courier New', Courier, monospace;
/*background-color: lightgray;*/
@@ -141,6 +164,8 @@
}
.editor_container {
+ display: flex;
+ flex-direction: column;
width: 48%;
height: 100%;
overflow-y: none;
diff --git a/peerprep/style/tiptap.css b/peerprep/style/tiptap.css
new file mode 100644
index 0000000000..c9c39f4f5e
--- /dev/null
+++ b/peerprep/style/tiptap.css
@@ -0,0 +1,54 @@
+.ProseMirror {
+ flex: 1;
+ height: 100%;
+ max-height: 100%;
+}
+
+.tiptap {
+ :first-child {
+ margin-top: 0;
+ }
+
+ outline: none;
+
+
+ ul {
+ padding: 0 1rem;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+ list-style-type: disc;
+
+ li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+ }
+
+ ol {
+ padding: 0 1rem;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+ list-style-type: decimal;
+
+ li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+ }
+
+
+ pre {
+ background: black;
+ border-radius: 0.5rem;
+ color: white;
+ font-family: 'JetBrainsMono', monospace;
+ margin: 1.5rem 0;
+ padding: 0.75rem 1rem;
+
+ code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/peerprep/tailwind.config.ts b/peerprep/tailwind.config.ts
index 51d07f6caa..d7d5d8d797 100644
--- a/peerprep/tailwind.config.ts
+++ b/peerprep/tailwind.config.ts
@@ -1,32 +1,82 @@
-import type { Config } from "tailwindcss";
+import type {Config} from "tailwindcss";
+
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const defaultTheme = require("tailwindcss/defaultTheme");
const config: Config = {
- content: [
+ darkMode: ["class"],
+ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
- extend: {
- backgroundImage: {
- "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
- "gradient-conic":
- "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
- },
- /*global theme colors*/
- colors: {
- "gray-1": "#1e293b",
- "gray-2": "#334155",
- "gray-3": "#475569",
- highlight: "#f8fafc",
- "text-1": "#f8fafc",
- "text-2": "#e2e8f0",
- "difficulty-easy": "#34d399",
- "difficulty-med": "#f59e0b",
- "difficulty-hard": "#f43f5e",
- },
- },
+ extend: {
+ fontFamily: {
+ sans: ["InterVariable", ...defaultTheme.fontFamily.sans]
+ },
+ backgroundImage: {
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+ 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
+ },
+ colors: {
+ 'gray-1': '#1e293b',
+ 'gray-2': '#334155',
+ 'gray-3': '#475569',
+ highlight: '#f8fafc',
+ 'text-1': '#f8fafc',
+ 'text-2': '#e2e8f0',
+ 'difficulty-easy': '#34d399',
+ 'difficulty-med': '#f59e0b',
+ 'difficulty-hard': '#f43f5e',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ '1': 'hsl(var(--chart-1))',
+ '2': 'hsl(var(--chart-2))',
+ '3': 'hsl(var(--chart-3))',
+ '4': 'hsl(var(--chart-4))',
+ '5': 'hsl(var(--chart-5))'
+ }
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ }
+ }
},
- plugins: [],
+ plugins: [require("tailwindcss-animate")],
};
export default config;
diff --git a/question_api/log/question_api.log b/question_api/log/question_api.log
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/storage-blob-api/.env.example b/storage-blob-api/.env.example
new file mode 100644
index 0000000000..8b3519dc88
--- /dev/null
+++ b/storage-blob-api/.env.example
@@ -0,0 +1,3 @@
+PORT=9300
+REDIS_URI=
+
diff --git a/storage-blob-api/.gitignore b/storage-blob-api/.gitignore
new file mode 100644
index 0000000000..fbf828d63a
--- /dev/null
+++ b/storage-blob-api/.gitignore
@@ -0,0 +1 @@
+log
\ No newline at end of file
diff --git a/storage-blob-api/Dockerfile b/storage-blob-api/Dockerfile
new file mode 100644
index 0000000000..753645bfa7
--- /dev/null
+++ b/storage-blob-api/Dockerfile
@@ -0,0 +1,16 @@
+FROM golang:1.20
+
+WORKDIR /storage-blob-api
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# Build
+RUN CGO_ENABLED=0 GOOS=linux go build -o /storage-blob-api/app
+
+EXPOSE 9300
+
+# Run
+CMD ["/storage-blob-api/app"]
\ No newline at end of file
diff --git a/storage-blob-api/go.mod b/storage-blob-api/go.mod
new file mode 100644
index 0000000000..4ae2b81ec2
--- /dev/null
+++ b/storage-blob-api/go.mod
@@ -0,0 +1,39 @@
+module storage-blob-api
+
+go 1.20
+
+require (
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/cors v1.7.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/gin-gonic/gin v1.10.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
+ github.com/go-redis/redis/v8 v8.11.5 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/storage-blob-api/go.sum b/storage-blob-api/go.sum
new file mode 100644
index 0000000000..69f34a0478
--- /dev/null
+++ b/storage-blob-api/go.sum
@@ -0,0 +1,95 @@
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
+github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/storage-blob-api/log/matching_service_api.log b/storage-blob-api/log/matching_service_api.log
new file mode 100644
index 0000000000..ec633a8b6f
--- /dev/null
+++ b/storage-blob-api/log/matching_service_api.log
@@ -0,0 +1,182 @@
+time="2024-10-16T11:48:22+08:00" level=info msg="Server started running successfully"
+time="2024-10-16T11:48:47+08:00" level=error msg="unable to unmarshal topicTags to json: unexpected end of JSON input"
+time="2024-10-16T11:51:52+08:00" level=error msg="unable to unmarshal topicTags to json: unexpected end of JSON input"
+time="2024-10-16T11:54:34+08:00" level=info msg="Server started running successfully"
+time="2024-10-16T11:54:38+08:00" level=error msg="unable to unmarshal topicTags to json: unexpected end of JSON input"
+time="2024-10-16T11:55:04+08:00" level=info msg="Server started running successfully"
+time="2024-10-16T11:55:08+08:00" level=error msg="unable to unmarshal topicTags to json: unexpected end of JSON input"
+time="2024-10-16T11:57:16+08:00" level=info msg="Server started running successfully"
+time="2024-10-16T12:15:40+08:00" level=info msg="Server started running successfully"
+time="2024-10-16T12:16:23+08:00" level=info msg="Request handled successfully"
+time="2024-10-25T22:39:43+08:00" level=info msg="Server started running successfully"
+time="2024-10-25T23:25:33+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-25T23:25:38+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-25T23:25:43+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-25T23:25:48+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-25T23:25:53+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-25T23:25:58+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:26:25+08:00" level=info msg="Server started running successfully"
+time="2024-10-27T15:27:00+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:01+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:05+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:06+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:10+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:11+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:15+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:16+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:20+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:21+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:25+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:27:26+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:12+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:15+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:17+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:20+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:22+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:25+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:27+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:30+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:32+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:35+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:37+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T15:29:40+08:00" level=error msg="error retrieving userId from database: dial tcp 127.0.0.1:6379: connectex: No connection could be made because the target machine actively refused it."
+time="2024-10-27T16:07:41+08:00" level=info msg="Server started running successfully"
+time="2024-10-27T16:16:35+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:16:39+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:19:15+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:19:15+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:19:46+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:19:47+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:21:16+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:21:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:21:29+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:21:29+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:22:49+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:22:49+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:27:08+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:27:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:27:53+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:27:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:28:34+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:28:34+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:29:38+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:29:39+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:22+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:22+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:27+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:27+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:32+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:32+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:37+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:34:37+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:35:59+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:01+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:04+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:06+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:10+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:12+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:15+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:18+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:23+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:36:59+08:00" level=info msg="Request handled successfully"
+time="2024-10-27T16:37:01+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:01:45+08:00" level=info msg="Server started running successfully"
+time="2024-10-28T13:06:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:06:57+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:06:57+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:06:57+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:12:27+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:12:29+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:51:05+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:51:08+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:58:04+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:58:04+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:59:39+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T13:59:40+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:00:51+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:00:51+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:04:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:04:56+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:10:08+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:10:08+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:17:53+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:17:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:17:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:17:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:20:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:20:18+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:31:34+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T14:31:34+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:41:50+08:00" level=info msg="Server started running successfully"
+time="2024-10-28T20:45:46+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:45:47+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:46:22+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:46:23+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:54:01+08:00" level=info msg="Server started running successfully"
+time="2024-10-28T20:56:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:56:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:58:10+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T20:58:12+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:01:05+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:01:05+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:01:40+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:01:41+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:09:16+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:32:19+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:32:20+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:33:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:33:12+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:33:42+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:33:43+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:34:08+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:34:10+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:34:52+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:34:52+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:36:23+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:36:24+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:39:25+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:39:26+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:40:28+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:40:30+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:41:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:41:13+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:43:24+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:43:26+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:43:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:43:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:45:16+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:45:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:48:26+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:48:26+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:49:35+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:49:36+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:49:51+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:49:51+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:52:16+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:52:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:54:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:54:12+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:54:53+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:54:53+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:57:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:57:11+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:57:27+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:57:28+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:58:01+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T21:58:02+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:00:27+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:00:28+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:03:17+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:03:18+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:03:54+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:03:55+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:06:04+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:06:04+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:07:10+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:07:10+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:07:30+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:07:30+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:09:56+08:00" level=info msg="Request handled successfully"
+time="2024-10-28T22:09:57+08:00" level=info msg="Request handled successfully"
diff --git a/storage-blob-api/main.go b/storage-blob-api/main.go
new file mode 100644
index 0000000000..4172e1c548
--- /dev/null
+++ b/storage-blob-api/main.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+ "log"
+ "os"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joho/godotenv"
+
+ "storage-blob-api/models"
+ "storage-blob-api/storage"
+ "storage-blob-api/transport"
+)
+
+func main() {
+ //initialise logger file and directory if they do not exist
+
+ err := godotenv.Load(".env")
+ if err != nil {
+ log.Fatal("Error loading environment variables: " + err.Error())
+ }
+
+ ORIGIN := os.Getenv("CORS_ORIGIN")
+ if ORIGIN == "" {
+ ORIGIN = "http://localhost:3000"
+ }
+ PORT := os.Getenv("PORT")
+ if PORT == "" {
+ PORT = ":9300"
+ }
+
+ logger := models.NewLogger()
+
+ logDirectory := "./log"
+
+ if err := os.MkdirAll(logDirectory, 0755); err != nil {
+ logger.Log.Error("Failed to create log directory: " + err.Error())
+ }
+
+ logFile, err := os.OpenFile("./log/matching_service_api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+
+ if err != nil {
+ logger.Log.Warn("Failed to log to file, using default stderr")
+ }
+
+ defer logFile.Close()
+
+ logger.Log.Out = logFile
+
+ REDIS_URI := os.Getenv("REDIS_URI")
+
+ if REDIS_URI == "" {
+ REDIS_URI = "localhost://9190"
+ }
+
+ REDIS_ROOM_MAPPING := 1
+
+ if os.Getenv("REDIS_ROOM_MAPPING") != "" {
+ num, err := strconv.Atoi(os.Getenv("REDIS_ROOM_MAPPING"))
+ if err != nil {
+ log.Fatal("DB no of room map is badly formatted" + err.Error())
+ } else {
+ REDIS_ROOM_MAPPING = num
+ }
+ }
+
+ roomMappings := storage.InitialiseRoomMappings(REDIS_URI, REDIS_ROOM_MAPPING)
+
+ router := gin.Default()
+ transport.SetCors(router, ORIGIN)
+ transport.SetAllEndpoints(router, roomMappings, logger)
+
+ logger.Log.Info("Server started running successfully")
+ router.Run(PORT)
+}
\ No newline at end of file
diff --git a/storage-blob-api/models/logger.go b/storage-blob-api/models/logger.go
new file mode 100644
index 0000000000..2149803b88
--- /dev/null
+++ b/storage-blob-api/models/logger.go
@@ -0,0 +1,15 @@
+package models
+
+import (
+ "github.com/sirupsen/logrus"
+)
+
+type Logger struct {
+ Log *logrus.Logger
+}
+
+func NewLogger() *Logger {
+ return &Logger {
+ Log: logrus.New(),
+ }
+}
\ No newline at end of file
diff --git a/storage-blob-api/models/room.go b/storage-blob-api/models/room.go
new file mode 100644
index 0000000000..576a1ddfbc
--- /dev/null
+++ b/storage-blob-api/models/room.go
@@ -0,0 +1,18 @@
+package models
+type Room struct {
+
+ RoomId string `json:"roomId"`
+ User1 string `json:"user1"` //requesting user
+ User2 string `json:"user2"` //other user
+ RequestTime string `json:"requestTime"` //takes user1's requestTime since this is older
+
+ //contains question Data
+ Title string `json:"title"`
+ TitleSlug string `json:"titleSlug"`
+ Difficulty string `json:"difficulty"`
+ TopicTags []string `json:"topicTags"`
+ Content string `json:"content"`
+ Schemas []string `json:"schemas"`
+ QuestionId int `json:"questionId"`
+
+}
diff --git a/storage-blob-api/storage/room_mappings.go b/storage-blob-api/storage/room_mappings.go
new file mode 100644
index 0000000000..41a5660b10
--- /dev/null
+++ b/storage-blob-api/storage/room_mappings.go
@@ -0,0 +1,20 @@
+package storage
+
+import (
+ redis "github.com/go-redis/redis/v8"
+)
+
+type RoomMappings struct {
+ Conn *redis.Client
+}
+
+func InitialiseRoomMappings(addr string, db_num int) *RoomMappings {
+ conn := redis.NewClient(&redis.Options{
+ Addr:addr,
+ DB: db_num,
+ })
+
+ return &RoomMappings{
+ Conn: conn,
+ }
+}
\ No newline at end of file
diff --git a/storage-blob-api/transport/endpoint.go b/storage-blob-api/transport/endpoint.go
new file mode 100644
index 0000000000..52789326cd
--- /dev/null
+++ b/storage-blob-api/transport/endpoint.go
@@ -0,0 +1,25 @@
+package transport
+
+import (
+ "storage-blob-api/models"
+ "storage-blob-api/storage"
+ "time"
+
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+)
+
+func SetAllEndpoints(router *gin.Engine, db *storage.RoomMappings, logger *models.Logger) {
+ router.GET("/request/:matchHash", HandleRequest(db, logger))
+}
+
+func SetCors(router *gin.Engine, origin string) {
+ router.Use(cors.New(cors.Config{
+ AllowOrigins: []string{origin},
+ AllowMethods: []string{"POST","OPTIONS"},
+ AllowHeaders: []string{"Origin", "Content-Type", "Content-Length", "Authorization"},
+ ExposeHeaders: []string{"Content-Length"},
+ AllowCredentials: true,
+ MaxAge: 2 * time.Minute,
+ }))
+}
\ No newline at end of file
diff --git a/storage-blob-api/transport/request_handler.go b/storage-blob-api/transport/request_handler.go
new file mode 100644
index 0000000000..df5b97dbed
--- /dev/null
+++ b/storage-blob-api/transport/request_handler.go
@@ -0,0 +1,84 @@
+package transport
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "storage-blob-api/models"
+ "storage-blob-api/storage"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-redis/redis/v8"
+)
+
+func HandleRequest(db *storage.RoomMappings, logger *models.Logger) (gin.HandlerFunc){
+ return func(ctx *gin.Context) {
+ matchHash := ctx.Param("matchHash")
+
+ result, err := db.Conn.HGetAll(context.Background(), matchHash).Result()
+
+ if err == redis.Nil {
+ logger.Log.Warn(fmt.Sprintf("matchHash %s expired: ", matchHash))
+ ctx.JSON(http.StatusGone, "matchHash has expired")
+ return
+ } else if err != nil {
+ logger.Log.Error(fmt.Errorf("error retrieving matchHash from database: %s", err.Error()))
+ ctx.JSON(http.StatusBadGateway, "error retriving matchHash from database")
+ return
+ }
+
+ if len(result) == 0 {
+ ctx.JSON(http.StatusNotFound, "data not ready")
+ return
+ }
+
+ var topics_json, schemas_json []string
+
+ if err1 := json.Unmarshal([]byte(result["topicTags"]), &topics_json); err1 != nil {
+ logger.Log.Error(fmt.Errorf("unable to unmarshal topicTags to json: %s", err1.Error()))
+ ctx.JSON(http.StatusBadGateway, "error unmarshling user topics")
+ return
+ }
+
+
+ if err2 := json.Unmarshal([]byte(result["schemas"]), &schemas_json); err2 != nil {
+ logger.Log.Error(fmt.Errorf("unable to unmarshal schemas to json: %s", err2.Error()))
+ ctx.JSON(http.StatusBadGateway, "error unmarshling schemas")
+ return
+ }
+
+ roomId, user1, user2, requestTime :=
+ result["roomId"], result["thisUser"], result["otherUser"], result["requestTime"]
+
+
+ title, titleSlug, difficulty, content, questionId_string :=
+ result["title"], result["titleSlug"], result["difficulty"], result["content"], result["id"]
+ questionId, err := strconv.Atoi(questionId_string)
+
+ if err != nil {
+ logger.Log.Error(fmt.Errorf("failed to convert questionId to int: %s", err.Error()))
+ ctx.JSON(http.StatusBadGateway, "questionId is not an int")
+ return
+ }
+
+ room := models.Room{
+ RoomId: roomId,
+ User1: user1,
+ User2: user2,
+ RequestTime: requestTime,
+
+ Title: title,
+ TitleSlug: titleSlug,
+ TopicTags: topics_json,
+ Difficulty: difficulty,
+ Content: content,
+ Schemas: schemas_json,
+ QuestionId: questionId,
+ }
+
+ ctx.JSON(http.StatusOK, room)
+ logger.Log.Info("Request handled successfully")
+ }
+}
\ No newline at end of file
diff --git a/user-service/Dockerfile b/user-service/Dockerfile
index 2abd40221c..72c48ca353 100644
--- a/user-service/Dockerfile
+++ b/user-service/Dockerfile
@@ -4,7 +4,8 @@ WORKDIR /user-service
# TODO: don't include the .env file in the COPY
# TODO: multistage build
COPY package*.json ./
-RUN npm install
+# why did it only work with --force
+RUN npm install --force --verbose
RUN npm rebuild bcrypt --build-from-source
COPY . .
EXPOSE 3001