diff --git a/.gitignore b/.gitignore index c118ab4b..ed98ea96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules dist -db/ iqps.db .env diff --git a/backend/go.mod b/backend/go.mod index 38e1764f..39b17f95 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -9,4 +9,14 @@ require ( github.com/lib/pq v1.10.9 ) -require github.com/joho/godotenv v1.5.1 // indirect +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) + +require ( + github.com/jackc/pgx/v5 v5.6.0 + github.com/joho/godotenv v1.5.1 +) diff --git a/backend/go.sum b/backend/go.sum index 04066dc7..d1a256b2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,8 +1,36 @@ +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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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= diff --git a/backend/handlers.go b/backend/handlers.go new file mode 100644 index 00000000..d3e20770 --- /dev/null +++ b/backend/handlers.go @@ -0,0 +1,486 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/jackc/pgx/v5" + "github.com/metakgp/iqps/backend/pkg/config" + "github.com/metakgp/iqps/backend/pkg/db" + "github.com/metakgp/iqps/backend/query" +) + +type contextKey string + +const ( + CLAIMS_KEY = contextKey("claims") + AUTHORIZATION_HEADER = "authorization" +) + +type httpResp struct { + Status string `json:"status"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +func HandleHealthCheck(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Yes, I'm alive!") +} + +func HandleQPYear(w http.ResponseWriter, r *http.Request) { + db := db.GetDB() + result := db.QueryRow(context.Background(), "SELECT MIN(year), MAX(year) FROM iqps") + var minYear, maxYear int + err := result.Scan(&minYear, &maxYear) + if err != nil { + config.Get().Logger.Info("HandleQPYear: No min and max year in database, setting to default") + minYear = time.Now().Year() + maxYear = time.Now().Year() + } + + sendResponse(w, http.StatusOK, map[string]int{"min": minYear, "max": maxYear}) +} + +func HandleLibraryPapers(w http.ResponseWriter, r *http.Request) { + db := db.GetDB() + rows, err := db.Query(context.Background(), "SELECT * FROM iqps WHERE from_library = 'true'") + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, "Could not Query Question Paper, Try Later!", nil) + return + } + defer rows.Close() + + var qps []QuestionPaper = make([]QuestionPaper, 0) + for rows.Next() { + qp := QuestionPaper{} + err := rows.Scan(&qp.ID, &qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink, &qp.FromLibrary) + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, "error parsing question paper details, contact admin ", nil) + config.Get().Logger.Error("HandleLibraryPapers: Error in parsing question paper details") + return + } + qps = append(qps, qp) + } + sendResponse(w, http.StatusOK, qps) +} + +func HandleQPSearch(w http.ResponseWriter, r *http.Request) { + db := db.GetDB() + course := r.URL.Query().Get("course") + if course == "" { + http.Error(w, "course is required", http.StatusBadRequest) + return + } + + query := query.QP_SEARCH + + // var params []interface{} + // params = append(params, course) + params := pgx.NamedArgs{ + "query_text": course, + } + exam := r.URL.Query().Get("exam") + if exam != "" { + query = fmt.Sprintf(`%s WHERE (exam = @exam OR exam = '')`, query) + params = pgx.NamedArgs{ + "query_text": course, + "exam": exam, + } + } + + rows, err := db.Query(context.Background(), query, params) + config.Get().Logger.Debug("rows were fetched") + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, err.Error(), nil) + return + } + defer rows.Close() + var qps []QuestionPaper = make([]QuestionPaper, 0) + for rows.Next() { + qp := QuestionPaper{} + err := rows.Scan(&qp.ID, &qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink, &qp.FromLibrary, &qp.UploadTimestamp, &qp.ApproveStatus) + if err != nil { + config.Get().Logger.Error("HandleQPSearch: Error parsing question paper details") + sendErrorResponse(w, http.StatusInternalServerError, err.Error(), nil) + return + } + if qp.Exam == "" { + qp.Exam = "unknown" + } + qp.FileLink = fmt.Sprintf("%s/%s", config.Get().StaticFilesUrl, qp.FileLink) + qps = append(qps, qp) + } + + config.Get().Logger.Info("HandleQPSearch: Question paper search query %s returned %d results", course, len(qps)) + sendResponse(w, http.StatusOK, qps) +} + +func ListUnapprovedPapers(w http.ResponseWriter, r *http.Request) { + db := db.GetDB() + rows, err := db.Query(context.Background(), "SELECT course_code, course_name, year, exam,filelink,id, from_library FROM iqps WHERE approve_status = false ORDER BY upload_timestamp ASC") + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, err.Error(), nil) + return + } + defer rows.Close() + + var qps []QuestionPaper = make([]QuestionPaper, 0) + for rows.Next() { + qp := QuestionPaper{} + err := rows.Scan(&qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink, &qp.ID, &qp.FromLibrary) + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, err.Error(), nil) + return + } + qp.FileLink = fmt.Sprintf("%s/%s", config.Get().StaticFilesUrl, qp.FileLink) + qps = append(qps, qp) + } + config.Get().Logger.Info("listUnapprovedPapers: Unapproved Question paper count", slog.Int("QP count", len(qps))) + sendResponse(w, http.StatusOK, qps) +} + +func HandleFileUpload(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var response []uploadEndpointRes = make([]uploadEndpointRes, 0) + // Max total size of 50MB + const MaxBodySize = 50 << 20 // 1<<20 = 1024*1024 = 1MB + r.Body = http.MaxBytesReader(w, r.Body, MaxBodySize) + + err := r.ParseMultipartForm(MaxBodySize) + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, "Incorrect File Size", nil) + config.Get().Logger.Error("HandleFileUpload: Request Body Size Exceeded") + return + } + + maxLimit := config.Get().MaxUploadLimit + + files := r.MultipartForm.File["files"] + log.Printf("/upload: Received %d files.", len(files)) + if len(files) > maxLimit { + config.Get().Logger.Error("HandleFileUpload: Allowed %s file uploads, found %d", slog.Int("max Limit", maxLimit), slog.Int("Upload Length", len(files))) + sendErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("maximum %d files allowed", maxLimit), nil) + return + } + + for _, fileHeader := range files { + resp := uploadEndpointRes{Filename: fileHeader.Filename, Status: "success"} + + if fileHeader.Size > 10<<20 { + config.Get().Logger.Error("HandleFileUpload: File of size %s uploaded", slog.Int64("filesize", fileHeader.Size)) + resp.Status = "failed" + resp.Description = "file size exceeds 10MB" + response = append(response, resp) + continue + } + + file, err := fileHeader.Open() + if err != nil { + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + } + defer file.Close() + + fileType := fileHeader.Header.Get("Content-Type") + if fileType != "application/pdf" { + config.Get().Logger.Error("HandleFileUpload:", slog.String("Invalid file type", fileType)) + resp.Status = "failed" + resp.Description = "invalid file type. Only PDFs are supported" + response = append(response, resp) + continue + } + + fileName := fileHeader.Filename + + FileNameList := r.MultipartForm.Value[fileName] + if len(FileNameList) == 0 { + config.Get().Logger.Error("HandleFileUpload: filename not provided") + resp.Status = "failed" + resp.Description = "filename not provided" + response = append(response, resp) + continue + } + + newFileName := FileNameList[0] + filePath := filepath.Join(config.Get().StaticFilesStorageLocation, config.Get().UploadedQPsPath, newFileName) + filePath = filePath + ".pdf" + filePath = filepath.Clean(filePath) + + // Duplicate filename handling + if _, err = os.Stat(filePath); err == nil { + for i := 1; true; i++ { + if _, err := os.Stat(fmt.Sprintf("%s-%d.pdf", filePath[:len(filePath)-4], i)); err == nil { + continue + } else if errors.Is(err, os.ErrNotExist) { + filePath = fmt.Sprintf("%s-%d.pdf", filePath[:len(filePath)-4], i) + break + } else { + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + } + } + } else if !errors.Is(err, os.ErrNotExist) { + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + } + + dest, err := os.Create(filePath) + if err != nil { + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + } + defer dest.Close() + + if _, err := io.Copy(dest, file); err != nil { + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + } + + err = populateDB(newFileName) + if err != nil { + _ = os.Remove(filePath) + resp.Status = "failed" + resp.Description = err.Error() + response = append(response, resp) + continue + + } + response = append(response, resp) + } + + config.Get().Logger.Info("HandleFileUpload: files successfully uploaded") + // return response to client + sendResponse(w, http.StatusAccepted, response) +} + +func populateDB(filename string) error { + db := db.GetDB() + qpData := strings.Split(filename, "_") + if len(qpData) != 5 { + return fmt.Errorf("invalid filename format") + } + + courseCode := qpData[0] + courseName := qpData[1] + + year, _ := strconv.Atoi(qpData[2]) + exam := qpData[3] + fromLibrary := false + fileLink := filepath.Join(config.Get().UploadedQPsPath, filename+".pdf") + query := "INSERT INTO iqps (course_code, course_name, year, exam, filelink, from_library) VALUES ($1, $2, $3, $4, $5, $6);" + + _, err := db.Exec(context.Background(), query, courseCode, courseName, year, exam, fileLink, fromLibrary) + if err != nil { + return fmt.Errorf("failed to add qp to database: %v", err.Error()) + } + return nil +} + +func GhAuth(w http.ResponseWriter, r *http.Request) { + ghOAuthReqBody := GhOAuthReqBody{} + if err := json.NewDecoder(r.Body).Decode(&ghOAuthReqBody); err != nil { + sendErrorResponse(w, http.StatusBadRequest, err.Error(), nil) + return + } + + if ghOAuthReqBody.GhCode == "" { + sendErrorResponse(w, http.StatusBadRequest, "Github OAuth Code cannot be empty", nil) + return + } + + // Get the access token for authenticating other endpoints + uri := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", config.Get().GithubSecrets.PublicKey, config.Get().GithubSecrets.PrivateKey, ghOAuthReqBody.GhCode) + + req, _ := http.NewRequest("POST", uri, nil) + req.Header.Set("Accept", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error Getting Github Access Token: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "Could not connect to Github", nil) + return + } + defer resp.Body.Close() + + // Decode the response + var tokenResponse GithubAccessTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { + fmt.Println("Error Decoding Github Access Token: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "Could not decode github token", nil) + return + } + + // Get the username of the user who made the request + req, _ = http.NewRequest("GET", "https://api.github.com/user", nil) + req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken) + + resp, err = client.Do(req) + if err != nil { + fmt.Println("Error getting username: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "Could not decode github token", nil) + return + } + defer resp.Body.Close() + + // Decode the response + var userResponse GithubUserResponse + if err := json.NewDecoder(resp.Body).Decode(&userResponse); err != nil { + fmt.Println("Error decoding username: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "Could not decode github token", nil) + return + } + + uname := userResponse.Login + // check if uname is empty + if uname == "" { + sendErrorResponse(w, http.StatusUnauthorized, "username does not exist", nil) + return + } + + // Send request to check status of the user in the given org's team + url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s", config.Get().GithubSecrets.OrgName, config.Get().GithubSecrets.OrgTeam, uname) + req, _ = http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+config.Get().GithubSecrets.OrgAdminToken) + resp, err = client.Do(req) + + var checkResp struct { + State string `json:"state"` + } + + if err != nil { + fmt.Println("Error validating user membership: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "could not validate user membership", nil) + return + } + + defer resp.Body.Close() + // decode the response + if err := json.NewDecoder(resp.Body).Decode(&checkResp); err != nil { + fmt.Println("Error decoding gh validation body: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "could not decode github response", nil) + return + } + + // Check if user is present in the team + if checkResp.State != "active" { + sendErrorResponse(w, http.StatusUnauthorized, "user not allowed admin access", nil) + return + } + + // create JWT token + expirationTime := time.Now().Add(5 * time.Hour) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ + Username: uname, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }, + }) + + tokenString, err := token.SignedString([]byte(config.Get().JWTSecret)) + if err != nil { + fmt.Println("Error Sigining JWT: ", err.Error()) + sendErrorResponse(w, http.StatusInternalServerError, "Error signing JWT token, try again later", nil) + return + } + respData.Token = tokenString + + // Send the response + sendResponse(w, http.StatusAccepted, respData) +} + +func JWTMiddleware(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // get the authorisation header + tokenString := r.Header.Get(AUTHORIZATION_HEADER) + if tokenString == "" { + sendErrorResponse(w, http.StatusUnauthorized, "Missing authorization header", nil) + return + } + JWTtoken := strings.Split(tokenString, " ") + + if len(JWTtoken) != 2 && JWTtoken[0] != "Bearer" { + sendErrorResponse(w, http.StatusBadRequest, "Authorization header is incorrect", nil) + return + } + + claims := &Claims{} + token, err := jwt.ParseWithClaims(JWTtoken[1], claims, func(t *jwt.Token) (interface{}, error) { + if _, OK := t.Method.(*jwt.SigningMethodHMAC); !OK { + return nil, errors.New("bad signed method received") + } + + return []byte(config.Get().JWTSecret), nil + }) + // Check if error in parsing jwt token + if err != nil { + sendErrorResponse(w, http.StatusUnauthorized, err.Error(), nil) + return + } + // Get the claims + + if token.Valid && claims.Username != "" { + // If valid claims found, send response + ctx := context.WithValue(r.Context(), CLAIMS_KEY, claims) + handler.ServeHTTP(w, r.WithContext(ctx)) + } else { + sendErrorResponse(w, http.StatusUnauthorized, "Invalid JWT Token", nil) + return + } + }) +} + +func sendResponse(w http.ResponseWriter, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + + out, err := json.Marshal(httpResp{Status: "success", Data: data}) + if err != nil { + sendErrorResponse(w, http.StatusInternalServerError, "Internal Server Error", nil) + return + } + + w.Write(out) +} + +func sendErrorResponse(w http.ResponseWriter, statusCode int, message string, data interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(statusCode) + + resp := httpResp{ + Status: "error", + Message: message, + Data: data, + } + out, _ := json.Marshal(resp) + w.Write(out) +} diff --git a/backend/main.go b/backend/main.go index 017273ad..f56715ac 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,617 +1,42 @@ package main import ( - "context" - "database/sql" - "encoding/json" "errors" - "fmt" - "io" - "log" + "log/slog" "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - "github.com/golang-jwt/jwt/v5" "github.com/joho/godotenv" + "github.com/metakgp/iqps/backend/pkg/config" "github.com/rs/cors" _ "github.com/lib/pq" ) -type contextKey string - -const claimsKey = contextKey("claims") - -type QuestionPaper struct { - ID int `json:"id,omitempty"` - CourseCode string `json:"course_code,omitempty"` - CourseName string `json:"course_name,omitempty"` - Year int `json:"year,omitempty"` - Exam string `json:"exam,omitempty"` - FileLink string `json:"filelink,omitempty"` - FromLibrary bool `json:"from_library,omitempty"` - UploadTimestamp string `json:"upload_timestamp,omitempty"` - ApproveStatus bool `json:"approve_status,omitempty"` - CourseDetails string `json:"course_details,omitempty"` -} - -type uploadEndpointRes struct { - Filename string `json:"filename"` - Status string `json:"status"` - Description string `json:"description"` -} - -var ( - db *sql.DB - staticFilesUrl string - staticFilesStorageLocation string - uploadedQpsPath string - gh_pubKey string - gh_pvtKey string - jwt_secret string - org_name string - org_team string - gh_org_admin_token string -) - -type GhOAuthReqBody struct { - GhCode string `json:"code"` -} - -type GithubAccessTokenResponse struct { - AccessToken string `json:"access_token"` - Scope string `json:"scope"` - TokenType string `json:"token_type"` -} - -type GithubUserResponse struct { - Login string `json:"login"` - ID int `json:"id"` -} - -var respData struct { - Token string `json:"token"` -} - -const init_db = `CREATE TABLE IF NOT EXISTS qp ( - id SERIAL PRIMARY KEY, - course_code TEXT NOT NULL DEFAULT '', - course_name TEXT NOT NULL, - year INTEGER NOT NULL, - exam TEXT CHECK (exam IN ('midsem', 'endsem') OR exam = ''), - filelink TEXT NOT NULL, - from_library BOOLEAN DEFAULT FALSE, - upload_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - approve_status BOOLEAN DEFAULT FALSE, - course_details TEXT NOT NULL DEFAULT '' -); -` - -func health(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Yes, I'm alive!") -} - -func year(w http.ResponseWriter, r *http.Request) { - min := db.QueryRow("SELECT MIN(year) FROM qp") - max := db.QueryRow("SELECT MAX(year) FROM qp") - - var minYear, maxYear int - err := min.Scan(&minYear) - if err != nil { - minYear = time.Now().Year() - } - err = max.Scan(&maxYear) - if err != nil { - maxYear = time.Now().Year() - } - - http.Header.Add(w.Header(), "content-type", "application/json") - err = json.NewEncoder(w).Encode(map[string]int{"min": minYear, "max": maxYear}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func library(w http.ResponseWriter, r *http.Request) { - rows, err := db.Query("SELECT * FROM qp WHERE from_library = 'true'") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer rows.Close() - - var qps []QuestionPaper = make([]QuestionPaper, 0) - for rows.Next() { - qp := QuestionPaper{} - err := rows.Scan(&qp.ID, &qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink, &qp.FromLibrary) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - qps = append(qps, qp) - } - http.Header.Add(w.Header(), "content-type", "application/json") - err = json.NewEncoder(w).Encode(&qps) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func search(w http.ResponseWriter, r *http.Request) { - course := r.URL.Query().Get("course") - if course == "" { - http.Error(w, "course is required", http.StatusBadRequest) - return - } - - // query := `SELECT id,course_code,course_name,year,exam,filelink,from_library,upload_timestamp,approve_status FROM qp WHERE course_code_tsvector @@ websearch_to_tsquery('english', $1)` - query := `SELECT * FROM (SELECT id,course_code,course_name,year,exam,filelink,from_library,upload_timestamp,approve_status FROM qp WHERE course_details_tsvector @@ websearch_to_tsquery('simple', $1) AND approve_status=true UNION SELECT id,course_code,course_name,year,exam,filelink,from_library,upload_timestamp,approve_status from qp where course_details %>> $1 AND approve_status=true UNION SELECT id,course_code,course_name,year,exam,filelink,from_library,upload_timestamp,approve_status from qp where course_details_tsvector @@ to_tsquery('simple', websearch_to_tsquery('simple', $1)::text || ':*') AND approve_status=true)` - - var params []interface{} - params = append(params, course) - - exam := r.URL.Query().Get("exam") - if exam != "" { - query = fmt.Sprintf(`%s WHERE (exam = $2 OR exam = '')`, query) - params = append(params, exam) - } - - rows, err := db.Query(query, params...) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer rows.Close() - - var qps []QuestionPaper = make([]QuestionPaper, 0) - for rows.Next() { - qp := QuestionPaper{} - err := rows.Scan(&qp.ID, &qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink, &qp.FromLibrary, &qp.UploadTimestamp, &qp.ApproveStatus) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - qp.FileLink = fmt.Sprintf("%s/%s", staticFilesUrl, qp.FileLink) - qps = append(qps, qp) - } - - http.Header.Add(w.Header(), "content-type", "application/json") - err = json.NewEncoder(w).Encode(&qps) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func listUnapprovedPapers(w http.ResponseWriter, r *http.Request) { - rows, err := db.Query("SELECT course_code, course_name, year, exam, filelink FROM qp WHERE approve_status = false ORDER BY upload_timestamp ASC") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer rows.Close() - - var qps []QuestionPaper = make([]QuestionPaper, 0) - for rows.Next() { - qp := QuestionPaper{} - err := rows.Scan(&qp.CourseCode, &qp.CourseName, &qp.Year, &qp.Exam, &qp.FileLink) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - qps = append(qps, qp) - } - http.Header.Add(w.Header(), "content-type", "application/json") - err = json.NewEncoder(w).Encode(&qps) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func upload(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - var response []uploadEndpointRes = make([]uploadEndpointRes, 0) - // Max total size of 50MB - const MaxBodySize = 50 << 20 // 1<<20 = 1024*1024 = 1MB - r.Body = http.MaxBytesReader(w, r.Body, MaxBodySize) - - err := r.ParseMultipartForm(MaxBodySize) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - maxLimit, err := strconv.Atoi(os.Getenv("MAX_UPLOAD_LIMIT")) - if err != nil || maxLimit < 1 { - maxLimit = 10 - } - - files := r.MultipartForm.File["files"] - log.Printf("/upload: Received %d files.", len(files)) - if len(files) > maxLimit { - http.Error(w, fmt.Sprintf("maximum %d files allowed", maxLimit), http.StatusBadRequest) - return - } - - for _, fileHeader := range files { - resp := uploadEndpointRes{Filename: fileHeader.Filename, Status: "success"} - - if fileHeader.Size > 10<<20 { - resp.Status = "failed" - resp.Description = "file size exceeds 10MB" - response = append(response, resp) - continue - } - - file, err := fileHeader.Open() - if err != nil { - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - } - defer file.Close() - - fileType := fileHeader.Header.Get("Content-Type") - if fileType != "application/pdf" { - resp.Status = "failed" - resp.Description = "invalid file type. Only PDFs are supported" - response = append(response, resp) - continue - } - - fileName := fileHeader.Filename - - FileNameList := r.MultipartForm.Value[fileName] - if len(FileNameList) == 0 { - resp.Status = "failed" - resp.Description = "filename not provided" - response = append(response, resp) - continue - } - - newFileName := FileNameList[0] - filePath := filepath.Join(staticFilesStorageLocation, uploadedQpsPath, newFileName) - filePath = filePath + ".pdf" - filePath = filepath.Clean(filePath) - - // Duplicate filename handling - if _, err = os.Stat(filePath); err == nil { - for i := 1; true; i++ { - if _, err := os.Stat(fmt.Sprintf("%s-%d.pdf", filePath[:len(filePath)-4], i)); err == nil { - continue - } else if errors.Is(err, os.ErrNotExist) { - filePath = fmt.Sprintf("%s-%d.pdf", filePath[:len(filePath)-4], i) - break - } else { - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - } - } - } else if !errors.Is(err, os.ErrNotExist) { - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - } - - dest, err := os.Create(filePath) - if err != nil { - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - } - defer dest.Close() - - if _, err := io.Copy(dest, file); err != nil { - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - } - - err = populateDB(newFileName) - if err != nil { - _ = os.Remove(filePath) - resp.Status = "failed" - resp.Description = err.Error() - response = append(response, resp) - continue - - } - response = append(response, resp) - } - // return response to client - http.Header.Add(w.Header(), "content-type", "application/json") - err = json.NewEncoder(w).Encode(&response) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func populateDB(filename string) error { - qpData := strings.Split(filename, "_") - if len(qpData) != 5 { - return fmt.Errorf("invalid filename format") - } - - courseCode := qpData[0] - courseName := qpData[1] - courseDetails := strings.Join(strings.Split(filename, "_"), " ") - - year, _ := strconv.Atoi(qpData[2]) - exam := qpData[3] - fromLibrary := false - fileLink := filepath.Join(uploadedQpsPath, filename+".pdf") - query := "INSERT INTO qp (course_code, course_name, year, exam, filelink, from_library,course_details) VALUES ($1, $2, $3, $4, $5, $6,$7);" - - _, err := db.Exec(query, courseCode, courseName, year, exam, fileLink, fromLibrary, courseDetails) - if err != nil { - return fmt.Errorf("failed to add qp to database: %v", err) - } - return nil -} - -func GhAuth(w http.ResponseWriter, r *http.Request) { - ghOAuthReqBody := GhOAuthReqBody{} - if err := json.NewDecoder(r.Body).Decode(&ghOAuthReqBody); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if ghOAuthReqBody.GhCode == "" { - http.Error(w, "Github OAuth Code cannot be empty", http.StatusBadRequest) - return - } - - // Get the access token for authenticating other endpoints - uri := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", gh_pubKey, gh_pvtKey, ghOAuthReqBody.GhCode) - - req, _ := http.NewRequest("POST", uri, nil) - req.Header.Set("Accept", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - fmt.Println("Error Getting Github Access Token: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Decode the response - var tokenResponse GithubAccessTokenResponse - if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { - fmt.Println("Error Decoding Github Access Token: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - // Get the username of the user who made the request - req, _ = http.NewRequest("GET", "https://api.github.com/user", nil) - req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken) - - resp, err = client.Do(req) - if err != nil { - fmt.Println("Error getting username: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - // Decode the response - var userResponse GithubUserResponse - if err := json.NewDecoder(resp.Body).Decode(&userResponse); err != nil { - fmt.Println("Error decoding username: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - uname := userResponse.Login - // check if uname is empty - if uname == "" { - http.Error(w, "No user found", http.StatusUnauthorized) - return - } - - // Send request to check status of the user in the given org's team - url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s", org_name, org_team, uname) - req, _ = http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", "Bearer "+gh_org_admin_token) - resp, err = client.Do(req) - - var checkResp struct { - State string `json:"state"` - } - - if err != nil { - fmt.Println("Error validating user membership: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - defer resp.Body.Close() - // decode the response - if err := json.NewDecoder(resp.Body).Decode(&checkResp); err != nil { - fmt.Println("Error decoding gh validation body: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - // Check if user is present in the team - if checkResp.State != "active" { - - http.Error(w, "User is not authenticated", http.StatusUnauthorized) - return - } - - // Create the response JWT - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "username": uname, - }) - - tokenString, err := token.SignedString([]byte(jwt_secret)) - if err != nil { - fmt.Println("Error Sigining JWT: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - http.Header.Add(w.Header(), "content-type", "application/json") - - // Send the response - - respData.Token = tokenString - err = json.NewEncoder(w).Encode(&respData) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func JWTMiddleware(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // get the authorisation header - tokenString := r.Header.Get("Authorization") - if tokenString == "" { - w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, "Missing authorization header") - return - } - JWTtoken := strings.Split(tokenString, " ") - - fmt.Println(JWTtoken) - if len(JWTtoken) != 2 { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, "Authorisation head is of incorrect type") - return - } - // parse the token - token, err := jwt.Parse(JWTtoken[1], func(t *jwt.Token) (interface{}, error) { - if _, OK := t.Method.(*jwt.SigningMethodHMAC); !OK { - return nil, errors.New("bad signed method received") - } - - return []byte(jwt_secret), nil - }) - // Check if error in parsing jwt token - if err != nil { - http.Error(w, "Bad JWT token", http.StatusUnauthorized) - return - } - // Get the claims - claims, ok := token.Claims.(jwt.MapClaims) - - if ok && token.Valid && claims["username"] != nil { - // If valid claims found, send response - ctx := context.WithValue(r.Context(), claimsKey, claims) - handler.ServeHTTP(w, r.WithContext(ctx)) - } else { - http.Error(w, "Invalid JWT token", http.StatusUnauthorized) - } - }) -} - -func CheckError(err error) { - if err != nil { - panic(err) - } -} - -func LoadGhEnv() { - gh_pubKey = os.Getenv("GH_CLIENT_ID") - gh_pvtKey = os.Getenv("GH_PRIVATE_ID") - org_name = os.Getenv("GH_ORG_NAME") - org_team = os.Getenv("GH_ORG_TEAM_SLUG") - gh_org_admin_token = os.Getenv("GH_ORG_ADMIN_TOKEN") - - jwt_secret = os.Getenv("JWT_SECRET") - - if gh_pubKey == "" { - panic("Client id for Github OAuth cannot be empty") - } - if gh_pvtKey == "" { - panic("Client Private Key for Github OAuth cannot be empty") - } - if org_name == "" { - panic("Organisation name cannot be empty") - } - if org_team == "" { - panic("Team name of the Organistion cannot be empty") - } - if jwt_secret == "" { - panic("JWT Secret Key cannot be empty") - } - if gh_org_admin_token == "" { - panic("Github Organisation Admin Token cannot be empty") - } -} - func main() { godotenv.Load() - host := os.Getenv("DB_HOST") - port, err := strconv.Atoi(os.Getenv("DB_PORT")) - CheckError(err) - - LoadGhEnv() - - user := os.Getenv("DB_USER") - password := os.Getenv("DB_PASSWORD") - dbname := os.Getenv("DB_NAME") - - staticFilesUrl = os.Getenv("STATIC_FILES_URL") - staticFilesStorageLocation = os.Getenv("STATIC_FILES_STORAGE_LOCATION") - uploadedQpsPath = os.Getenv("UPLOADED_QPS_PATH") - - psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) - - db, err = sql.Open("postgres", psqlconn) - CheckError(err) - defer db.Close() - - err = db.Ping() - CheckError(err) - - _, err = db.Exec(init_db) - if err != nil { - log.Fatal(err) - } - http.HandleFunc("/health", health) - http.HandleFunc("/search", search) - http.HandleFunc("/year", year) - http.HandleFunc("/library", library) - http.HandleFunc("POST /upload", upload) + http.HandleFunc("/health", HandleHealthCheck) + http.HandleFunc("/search", HandleQPSearch) + http.HandleFunc("/year", HandleQPYear) + http.HandleFunc("/library", HandleLibraryPapers) + http.HandleFunc("POST /upload", HandleFileUpload) http.HandleFunc("POST /oauth", GhAuth) - http.Handle("GET /unapproved", JWTMiddleware(http.HandlerFunc(listUnapprovedPapers))) + http.Handle("GET /unapproved", JWTMiddleware(http.HandlerFunc(ListUnapprovedPapers))) + logger := config.Get().Logger c := cors.New(cors.Options{ AllowCredentials: true, AllowedHeaders: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, - AllowedOrigins: []string{"https://qp.metakgp.org", "http://localhost:3000"}, + AllowedOrigins: []string{"https://qp.metakgp.org", "http://localhost:3000", "http://localhost:5173"}, }) - fmt.Println("Starting server on port 5000") - err = http.ListenAndServe(":5000", c.Handler(http.DefaultServeMux)) + logger.Info("Main: Starting server on port 5000") + err := http.ListenAndServe(":5000", c.Handler(http.DefaultServeMux)) if errors.Is(err, http.ErrServerClosed) { - fmt.Printf("server closed\n") + logger.Error("server closed\n") } else if err != nil { - fmt.Printf("error starting server: %s\n", err) + logger.Error("Main:", slog.String("Error starting server", err.Error())) panic(err) } } diff --git a/backend/model.go b/backend/model.go new file mode 100644 index 00000000..e07499bb --- /dev/null +++ b/backend/model.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +var respData struct { + Token string `json:"token"` +} + +type QuestionPaper struct { + ID int `json:"id"` + CourseCode string `json:"course_code"` + CourseName string `json:"course_name"` + Year int `json:"year"` + Exam string `json:"exam"` + FileLink string `json:"filelink"` + FromLibrary bool `json:"from_library"` + UploadTimestamp pgtype.Timestamp `json:"upload_timestamp,omitempty"` + ApproveStatus bool `json:"approve_status,omitempty"` + CourseDetails string `json:"course_details,omitempty"` +} + +type uploadEndpointRes struct { + Filename string `json:"filename"` + Status string `json:"status"` + Description string `json:"description"` +} + +type GhOAuthReqBody struct { + GhCode string `json:"code"` +} + +type GithubAccessTokenResponse struct { + AccessToken string `json:"access_token"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` +} + +type GithubUserResponse struct { + Login string `json:"login"` + ID int `json:"id"` +} diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go new file mode 100644 index 00000000..574bc3fd --- /dev/null +++ b/backend/pkg/config/config.go @@ -0,0 +1,81 @@ +package config + +import ( + "log/slog" + "os" + "strconv" +) + +type Config struct { + DB databaseConfig + GithubSecrets githubSecrets + JWTSecret string + StaticFilesUrl string + StaticFilesStorageLocation string + UploadedQPsPath string + MaxUploadLimit int + Logger *slog.Logger +} + +type githubSecrets struct { + PublicKey string + PrivateKey string + OrgName string + OrgTeam string + OrgAdminToken string +} + +type databaseConfig struct { + Host string + Username string + Password string + DBname string + Port uint +} + +var config *Config + +func Get() *Config { + if config != nil { + return config + } + + var pgConf databaseConfig + port, err := strconv.ParseUint(os.Getenv("DB_PORT"), 10, 32) + if err != nil { + panic(err) + } + pgConf = databaseConfig{ + DBname: os.Getenv("DB_NAME"), + Username: os.Getenv("DB_USER"), + Password: os.Getenv("DB_PASSWORD"), + Host: os.Getenv("DB_HOST"), + Port: uint(port), + } + + githubSecrets := githubSecrets{ + PublicKey: os.Getenv("GH_CLIENT_ID"), + PrivateKey: os.Getenv("GH_PRIVATE_ID"), + OrgName: os.Getenv("GH_ORG_NAME"), + OrgTeam: os.Getenv("GH_ORG_TEAM_SLUG"), + OrgAdminToken: os.Getenv("GH_ORG_ADMIN_TOKEN"), + } + + maxUploadLimit, err := strconv.Atoi(os.Getenv("MAX_UPLOAD_LIMIT")) + if err != nil || maxUploadLimit < 1 { + maxUploadLimit = 10 + } + + config = &Config{ + DB: pgConf, + GithubSecrets: githubSecrets, + JWTSecret: os.Getenv("JWT_SECRET"), + StaticFilesUrl: os.Getenv("STATIC_FILES_URL"), + StaticFilesStorageLocation: os.Getenv("STATIC_FILES_STORAGE_LOCATION"), + UploadedQPsPath: os.Getenv("UPLOADED_QPS_PATH"), + Logger: slog.Default(), + MaxUploadLimit: maxUploadLimit, + } + config.Logger.Info("config successfully setup") + return config +} diff --git a/backend/pkg/db/db.go b/backend/pkg/db/db.go new file mode 100644 index 00000000..682bef87 --- /dev/null +++ b/backend/pkg/db/db.go @@ -0,0 +1,70 @@ +package db + +import ( + "context" + "fmt" + "log" + "sync" + + "github.com/jackc/pgx/v5" + "github.com/metakgp/iqps/backend/pkg/config" +) + +var ( + database *pgx.Conn + mu sync.Mutex +) + +const init_db = `CREATE TABLE IF NOT EXISTS iqps ( + id SERIAL PRIMARY KEY, + course_code TEXT NOT NULL DEFAULT '', + course_name TEXT NOT NULL, + year INTEGER NOT NULL, + exam TEXT CHECK (exam IN ('midsem', 'endsem') OR exam = ''), + filelink TEXT NOT NULL, + from_library BOOLEAN DEFAULT FALSE, + upload_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + approve_status BOOLEAN DEFAULT FALSE, + fts_course_details tsvector GENERATED ALWAYS AS (to_tsvector('english', course_code || ' ' || course_name)) stored +); + +CREATE INDEX IF NOT EXISTS iqps_fts ON iqps USING gin (fts_course_details); + +create index IF NOT EXISTS idx_course_name_trgm on iqps using gin (course_name gin_trgm_ops); + +` + +func InitDB() *pgx.Conn { + var err error + dbConfig := config.Get().DB + psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbConfig.Host, dbConfig.Port, dbConfig.Username, dbConfig.Password, dbConfig.DBname) + // database, err = sql.Open("postgres", psqlconn) + database, err = pgx.Connect(context.Background(), psqlconn) + if err != nil { + panic("Invalid Database connection string") + } + + err = database.Ping(context.Background()) + if err != nil { + panic("Database did not respond to ping") + } + fmt.Println("Database connected") + _, err = database.Exec(context.Background(), init_db) + if err != nil { + log.Fatal("Error initializting database: ", err.Error()) + } + + config.Get().Logger.Info("Successfully connected to database") + return database +} + +func GetDB() *pgx.Conn { + if database == nil { + mu.Lock() + defer mu.Unlock() + if database == nil { + database = InitDB() + } + } + return database +} diff --git a/backend/sql/indexes.sql b/backend/query/indexes.sql similarity index 100% rename from backend/sql/indexes.sql rename to backend/query/indexes.sql diff --git a/backend/sql/init_table.sql b/backend/query/init_table.sql similarity index 100% rename from backend/sql/init_table.sql rename to backend/query/init_table.sql diff --git a/backend/query/queries.go b/backend/query/queries.go new file mode 100644 index 00000000..47933bfa --- /dev/null +++ b/backend/query/queries.go @@ -0,0 +1,106 @@ +package query + +const QP_SEARCH_QUERY = ` +SELECT + * +FROM + ( + SELECT + id, + course_code, + course_name, + year, + exam, + filelink, + from_library, + upload_timestamp, + approve_status + FROM + qp + WHERE + course_details_tsvector @@ websearch_to_tsquery('simple', $1) + AND approve_status = true + UNION + SELECT + id, + course_code, + course_name, + year, + exam, + filelink, + from_library, + upload_timestamp, + approve_status + from + qp + where + course_details %>> $1 + AND approve_status = true + UNION + SELECT + id, + course_code, + course_name, + year, + exam, + filelink, + from_library, + upload_timestamp, + approve_status + from + qp + where + course_details_tsvector @@ to_tsquery( + 'simple', + websearch_to_tsquery('simple', $1)::text || ':*' + ) + AND approve_status = true + )` + +const QP_SEARCH = ` +with fuzzy as ( + select id, + similarity(course_code || ' ' || course_name, @query_text) as sim_score, + row_number() over (order by similarity(course_code || ' ' || course_name, @query_text) desc) as rank_ix + from iqps_test + where (course_code || ' ' || course_name) %>> @query_text + order by rank_ix +), +full_text as ( + select + id, + ts_rank_cd(fts_course_details, websearch_to_tsquery(@query_text)) as rank_score, + row_number() over(order by ts_rank_cd(fts_course_details , websearch_to_tsquery(@query_text)) desc) as rank_ix + from + iqps_test + where + fts_course_details @@ websearch_to_tsquery(@query_text) + AND approve_status = true + order by rank_ix +), +partial_search as ( + select id, + ts_rank_cd(fts_course_details , to_tsquery('simple', websearch_to_tsquery('simple', @query_text)::text || ':*' )) as rank_score, + row_number() over(order by ts_rank_cd(fts_course_details , to_tsquery('simple', websearch_to_tsquery('simple', @query_text)::text || ':*' )) desc) as rank_ix + from iqps_test where + fts_course_details @@ to_tsquery( + 'simple', + websearch_to_tsquery('simple', @query_text)::text || ':*' + ) + AND approve_status = true +), result as ( + select + iqps_test.id,iqps_test.course_code, iqps_test.course_name, iqps_test.year, iqps_test.exam, iqps_test.filelink, iqps_test.from_library, iqps_test.upload_timestamp, iqps_test.approve_status +from + fuzzy + full outer join full_text on fuzzy.id = full_text.id + full outer join partial_search on coalesce(fuzzy.id, full_text.id) = partial_search.id + join iqps_test on coalesce(fuzzy.id, full_text.id, partial_search.id) = iqps_test.id +order by + coalesce(1.0 / (50 + fuzzy.rank_ix), 0.0) * 1 + + coalesce(1.0 / (50 + full_text.rank_ix), 0.0) * 1 + + coalesce(1.0 / (50 + partial_search.rank_ix), 0.0) * 1 + desc +) + select * from result +` diff --git a/frontend/src/components/EditModal.tsx b/frontend/src/components/EditModal.tsx index 35460344..6ce5cba9 100644 --- a/frontend/src/components/EditModal.tsx +++ b/frontend/src/components/EditModal.tsx @@ -1,5 +1,5 @@ import { Component, createEffect, createSignal } from "solid-js"; -import { IErrorMessage, IQuestionPaperFile } from "../types/types"; +import { IErrorMessage, IQuestionPaperFile } from "../types/question_paper"; import { getCourseFromCode } from "../utils/autofillData"; import toast from "solid-toast"; import { validate } from "../utils/validateInput"; diff --git a/frontend/src/components/FileCard.tsx b/frontend/src/components/FileCard.tsx index f7e11cc0..d1b59249 100644 --- a/frontend/src/components/FileCard.tsx +++ b/frontend/src/components/FileCard.tsx @@ -3,7 +3,7 @@ import { AiOutlineDelete as CloseIcon, } from "solid-icons/ai"; import { Component } from "solid-js"; -import { IQuestionPaperFile } from "../types/types"; +import { IQuestionPaperFile } from "../types/question_paper"; import { FaRegularPenToSquare as EditIcon } from "solid-icons/fa"; type Props = { diff --git a/frontend/src/components/PDFTableBody.tsx b/frontend/src/components/PDFTableBody.tsx index a7820460..33b259b0 100644 --- a/frontend/src/components/PDFTableBody.tsx +++ b/frontend/src/components/PDFTableBody.tsx @@ -3,7 +3,7 @@ import { FaSolidFilePdf as PDFIcon } from "solid-icons/fa"; import COURSE_CODE_MAP from "../data/courses.json"; import { Component } from "solid-js"; import { createStore } from "solid-js/store"; -import { IAdminDashboardQP } from "../types/types"; +import { IAdminDashboardQP } from "../types/question_paper"; import { IoCheckmarkCircle } from "solid-icons/io"; import { Select, createOptions } from "@thisbeyond/solid-select"; import { fileNamer } from "../utils/fileNamer"; diff --git a/frontend/src/components/PDFTableHead.tsx b/frontend/src/components/PDFTableHead.tsx index 1b9810ec..cc89e9db 100644 --- a/frontend/src/components/PDFTableHead.tsx +++ b/frontend/src/components/PDFTableHead.tsx @@ -1,5 +1,5 @@ import { Component, For, createSignal } from "solid-js"; -import { IAdminDashboardQP } from "../types/types"; +import { IAdminDashboardQP } from "../types/question_paper"; import { ListElement } from "./PDFTableBody"; import { createStore } from "solid-js/store"; diff --git a/frontend/src/components/SearchForm.tsx b/frontend/src/components/SearchForm.tsx index 092d7ef4..8060c69d 100644 --- a/frontend/src/components/SearchForm.tsx +++ b/frontend/src/components/SearchForm.tsx @@ -1,7 +1,7 @@ import { Component, createSignal } from "solid-js"; import { IoSearch as SearchIcon, IoLink as ShareIcon } from "solid-icons/io"; import SearchResults from "./SearchResults"; -import type { ISearchResult } from "../types/types"; +import type { ISearchResult } from "../types/question_paper"; import "../styles/styles.scss"; import { copyLink } from "../utils/copyLink"; import { makeRequest } from "../utils/backend"; @@ -26,8 +26,8 @@ const CourseSearchForm: Component=()=> { setAwaitingResponse(true); const response = await makeRequest(`search?${params}`, 'get'); - if (response.is_ok) { - const data: ISearchResult[] = response.response; + if (response.status === 'success') { + const data: ISearchResult[] = response.data; setSearchResults(data); // Handle the response data @@ -46,7 +46,7 @@ const CourseSearchForm: Component=()=> { setAwaitingResponse(false); setErrMsg("Error fetching data. Please try again later."); - console.error("Error fetching data:", response.response.message); + console.error("Error fetching data:", response.message); } } } diff --git a/frontend/src/components/SearchResults.tsx b/frontend/src/components/SearchResults.tsx index db3ff419..ae0bce96 100644 --- a/frontend/src/components/SearchResults.tsx +++ b/frontend/src/components/SearchResults.tsx @@ -1,5 +1,5 @@ import { Component, For, createEffect, createSignal } from "solid-js"; -import type { ISearchResult } from "../types/types"; +import type { ISearchResult } from "../types/question_paper"; import { Spinner } from "./Spinner"; import { IoLink as ShareIcon } from "solid-icons/io"; import { FaSolidFilePdf as DownloadIcon } from "solid-icons/fa"; diff --git a/frontend/src/data/dummyQPs.ts b/frontend/src/data/dummyQPs.ts index 62d2b2e9..a8d2bd6c 100644 --- a/frontend/src/data/dummyQPs.ts +++ b/frontend/src/data/dummyQPs.ts @@ -1,4 +1,4 @@ -import { IAdminDashboardQP } from "../types/types"; +import { IAdminDashboardQP } from "../types/question_paper"; import { getCourseFromCode } from "../utils/autofillData"; import COURSE_CODE_MAP from "./courses.json" diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx index 4d1e3a5e..f15ce158 100644 --- a/frontend/src/pages/AdminDashboard.tsx +++ b/frontend/src/pages/AdminDashboard.tsx @@ -2,7 +2,7 @@ import { Component, createSignal } from "solid-js"; import { PDFLister } from "../components/PDFTableHead"; import { A } from "@solidjs/router"; import { useAuth } from "../components/AuthProvider"; -import { IAdminDashboardQP } from "../types/types"; +import { IAdminDashboardQP } from "../types/question_paper"; import { makeRequest } from "../utils/backend"; import { Spinner } from "../components/Spinner"; @@ -16,11 +16,11 @@ export const AdminPage: Component = () => { setFetchStatus("awaiting"); const response = await makeRequest('unapproved', 'get', null, auth.jwt()); - if (response.is_ok) { - setUnapprovedPapers(response.response); + if (response.status === 'success') { + setUnapprovedPapers(response.data); setFetchStatus("fetched"); } else { - setErrMsg(`Error fetching papers: ${response.response.message} (Status code: ${response.status_code})`); + setErrMsg(`Error fetching papers: ${response.message} (Status code: ${response.status_code})`); setFetchStatus("error"); } } diff --git a/frontend/src/pages/OAuth.tsx b/frontend/src/pages/OAuth.tsx index 02abcfef..00caba1f 100644 --- a/frontend/src/pages/OAuth.tsx +++ b/frontend/src/pages/OAuth.tsx @@ -11,9 +11,9 @@ export const OAuthPage: Component = () => { const loginHandler = async (code: string) => { const response = await makeRequest('oauth', 'post', {code}); - if (response.is_ok) { - if ("token" in response.response) { - auth.logIn(response.response["token"]); + if (response.status === 'success') { + if ("token" in response.data) { + auth.logIn(response.data["token"]); navigate('/admin'); } else { diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index be04b44d..4fa1e20b 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -5,10 +5,11 @@ import toast, { Toaster } from "solid-toast"; import { AiOutlineCloudUpload as UploadIcon, AiOutlineFileAdd as FileAddIcon } from "solid-icons/ai"; import { IoSearch as SearchIcon } from "solid-icons/io"; import { autofillData, sanitizeQP } from "../utils/autofillData"; -import { IQuestionPaperFile, UploadResults } from "../types/types"; +import { IQuestionPaperFile } from "../types/question_paper"; import Modal from "../components/EditModal"; import { Spinner } from "../components/Spinner"; import { validate } from "../utils/validateInput"; +import { makeRequest } from "../utils/backend"; const UploadPage: Component = () => { let MAX_UPLOAD_LIMIT = parseInt(import.meta.env.VITE_MAX_UPLOAD_LIMIT) @@ -155,37 +156,37 @@ const UploadPage: Component = () => { toast(`Uploading ${numPapers} file${numPapers > 1 ? 's' : ''}.`); setAwaitingResponse(true); - const response = await fetch( - `${import.meta.env.VITE_BACKEND_URL}/upload`, - { - method: "POST", - body: formData + const response = await makeRequest('upload', 'post', formData); + + if (response.status === 'success') { + const upload_results = response.data; + + for (const result of upload_results) { + if (result.status === "success") { + toast.success( + `File ${result.filename} uploaded successfully` + ); + } else { + toast.error( + `Failed to upload file ${result.filename}: ${result.description}` + ); + } } - ); - const upload_results: UploadResults = await response.json(); - for (const result of upload_results) { - if (result.status === "success") { - toast.success( - `File ${result.filename} uploaded successfully` - ); - } else { - toast.error( - `Failed to upload file ${result.filename}: ${result.description}` - ); + if (upload_results.length < numPapers) { + const failedPapers = numPapers - upload_results.length; + toast.error(`${failedPapers} paper${failedPapers > 1 ? 's' : ''} failed to upload.`); } - } - if (upload_results.length < numPapers) { - const failedPapers = numPapers - upload_results.length; - toast.error(`${failedPapers} paper${failedPapers > 1 ? 's' : ''} failed to upload.`) + clearQPapers(); + } else { + toast.error(`Failed to upload files. Error: ${response.message} (${response.status_code})`); } - clearQPapers(); setAwaitingResponse(false); } catch (error) { toast.error("Failed to upload file due to an unknown error. Please try again later."); - console.error("Error during upload:", error); + console.error("Upload error:", error); setAwaitingResponse(false); } } diff --git a/frontend/src/styles/search_results.scss b/frontend/src/styles/search_results.scss index ac4ad281..36782ec6 100644 --- a/frontend/src/styles/search_results.scss +++ b/frontend/src/styles/search_results.scss @@ -125,3 +125,20 @@ } } } + +.select-wrapper { + position: relative; + + select { + appearance: none; + } + + &::after { + content: "▼"; + line-height: 100%; + position: absolute; + top: calc(50%); + right: 15px; + transform: translateY(calc(-100%)); + } +} \ No newline at end of file diff --git a/frontend/src/styles/styles.scss b/frontend/src/styles/styles.scss index b291a27b..eec5e643 100644 --- a/frontend/src/styles/styles.scss +++ b/frontend/src/styles/styles.scss @@ -44,14 +44,6 @@ body { display: flex; flex-direction: column; width: 100%; - - .meta-footer { - width: 100%; - text-align: center; - - margin-top: auto; - margin-bottom: 10px; - } } } @@ -154,4 +146,12 @@ select option { fill: rgba($accent-color, 0.8); color: rgba($fg-color, 0.8); animation: spin 1s infinite; +} + +.meta-footer { + width: 100%; + text-align: center; + + margin-top: auto; + margin-bottom: 10px; } \ No newline at end of file diff --git a/frontend/src/types/backend.ts b/frontend/src/types/backend.ts new file mode 100644 index 00000000..63707dcf --- /dev/null +++ b/frontend/src/types/backend.ts @@ -0,0 +1,44 @@ +import { IAdminDashboardQP, ISearchResult } from "./question_paper"; + +export type AllowedBackendMethods = "get" | "post"; + +export interface IOkResponse { + status: "success"; + data: T; + status_code: 200; +} + +export interface IErrorResponse { + status: "error"; + message: string; + status_code: number | string; +} + +export type BackendResponse = IOkResponse | IErrorResponse; + +export interface IEndpointTypes { + [route: `search?${string}`]: { + request: null, + response: ISearchResult[] + }, + oauth: { + request: { + code: string + }, + response: { + token: string + } + }, + unapproved: { + request: null, + response: IAdminDashboardQP[] + }, + upload: { + request: FormData, + response: { + filename: string; + status: string; + description: string; + }[] + } +} \ No newline at end of file diff --git a/frontend/src/types/types.ts b/frontend/src/types/question_paper.ts similarity index 84% rename from frontend/src/types/types.ts rename to frontend/src/types/question_paper.ts index 38c9432c..d266954d 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/question_paper.ts @@ -31,12 +31,4 @@ export interface IErrorMessage { yearErr: string | null; examErr: string | null; semesterErr: string | null; -}; - -interface IUploadResult { - filename: string; - status: string; - description: string; -}; - -export type UploadResults = IUploadResult[]; \ No newline at end of file +}; \ No newline at end of file diff --git a/frontend/src/utils/autofillData.ts b/frontend/src/utils/autofillData.ts index 9893e65c..04427876 100644 --- a/frontend/src/utils/autofillData.ts +++ b/frontend/src/utils/autofillData.ts @@ -1,5 +1,5 @@ import COURSE_CODE_MAP from "../data/courses.json"; -import { Exam, IQuestionPaper, IQuestionPaperFile, Semester } from "../types/types"; +import { Exam, IQuestionPaper, IQuestionPaperFile, Semester } from "../types/question_paper"; import * as pdfjsLib from 'pdfjs-dist'; import Tesseract from 'tesseract.js'; import { validate, validateCourseCode, validateExam, validateSemester, validateYear } from "./validateInput"; diff --git a/frontend/src/utils/backend.ts b/frontend/src/utils/backend.ts index 45a017eb..b02ff425 100644 --- a/frontend/src/utils/backend.ts +++ b/frontend/src/utils/backend.ts @@ -1,14 +1,12 @@ -import { IAdminDashboardQP, IQuestionPaper, ISearchResult } from "../types/types"; +import { AllowedBackendMethods, BackendResponse, IEndpointTypes } from "../types/backend"; -const BACKEND_URL: string = import.meta.env.VITE_BACKEND_URL; - -type AllowedBackendMethods = "get" | "post"; +export const BACKEND_URL: string = import.meta.env.VITE_BACKEND_URL; async function makeBackendRequest( endpoint: string, method: AllowedBackendMethods, jwt: string | null, - body: Object | null, + body: Object | FormData | null, ): Promise { const headers: { "Content-Type"?: string; @@ -16,7 +14,12 @@ async function makeBackendRequest( } = new Object(); if (jwt !== null) headers["Authorization"] = `Bearer ${jwt}`; - if (body !== null) headers["Content-Type"] = "application/json"; + if ( + !( + body == null || + body instanceof FormData + ) + ) headers["Content-Type"] = "application/json"; switch (method) { case "get": @@ -28,49 +31,11 @@ async function makeBackendRequest( return await fetch(`${BACKEND_URL}/${endpoint}`, { method, headers, - body: JSON.stringify(body ?? {}), + body: body instanceof FormData ? body : JSON.stringify(body ?? {}), }); } } -interface IOkResponse { - is_ok: true; - status_code: 200; - response: T; -} - -interface IErrorResponse { - is_ok: false; - status_code: number | string; - response: IHTTPMessage; -} - -type BackendResponse = IOkResponse | IErrorResponse; - -export interface IHTTPMessage { - status_code?: number; - message: string; -} - -export interface IEndpointTypes { - [route: `search?${string}`]: { - request: null, - response: ISearchResult[] - }, - oauth: { - request: { - code: string - }, - response: { - token: string - } - }, - unapproved: { - request: null, - response: IAdminDashboardQP[] - } -} - export async function makeRequest( endpoint: E, method: AllowedBackendMethods, @@ -81,39 +46,22 @@ export async function makeRequest( const response = await makeBackendRequest(endpoint, method, jwt, params); try { - if (response.ok) { - return { - is_ok: true, - status_code: 200, - response: await response.json(), - }; - } - return { - is_ok: false, - status_code: response.status, - response: { - status_code: response.status, - message: await response.text() - }, - }; + ...await response.json(), + status_code: response.status + } } catch (e) { return { - is_ok: false, + status: "error", status_code: response.status, - response: { - status_code: response.status, - message: "An unexpected error occurred.", - }, + message: await response.text() }; } } catch (e) { return { - is_ok: false, - status_code: 'over 9000', - response: { - message: `An unexpected error occurred: ${e}` - } + status: "error", + status_code: 'Over 9000', + message: `An unexpected error occurred: ${e}` } } } \ No newline at end of file diff --git a/frontend/src/utils/fileNamer.ts b/frontend/src/utils/fileNamer.ts index 1c08ffe6..f15f1acf 100644 --- a/frontend/src/utils/fileNamer.ts +++ b/frontend/src/utils/fileNamer.ts @@ -1,4 +1,4 @@ -import { IAdminQuestionPaperResult } from "../types/types"; +import { IAdminQuestionPaperResult } from "../types/question_paper"; export function fileNamer (qp: IAdminQuestionPaperResult) { let exam = qp.exam === "endsem" ? "E" : "M"; diff --git a/frontend/src/utils/validateInput.ts b/frontend/src/utils/validateInput.ts index 0ac5371d..0d2f9cdb 100644 --- a/frontend/src/utils/validateInput.ts +++ b/frontend/src/utils/validateInput.ts @@ -1,4 +1,4 @@ -import { IErrorMessage, IQuestionPaperFile } from "../types/types"; +import { IErrorMessage, IQuestionPaperFile } from "../types/question_paper"; export const validateCourseCode = (course_code: string): boolean => { return course_code.length === 7 && course_code.match(/[a-z]{2}\d{5}/i) !== null; diff --git a/iqps-frontend/.env.development b/iqps-frontend/.env.development new file mode 100644 index 00000000..10f5ac1c --- /dev/null +++ b/iqps-frontend/.env.development @@ -0,0 +1,3 @@ +VITE_BACKEND_URL=http://localhost:5000 +VITE_MAX_UPLOAD_LIMIT=10 +VITE_GH_OAUTH_CLIENT_ID=Ov23lifQ41R6QGgUN4rM \ No newline at end of file diff --git a/iqps-frontend/.env.production b/iqps-frontend/.env.production new file mode 100644 index 00000000..251bd285 --- /dev/null +++ b/iqps-frontend/.env.production @@ -0,0 +1,3 @@ +VITE_BACKEND_URL=https://iqps-server.metakgp.org +VITE_MAX_UPLOAD_LIMIT=10 +VITE_GH_OAUTH_CLIENT_ID=Ov23lifQ41R6QGgUN4rM \ No newline at end of file diff --git a/iqps-frontend/.gitignore b/iqps-frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/iqps-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/iqps-frontend/eslint.config.js b/iqps-frontend/eslint.config.js new file mode 100644 index 00000000..092408a9 --- /dev/null +++ b/iqps-frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/iqps-frontend/favicon.ico b/iqps-frontend/favicon.ico new file mode 100644 index 00000000..80d9002b Binary files /dev/null and b/iqps-frontend/favicon.ico differ diff --git a/iqps-frontend/index.html b/iqps-frontend/index.html new file mode 100644 index 00000000..27643167 --- /dev/null +++ b/iqps-frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + + + IQPS - Metakgp + + +
+ + + diff --git a/iqps-frontend/package.json b/iqps-frontend/package.json new file mode 100644 index 00000000..4ebd419e --- /dev/null +++ b/iqps-frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "iqps-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "start": "pnpm dev", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "pdfjs-dist": "^4.6.82", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.3.0", + "react-router-dom": "^6.26.1", + "rollup-plugin-copy": "^3.5.0", + "tesseract.js": "^5.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.1", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.1", + "eslint-plugin-react-hooks": "5.1.0-rc-fb9a90fa48-20240614", + "eslint-plugin-react-refresh": "^0.4.11", + "globals": "^15.9.0", + "sass": "^1.77.8", + "typescript": "^5.5.4", + "typescript-eslint": "^8.3.0", + "vite": "^5.4.2" + }, + "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" +} diff --git a/iqps-frontend/pnpm-lock.yaml b/iqps-frontend/pnpm-lock.yaml new file mode 100644 index 00000000..ebbb0d2b --- /dev/null +++ b/iqps-frontend/pnpm-lock.yaml @@ -0,0 +1,2833 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + pdfjs-dist: + specifier: ^4.6.82 + version: 4.6.82 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-icons: + specifier: ^5.3.0 + version: 5.3.0(react@18.3.1) + react-router-dom: + specifier: ^6.26.1 + version: 6.26.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rollup-plugin-copy: + specifier: ^3.5.0 + version: 3.5.0 + tesseract.js: + specifier: ^5.1.1 + version: 5.1.1 + devDependencies: + '@eslint/js': + specifier: ^9.9.1 + version: 9.9.1 + '@types/react': + specifier: ^18.3.5 + version: 18.3.5 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.4.2(@types/node@22.5.2)(sass@1.77.8)) + eslint: + specifier: ^9.9.1 + version: 9.9.1 + eslint-plugin-react-hooks: + specifier: 5.1.0-rc-fb9a90fa48-20240614 + version: 5.1.0-rc-fb9a90fa48-20240614(eslint@9.9.1) + eslint-plugin-react-refresh: + specifier: ^0.4.11 + version: 0.4.11(eslint@9.9.1) + globals: + specifier: ^15.9.0 + version: 15.9.0 + sass: + specifier: ^1.77.8 + version: 1.77.8 + typescript: + specifier: ^5.5.4 + version: 5.5.4 + typescript-eslint: + specifier: ^8.3.0 + version: 8.3.0(eslint@9.9.1)(typescript@5.5.4) + vite: + specifier: ^5.4.2 + version: 5.4.2(@types/node@22.5.2)(sass@1.77.8) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.25.4': + resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.2': + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.6': + resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.2': + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.25.2': + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.24.8': + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.24.8': + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.25.6': + resolution: {integrity: sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.24.7': + resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.24.7': + resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.6': + resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.18.0': + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.9.1': + resolution: {integrity: sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@remix-run/router@1.19.1': + resolution: {integrity: sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==} + engines: {node: '>=14.0.0'} + + '@rollup/rollup-android-arm-eabi@4.21.2': + resolution: {integrity: sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.21.2': + resolution: {integrity: sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.21.2': + resolution: {integrity: sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.21.2': + resolution: {integrity: sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.21.2': + resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.21.2': + resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.21.2': + resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.21.2': + resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': + resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.21.2': + resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.21.2': + resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.21.2': + resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.21.2': + resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.21.2': + resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.21.2': + resolution: {integrity: sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.21.2': + resolution: {integrity: sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/node@22.5.2': + resolution: {integrity: sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==} + + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + '@types/react-dom@18.3.0': + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + + '@types/react@18.3.5': + resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==} + + '@typescript-eslint/eslint-plugin@8.3.0': + resolution: {integrity: sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.3.0': + resolution: {integrity: sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.3.0': + resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.3.0': + resolution: {integrity: sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.3.0': + resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.3.0': + resolution: {integrity: sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.3.0': + resolution: {integrity: sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.3.0': + resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.3.1': + resolution: {integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001655: + resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==} + + canvas@2.11.2: + resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@4.2.1: + resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} + engines: {node: '>=8'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.13: + resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614: + resolution: {integrity: sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.11: + resolution: {integrity: sha512-wrAKxMbVr8qhXTtIKfXqAn5SAtRZt0aXxe5P23Fh4pUAdC6XEsybGLB8P0PI4j1yYqOgUEUlzKAGDfo7rJOjcw==} + peerDependencies: + eslint: '>=7' + + eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.9.1: + resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} + engines: {node: '>=18'} + + globby@10.0.1: + resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} + engines: {node: '>=8'} + + goober@2.1.14: + resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + peerDependencies: + csstype: ^3.0.10 + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-object@3.0.1: + resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} + engines: {node: '>=0.10.0'} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-response@2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + nan@2.20.0: + resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path2d@0.2.1: + resolution: {integrity: sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==} + engines: {node: '>=6'} + + pdfjs-dist@4.6.82: + resolution: {integrity: sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==} + engines: {node: '>=18'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + postcss@8.4.44: + resolution: {integrity: sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-hot-toast@2.4.1: + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-icons@5.3.0: + resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} + peerDependencies: + react: '*' + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.26.1: + resolution: {integrity: sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.26.1: + resolution: {integrity: sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup-plugin-copy@3.5.0: + resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} + engines: {node: '>=8.3'} + + rollup@4.21.2: + resolution: {integrity: sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sass@1.77.8: + resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@3.1.1: + resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tesseract.js-core@5.1.1: + resolution: {integrity: sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==} + + tesseract.js@5.1.1: + resolution: {integrity: sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.3.0: + resolution: {integrity: sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.2: + resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + wasm-feature-detect@1.6.2: + resolution: {integrity: sha512-4dnaZ+Fq/q+BbMlTIfaNS851i+0zmHzui++NUZdskESRu3xwB6g6x2FnGvBdWtpijqO5yuj1l+EUTJGc4S4DKg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/compat-data@7.25.4': {} + + '@babel/core@7.25.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.6 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + convert-source-map: 2.0.0 + debug: 4.3.6 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.6': + dependencies: + '@babel/types': 7.25.6 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-compilation-targets@7.25.2': + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.24.8': {} + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/helper-validator-option@7.24.8': {} + + '@babel/helpers@7.25.6': + dependencies: + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.25.6': + dependencies: + '@babel/types': 7.25.6 + + '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + + '@babel/traverse@7.25.6': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 + debug: 4.3.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.25.6': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@9.9.1)': + dependencies: + eslint: 9.9.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint/config-array@0.18.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.6 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.6 + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.9.1': {} + + '@eslint/object-schema@2.1.4': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.0': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@remix-run/router@1.19.1': {} + + '@rollup/rollup-android-arm-eabi@4.21.2': + optional: true + + '@rollup/rollup-android-arm64@4.21.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.21.2': + optional: true + + '@rollup/rollup-darwin-x64@4.21.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.21.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.21.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.21.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.21.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.21.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.21.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.21.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.21.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.21.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.21.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.21.2': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.25.6 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.25.6 + + '@types/estree@1.0.5': {} + + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 22.5.2 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.5.2 + + '@types/minimatch@5.1.2': {} + + '@types/node@22.5.2': + dependencies: + undici-types: 6.19.8 + + '@types/prop-types@15.7.12': {} + + '@types/react-dom@18.3.0': + dependencies: + '@types/react': 18.3.5 + + '@types/react@18.3.5': + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + + '@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/type-utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + eslint: 9.9.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.6 + eslint: 9.9.1 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.3.0': + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + + '@typescript-eslint/type-utils@8.3.0(eslint@9.9.1)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + debug: 4.3.6 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.3.0': {} + + '@typescript-eslint/typescript-estree@8.3.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.6 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.3.0(eslint@9.9.1)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + eslint: 9.9.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.3.0': + dependencies: + '@typescript-eslint/types': 8.3.0 + eslint-visitor-keys: 3.4.3 + + '@vitejs/plugin-react@4.3.1(vite@5.4.2(@types/node@22.5.2)(sass@1.77.8))': + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.2(@types/node@22.5.2)(sass@1.77.8) + transitivePeerDependencies: + - supports-color + + abbrev@1.1.1: + optional: true + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + optional: true + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: + optional: true + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + bmp-js@0.1.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001655 + electron-to-chromium: 1.5.13 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001655: {} + + canvas@2.11.2: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + nan: 2.20.0 + simple-get: 3.1.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@2.0.0: + optional: true + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-support@1.1.3: + optional: true + + colorette@1.4.0: {} + + concat-map@0.0.1: {} + + console-control-strings@1.1.0: + optional: true + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + debug@4.3.6: + dependencies: + ms: 2.1.2 + + decompress-response@4.2.1: + dependencies: + mimic-response: 2.1.0 + optional: true + + deep-is@0.1.4: {} + + delegates@1.0.0: + optional: true + + detect-libc@2.0.3: + optional: true + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + electron-to-chromium@1.5.13: {} + + emoji-regex@8.0.0: + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.9.1): + dependencies: + eslint: 9.9.1 + + eslint-plugin-react-refresh@0.4.11(eslint@9.9.1): + dependencies: + eslint: 9.9.1 + + eslint-scope@8.0.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.0.0: {} + + eslint@9.9.1: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.18.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.9.1 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6 + escape-string-regexp: 4.0.0 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.1.0: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + optional: true + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.9.0: {} + + globby@10.0.1: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + goober@2.1.14(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-unicode@2.0.1: + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + optional: true + + idb-keyval@6.2.1: {} + + ignore@5.3.2: {} + + immutable@4.3.7: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-electron@2.2.2: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-object@3.0.1: {} + + is-url@1.2.4: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@2.5.2: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + optional: true + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-response@2.1.0: + optional: true + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + optional: true + + minipass@5.0.0: + optional: true + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + optional: true + + mkdirp@1.0.4: + optional: true + + ms@2.1.2: {} + + nan@2.20.0: + optional: true + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.18: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + normalize-path@3.0.0: {} + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + optional: true + + object-assign@4.1.1: + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + opencollective-postinstall@2.0.3: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + path2d@0.2.1: + optional: true + + pdfjs-dist@4.6.82: + optionalDependencies: + canvas: 2.11.2 + path2d: 0.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + postcss@8.4.44: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + goober: 2.1.14(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - csstype + + react-icons@5.3.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-refresh@0.14.2: {} + + react-router-dom@6.26.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.19.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.26.1(react@18.3.1) + + react-router@6.26.1(react@18.3.1): + dependencies: + '@remix-run/router': 1.19.1 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.13.11: {} + + resolve-from@4.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + rollup-plugin-copy@3.5.0: + dependencies: + '@types/fs-extra': 8.1.5 + colorette: 1.4.0 + fs-extra: 8.1.0 + globby: 10.0.1 + is-plain-object: 3.0.1 + + rollup@4.21.2: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.21.2 + '@rollup/rollup-android-arm64': 4.21.2 + '@rollup/rollup-darwin-arm64': 4.21.2 + '@rollup/rollup-darwin-x64': 4.21.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.2 + '@rollup/rollup-linux-arm-musleabihf': 4.21.2 + '@rollup/rollup-linux-arm64-gnu': 4.21.2 + '@rollup/rollup-linux-arm64-musl': 4.21.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.2 + '@rollup/rollup-linux-riscv64-gnu': 4.21.2 + '@rollup/rollup-linux-s390x-gnu': 4.21.2 + '@rollup/rollup-linux-x64-gnu': 4.21.2 + '@rollup/rollup-linux-x64-musl': 4.21.2 + '@rollup/rollup-win32-arm64-msvc': 4.21.2 + '@rollup/rollup-win32-ia32-msvc': 4.21.2 + '@rollup/rollup-win32-x64-msvc': 4.21.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: + optional: true + + sass@1.77.8: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.6.3: {} + + set-blocking@2.0.0: + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: + optional: true + + simple-concat@1.0.1: + optional: true + + simple-get@3.1.1: + dependencies: + decompress-response: 4.2.1 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + slash@3.0.0: {} + + source-map-js@1.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + optional: true + + tesseract.js-core@5.1.1: {} + + tesseract.js@5.1.1: + dependencies: + bmp-js: 0.1.0 + idb-keyval: 6.2.1 + is-electron: 2.2.2 + is-url: 1.2.4 + node-fetch: 2.7.0 + opencollective-postinstall: 2.0.3 + regenerator-runtime: 0.13.11 + tesseract.js-core: 5.1.1 + wasm-feature-detect: 1.6.2 + zlibjs: 0.3.1 + transitivePeerDependencies: + - encoding + + text-table@0.2.0: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-api-utils@1.3.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.3.0(eslint@9.9.1)(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.5.4: {} + + undici-types@6.19.8: {} + + universalify@0.1.2: {} + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.0.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: + optional: true + + vite@5.4.2(@types/node@22.5.2)(sass@1.77.8): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.44 + rollup: 4.21.2 + optionalDependencies: + '@types/node': 22.5.2 + fsevents: 2.3.3 + sass: 1.77.8 + + wasm-feature-detect@1.6.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yallist@3.1.1: {} + + yallist@4.0.0: + optional: true + + yocto-queue@0.1.0: {} + + zlibjs@0.3.1: {} diff --git a/iqps-frontend/src/App.tsx b/iqps-frontend/src/App.tsx new file mode 100644 index 00000000..4613f959 --- /dev/null +++ b/iqps-frontend/src/App.tsx @@ -0,0 +1,43 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import SearchPage from "./pages/SearchPage"; +import UploadPage from "./pages/UploadPage"; +import OAuthPage from "./pages/OAuthPage"; +import AdminDashboard from "./pages/AdminDashboard"; +import { Footer } from "./components/Common/Common"; +import { Toaster } from "react-hot-toast"; + +function App() { + return ( + <> + + + } + /> + } + /> + } + /> + } + /> + +