diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go index a06eccd..bb51584 100644 --- a/api/v1/api.gen.go +++ b/api/v1/api.gen.go @@ -9,17 +9,37 @@ import ( "context" "encoding/json" "fmt" + "mime/multipart" "net/http" "time" "github.com/oapi-codegen/runtime" strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" + openapi_types "github.com/oapi-codegen/runtime/types" ) const ( BearerAuthScopes = "BearerAuth.Scopes" ) +// Defines values for GetSearchParamsSource. +const ( + AnnasArchive GetSearchParamsSource = "Annas Archive" + LibGen GetSearchParamsSource = "LibGen" +) + +// Valid indicates whether the value is a known member of the GetSearchParamsSource enum. +func (e GetSearchParamsSource) Valid() bool { + switch e { + case AnnasArchive: + return true + case LibGen: + return true + default: + return false + } +} + // Activity defines model for Activity. type Activity struct { ActivityType string `json:"activity_type"` @@ -35,15 +55,34 @@ type ActivityResponse struct { User UserData `json:"user"` } +// DatabaseInfo defines model for DatabaseInfo. +type DatabaseInfo struct { + ActivitySize int64 `json:"activity_size"` + DevicesSize int64 `json:"devices_size"` + DocumentsSize int64 `json:"documents_size"` + ProgressSize int64 `json:"progress_size"` +} + +// Device defines model for Device. +type Device struct { + CreatedAt *time.Time `json:"created_at,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + Id *string `json:"id,omitempty"` + LastSynced *time.Time `json:"last_synced,omitempty"` +} + // Document defines model for Document. type Document struct { - Author string `json:"author"` - CreatedAt time.Time `json:"created_at"` - Deleted bool `json:"deleted"` - Id string `json:"id"` - Title string `json:"title"` - UpdatedAt time.Time `json:"updated_at"` - Words *int64 `json:"words,omitempty"` + Author string `json:"author"` + CreatedAt time.Time `json:"created_at"` + Deleted bool `json:"deleted"` + Filepath *string `json:"filepath,omitempty"` + Id string `json:"id"` + Percentage *float32 `json:"percentage,omitempty"` + Title string `json:"title"` + TotalTimeSeconds *int64 `json:"total_time_seconds,omitempty"` + UpdatedAt time.Time `json:"updated_at"` + Words *int64 `json:"words,omitempty"` } // DocumentResponse defines model for DocumentResponse. @@ -72,6 +111,41 @@ type ErrorResponse struct { Message string `json:"message"` } +// GraphDataPoint defines model for GraphDataPoint. +type GraphDataPoint struct { + Date string `json:"date"` + MinutesRead int64 `json:"minutes_read"` +} + +// GraphDataResponse defines model for GraphDataResponse. +type GraphDataResponse struct { + GraphData []GraphDataPoint `json:"graph_data"` + User UserData `json:"user"` +} + +// HomeResponse defines model for HomeResponse. +type HomeResponse struct { + DatabaseInfo DatabaseInfo `json:"database_info"` + GraphData GraphDataResponse `json:"graph_data"` + Streaks StreaksResponse `json:"streaks"` + User UserData `json:"user"` + UserStatistics UserStatisticsResponse `json:"user_statistics"` +} + +// LeaderboardData defines model for LeaderboardData. +type LeaderboardData struct { + All []LeaderboardEntry `json:"all"` + Month []LeaderboardEntry `json:"month"` + Week []LeaderboardEntry `json:"week"` + Year []LeaderboardEntry `json:"year"` +} + +// LeaderboardEntry defines model for LeaderboardEntry. +type LeaderboardEntry struct { + UserId string `json:"user_id"` + Value int64 `json:"value"` +} + // LoginRequest defines model for LoginRequest. type LoginRequest struct { Password string `json:"password"` @@ -86,38 +160,89 @@ type LoginResponse struct { // Progress defines model for Progress. type Progress struct { - CreatedAt time.Time `json:"created_at"` - DeviceId string `json:"device_id"` - DocumentId string `json:"document_id"` - Percentage float64 `json:"percentage"` - Progress string `json:"progress"` - UserId string `json:"user_id"` + Author *string `json:"author,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + DocumentId *string `json:"document_id,omitempty"` + Percentage *float64 `json:"percentage,omitempty"` + Title *string `json:"title,omitempty"` + UserId *string `json:"user_id,omitempty"` +} + +// ProgressListResponse defines model for ProgressListResponse. +type ProgressListResponse struct { + Limit *int64 `json:"limit,omitempty"` + NextPage *int64 `json:"next_page,omitempty"` + Page *int64 `json:"page,omitempty"` + PreviousPage *int64 `json:"previous_page,omitempty"` + Progress *[]Progress `json:"progress,omitempty"` + Total *int64 `json:"total,omitempty"` + User *UserData `json:"user,omitempty"` } // ProgressResponse defines model for ProgressResponse. -type ProgressResponse = Progress +type ProgressResponse struct { + Progress *Progress `json:"progress,omitempty"` + User *UserData `json:"user,omitempty"` +} -// Setting defines model for Setting. -type Setting struct { - Id string `json:"id"` - Key string `json:"key"` - UserId string `json:"user_id"` - Value string `json:"value"` +// SearchItem defines model for SearchItem. +type SearchItem struct { + Author *string `json:"author,omitempty"` + FileSize *string `json:"file_size,omitempty"` + FileType *string `json:"file_type,omitempty"` + Id *string `json:"id,omitempty"` + Language *string `json:"language,omitempty"` + Series *string `json:"series,omitempty"` + Title *string `json:"title,omitempty"` + UploadDate *string `json:"upload_date,omitempty"` +} + +// SearchResponse defines model for SearchResponse. +type SearchResponse struct { + Query string `json:"query"` + Results []SearchItem `json:"results"` + Source string `json:"source"` } // SettingsResponse defines model for SettingsResponse. type SettingsResponse struct { - Settings []Setting `json:"settings"` + Devices *[]Device `json:"devices,omitempty"` Timezone *string `json:"timezone,omitempty"` User UserData `json:"user"` } +// StreaksResponse defines model for StreaksResponse. +type StreaksResponse struct { + Streaks []UserStreak `json:"streaks"` + User UserData `json:"user"` +} + // UserData defines model for UserData. type UserData struct { IsAdmin bool `json:"is_admin"` Username string `json:"username"` } +// UserStatisticsResponse defines model for UserStatisticsResponse. +type UserStatisticsResponse struct { + Duration LeaderboardData `json:"duration"` + User UserData `json:"user"` + Words LeaderboardData `json:"words"` + Wpm LeaderboardData `json:"wpm"` +} + +// UserStreak defines model for UserStreak. +type UserStreak struct { + CurrentStreak int64 `json:"current_streak"` + CurrentStreakEndDate string `json:"current_streak_end_date"` + CurrentStreakStartDate string `json:"current_streak_start_date"` + MaxStreak int64 `json:"max_streak"` + MaxStreakEndDate string `json:"max_streak_end_date"` + MaxStreakStartDate string `json:"max_streak_start_date"` + Window string `json:"window"` +} + // WordCount defines model for WordCount. type WordCount struct { Count int64 `json:"count"` @@ -139,9 +264,44 @@ type GetDocumentsParams struct { Search *string `form:"search,omitempty" json:"search,omitempty"` } +// CreateDocumentMultipartBody defines parameters for CreateDocument. +type CreateDocumentMultipartBody struct { + DocumentFile openapi_types.File `json:"document_file"` +} + +// GetProgressListParams defines parameters for GetProgressList. +type GetProgressListParams struct { + Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` + Document *string `form:"document,omitempty" json:"document,omitempty"` +} + +// GetSearchParams defines parameters for GetSearch. +type GetSearchParams struct { + Query string `form:"query" json:"query"` + Source GetSearchParamsSource `form:"source" json:"source"` +} + +// GetSearchParamsSource defines parameters for GetSearch. +type GetSearchParamsSource string + +// PostSearchFormdataBody defines parameters for PostSearch. +type PostSearchFormdataBody struct { + Author string `form:"author" json:"author"` + Id string `form:"id" json:"id"` + Source string `form:"source" json:"source"` + Title string `form:"title" json:"title"` +} + // LoginJSONRequestBody defines body for Login for application/json ContentType. type LoginJSONRequestBody = LoginRequest +// CreateDocumentMultipartRequestBody defines body for CreateDocument for multipart/form-data ContentType. +type CreateDocumentMultipartRequestBody CreateDocumentMultipartBody + +// PostSearchFormdataRequestBody defines body for PostSearch for application/x-www-form-urlencoded ContentType. +type PostSearchFormdataRequestBody PostSearchFormdataBody + // ServerInterface represents all server handlers. type ServerInterface interface { // Get activity data @@ -159,12 +319,36 @@ type ServerInterface interface { // List documents // (GET /documents) GetDocuments(w http.ResponseWriter, r *http.Request, params GetDocumentsParams) + // Upload a new document + // (POST /documents) + CreateDocument(w http.ResponseWriter, r *http.Request) // Get a single document // (GET /documents/{id}) GetDocument(w http.ResponseWriter, r *http.Request, id string) + // Get home page data + // (GET /home) + GetHome(w http.ResponseWriter, r *http.Request) + // Get daily read stats graph data + // (GET /home/graph) + GetGraphData(w http.ResponseWriter, r *http.Request) + // Get user statistics (leaderboards) + // (GET /home/statistics) + GetUserStatistics(w http.ResponseWriter, r *http.Request) + // Get user streaks + // (GET /home/streaks) + GetStreaks(w http.ResponseWriter, r *http.Request) + // List progress records + // (GET /progress) + GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) // Get document progress // (GET /progress/{id}) GetProgress(w http.ResponseWriter, r *http.Request, id string) + // Search external book sources + // (GET /search) + GetSearch(w http.ResponseWriter, r *http.Request, params GetSearchParams) + // Download search result + // (POST /search) + PostSearch(w http.ResponseWriter, r *http.Request) // Get user settings // (GET /settings) GetSettings(w http.ResponseWriter, r *http.Request) @@ -339,6 +523,26 @@ func (siw *ServerInterfaceWrapper) GetDocuments(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } +// CreateDocument operation middleware +func (siw *ServerInterfaceWrapper) CreateDocument(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateDocument(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetDocument operation middleware func (siw *ServerInterfaceWrapper) GetDocument(w http.ResponseWriter, r *http.Request) { @@ -370,6 +574,135 @@ func (siw *ServerInterfaceWrapper) GetDocument(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// GetHome operation middleware +func (siw *ServerInterfaceWrapper) GetHome(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetHome(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetGraphData operation middleware +func (siw *ServerInterfaceWrapper) GetGraphData(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetGraphData(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetUserStatistics operation middleware +func (siw *ServerInterfaceWrapper) GetUserStatistics(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetUserStatistics(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetStreaks operation middleware +func (siw *ServerInterfaceWrapper) GetStreaks(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetStreaks(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetProgressList operation middleware +func (siw *ServerInterfaceWrapper) GetProgressList(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetProgressListParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "document" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "document", r.URL.Query(), ¶ms.Document, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "document", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetProgressList(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetProgress operation middleware func (siw *ServerInterfaceWrapper) GetProgress(w http.ResponseWriter, r *http.Request) { @@ -401,6 +734,81 @@ func (siw *ServerInterfaceWrapper) GetProgress(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// GetSearch operation middleware +func (siw *ServerInterfaceWrapper) GetSearch(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetSearchParams + + // ------------- Required query parameter "query" ------------- + + if paramValue := r.URL.Query().Get("query"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "query"}) + return + } + + err = runtime.BindQueryParameterWithOptions("form", true, true, "query", r.URL.Query(), ¶ms.Query, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "query", Err: err}) + return + } + + // ------------- Required query parameter "source" ------------- + + if paramValue := r.URL.Query().Get("source"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "source"}) + return + } + + err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSearch(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostSearch operation middleware +func (siw *ServerInterfaceWrapper) PostSearch(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostSearch(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetSettings operation middleware func (siw *ServerInterfaceWrapper) GetSettings(w http.ResponseWriter, r *http.Request) { @@ -546,8 +954,16 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc("POST "+options.BaseURL+"/auth/logout", wrapper.Logout) m.HandleFunc("GET "+options.BaseURL+"/auth/me", wrapper.GetMe) m.HandleFunc("GET "+options.BaseURL+"/documents", wrapper.GetDocuments) + m.HandleFunc("POST "+options.BaseURL+"/documents", wrapper.CreateDocument) m.HandleFunc("GET "+options.BaseURL+"/documents/{id}", wrapper.GetDocument) + m.HandleFunc("GET "+options.BaseURL+"/home", wrapper.GetHome) + m.HandleFunc("GET "+options.BaseURL+"/home/graph", wrapper.GetGraphData) + m.HandleFunc("GET "+options.BaseURL+"/home/statistics", wrapper.GetUserStatistics) + m.HandleFunc("GET "+options.BaseURL+"/home/streaks", wrapper.GetStreaks) + m.HandleFunc("GET "+options.BaseURL+"/progress", wrapper.GetProgressList) m.HandleFunc("GET "+options.BaseURL+"/progress/{id}", wrapper.GetProgress) + m.HandleFunc("GET "+options.BaseURL+"/search", wrapper.GetSearch) + m.HandleFunc("POST "+options.BaseURL+"/search", wrapper.PostSearch) m.HandleFunc("GET "+options.BaseURL+"/settings", wrapper.GetSettings) return m @@ -716,6 +1132,50 @@ func (response GetDocuments500JSONResponse) VisitGetDocumentsResponse(w http.Res return json.NewEncoder(w).Encode(response) } +type CreateDocumentRequestObject struct { + Body *multipart.Reader +} + +type CreateDocumentResponseObject interface { + VisitCreateDocumentResponse(w http.ResponseWriter) error +} + +type CreateDocument200JSONResponse DocumentResponse + +func (response CreateDocument200JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument400JSONResponse ErrorResponse + +func (response CreateDocument400JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument401JSONResponse ErrorResponse + +func (response CreateDocument401JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument500JSONResponse ErrorResponse + +func (response CreateDocument500JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetDocumentRequestObject struct { Id string `json:"id"` } @@ -760,6 +1220,177 @@ func (response GetDocument500JSONResponse) VisitGetDocumentResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type GetHomeRequestObject struct { +} + +type GetHomeResponseObject interface { + VisitGetHomeResponse(w http.ResponseWriter) error +} + +type GetHome200JSONResponse HomeResponse + +func (response GetHome200JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetHome401JSONResponse ErrorResponse + +func (response GetHome401JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetHome500JSONResponse ErrorResponse + +func (response GetHome500JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphDataRequestObject struct { +} + +type GetGraphDataResponseObject interface { + VisitGetGraphDataResponse(w http.ResponseWriter) error +} + +type GetGraphData200JSONResponse GraphDataResponse + +func (response GetGraphData200JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphData401JSONResponse ErrorResponse + +func (response GetGraphData401JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphData500JSONResponse ErrorResponse + +func (response GetGraphData500JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatisticsRequestObject struct { +} + +type GetUserStatisticsResponseObject interface { + VisitGetUserStatisticsResponse(w http.ResponseWriter) error +} + +type GetUserStatistics200JSONResponse UserStatisticsResponse + +func (response GetUserStatistics200JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatistics401JSONResponse ErrorResponse + +func (response GetUserStatistics401JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatistics500JSONResponse ErrorResponse + +func (response GetUserStatistics500JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaksRequestObject struct { +} + +type GetStreaksResponseObject interface { + VisitGetStreaksResponse(w http.ResponseWriter) error +} + +type GetStreaks200JSONResponse StreaksResponse + +func (response GetStreaks200JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaks401JSONResponse ErrorResponse + +func (response GetStreaks401JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaks500JSONResponse ErrorResponse + +func (response GetStreaks500JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressListRequestObject struct { + Params GetProgressListParams +} + +type GetProgressListResponseObject interface { + VisitGetProgressListResponse(w http.ResponseWriter) error +} + +type GetProgressList200JSONResponse ProgressListResponse + +func (response GetProgressList200JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressList401JSONResponse ErrorResponse + +func (response GetProgressList401JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressList500JSONResponse ErrorResponse + +func (response GetProgressList500JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetProgressRequestObject struct { Id string `json:"id"` } @@ -804,6 +1435,84 @@ func (response GetProgress500JSONResponse) VisitGetProgressResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type GetSearchRequestObject struct { + Params GetSearchParams +} + +type GetSearchResponseObject interface { + VisitGetSearchResponse(w http.ResponseWriter) error +} + +type GetSearch200JSONResponse SearchResponse + +func (response GetSearch200JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch400JSONResponse ErrorResponse + +func (response GetSearch400JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch401JSONResponse ErrorResponse + +func (response GetSearch401JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch500JSONResponse ErrorResponse + +func (response GetSearch500JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type PostSearchRequestObject struct { + Body *PostSearchFormdataRequestBody +} + +type PostSearchResponseObject interface { + VisitPostSearchResponse(w http.ResponseWriter) error +} + +type PostSearch200Response struct { +} + +func (response PostSearch200Response) VisitPostSearchResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type PostSearch401JSONResponse ErrorResponse + +func (response PostSearch401JSONResponse) VisitPostSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type PostSearch500JSONResponse ErrorResponse + +func (response PostSearch500JSONResponse) VisitPostSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetSettingsRequestObject struct { } @@ -855,12 +1564,36 @@ type StrictServerInterface interface { // List documents // (GET /documents) GetDocuments(ctx context.Context, request GetDocumentsRequestObject) (GetDocumentsResponseObject, error) + // Upload a new document + // (POST /documents) + CreateDocument(ctx context.Context, request CreateDocumentRequestObject) (CreateDocumentResponseObject, error) // Get a single document // (GET /documents/{id}) GetDocument(ctx context.Context, request GetDocumentRequestObject) (GetDocumentResponseObject, error) + // Get home page data + // (GET /home) + GetHome(ctx context.Context, request GetHomeRequestObject) (GetHomeResponseObject, error) + // Get daily read stats graph data + // (GET /home/graph) + GetGraphData(ctx context.Context, request GetGraphDataRequestObject) (GetGraphDataResponseObject, error) + // Get user statistics (leaderboards) + // (GET /home/statistics) + GetUserStatistics(ctx context.Context, request GetUserStatisticsRequestObject) (GetUserStatisticsResponseObject, error) + // Get user streaks + // (GET /home/streaks) + GetStreaks(ctx context.Context, request GetStreaksRequestObject) (GetStreaksResponseObject, error) + // List progress records + // (GET /progress) + GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error) // Get document progress // (GET /progress/{id}) GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) + // Search external book sources + // (GET /search) + GetSearch(ctx context.Context, request GetSearchRequestObject) (GetSearchResponseObject, error) + // Download search result + // (POST /search) + PostSearch(ctx context.Context, request PostSearchRequestObject) (PostSearchResponseObject, error) // Get user settings // (GET /settings) GetSettings(ctx context.Context, request GetSettingsRequestObject) (GetSettingsResponseObject, error) @@ -1026,6 +1759,37 @@ func (sh *strictHandler) GetDocuments(w http.ResponseWriter, r *http.Request, pa } } +// CreateDocument operation middleware +func (sh *strictHandler) CreateDocument(w http.ResponseWriter, r *http.Request) { + var request CreateDocumentRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateDocument(ctx, request.(CreateDocumentRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateDocument") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateDocumentResponseObject); ok { + if err := validResponse.VisitCreateDocumentResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetDocument operation middleware func (sh *strictHandler) GetDocument(w http.ResponseWriter, r *http.Request, id string) { var request GetDocumentRequestObject @@ -1052,6 +1816,128 @@ func (sh *strictHandler) GetDocument(w http.ResponseWriter, r *http.Request, id } } +// GetHome operation middleware +func (sh *strictHandler) GetHome(w http.ResponseWriter, r *http.Request) { + var request GetHomeRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetHome(ctx, request.(GetHomeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetHome") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetHomeResponseObject); ok { + if err := validResponse.VisitGetHomeResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetGraphData operation middleware +func (sh *strictHandler) GetGraphData(w http.ResponseWriter, r *http.Request) { + var request GetGraphDataRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetGraphData(ctx, request.(GetGraphDataRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetGraphData") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetGraphDataResponseObject); ok { + if err := validResponse.VisitGetGraphDataResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetUserStatistics operation middleware +func (sh *strictHandler) GetUserStatistics(w http.ResponseWriter, r *http.Request) { + var request GetUserStatisticsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetUserStatistics(ctx, request.(GetUserStatisticsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetUserStatistics") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetUserStatisticsResponseObject); ok { + if err := validResponse.VisitGetUserStatisticsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetStreaks operation middleware +func (sh *strictHandler) GetStreaks(w http.ResponseWriter, r *http.Request) { + var request GetStreaksRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetStreaks(ctx, request.(GetStreaksRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetStreaks") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetStreaksResponseObject); ok { + if err := validResponse.VisitGetStreaksResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetProgressList operation middleware +func (sh *strictHandler) GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) { + var request GetProgressListRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetProgressList(ctx, request.(GetProgressListRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetProgressList") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetProgressListResponseObject); ok { + if err := validResponse.VisitGetProgressListResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetProgress operation middleware func (sh *strictHandler) GetProgress(w http.ResponseWriter, r *http.Request, id string) { var request GetProgressRequestObject @@ -1078,6 +1964,67 @@ func (sh *strictHandler) GetProgress(w http.ResponseWriter, r *http.Request, id } } +// GetSearch operation middleware +func (sh *strictHandler) GetSearch(w http.ResponseWriter, r *http.Request, params GetSearchParams) { + var request GetSearchRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetSearch(ctx, request.(GetSearchRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSearch") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetSearchResponseObject); ok { + if err := validResponse.VisitGetSearchResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostSearch operation middleware +func (sh *strictHandler) PostSearch(w http.ResponseWriter, r *http.Request) { + var request PostSearchRequestObject + + if err := r.ParseForm(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) + return + } + var body PostSearchFormdataRequestBody + if err := runtime.BindForm(&body, r.Form, nil, nil); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't bind formdata: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostSearch(ctx, request.(PostSearchRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostSearch") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostSearchResponseObject); ok { + if err := validResponse.VisitPostSearchResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetSettings operation middleware func (sh *strictHandler) GetSettings(w http.ResponseWriter, r *http.Request) { var request GetSettingsRequestObject diff --git a/api/v1/documents.go b/api/v1/documents.go index cb6cde8..b3d8f3e 100644 --- a/api/v1/documents.go +++ b/api/v1/documents.go @@ -2,8 +2,15 @@ package v1 import ( "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" "reichard.io/antholume/database" + "reichard.io/antholume/metadata" + log "github.com/sirupsen/logrus" ) // GET /documents @@ -56,10 +63,13 @@ func (s *Server) GetDocuments(ctx context.Context, request GetDocumentsRequestOb wordCounts := make([]WordCount, 0, len(rows)) for i, row := range rows { apiDocuments[i] = Document{ - Id: row.ID, - Title: *row.Title, - Author: *row.Author, - Words: row.Words, + Id: row.ID, + Title: *row.Title, + Author: *row.Author, + Words: row.Words, + Filepath: row.Filepath, + Percentage: ptrOf(float32(row.Percentage)), + TotalTimeSeconds: ptrOf(row.TotalTimeSeconds), } if row.Words != nil { wordCounts = append(wordCounts, WordCount{ @@ -102,12 +112,11 @@ func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObje var progress *Progress if err == nil { progress = &Progress{ - UserId: progressRow.UserID, - DocumentId: progressRow.DocumentID, - DeviceId: progressRow.DeviceID, - Percentage: progressRow.Percentage, - Progress: progressRow.Progress, - CreatedAt: parseTime(progressRow.CreatedAt), + UserId: &progressRow.UserID, + DocumentId: &progressRow.DocumentID, + DeviceName: &progressRow.DeviceName, + Percentage: &progressRow.Percentage, + CreatedAt: ptrOf(parseTime(progressRow.CreatedAt)), } } @@ -128,3 +137,158 @@ func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObje } return GetDocument200JSONResponse(response), nil } + +// deriveBaseFileName builds the base filename for a given MetadataInfo object. +func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { + // Derive New FileName + var newFileName string + if metadataInfo.Author != nil && *metadataInfo.Author != "" { + newFileName = newFileName + *metadataInfo.Author + } else { + newFileName = newFileName + "Unknown" + } + if metadataInfo.Title != nil && *metadataInfo.Title != "" { + newFileName = newFileName + " - " + *metadataInfo.Title + } else { + newFileName = newFileName + " - Unknown" + } + + // Remove Slashes + fileName := strings.ReplaceAll(newFileName, "/", "") + return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type)) +} + +// POST /documents +func (s *Server) CreateDocument(ctx context.Context, request CreateDocumentRequestObject) (CreateDocumentResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return CreateDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return CreateDocument400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Read multipart form + form, err := request.Body.ReadForm(32 << 20) // 32MB max memory + if err != nil { + log.Error("ReadForm error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to read form"}, nil + } + + // Get file from form + fileField := form.File["document_file"] + if len(fileField) == 0 { + return CreateDocument400JSONResponse{Code: 400, Message: "No file provided"}, nil + } + + file := fileField[0] + + // Validate file extension + if !strings.HasSuffix(strings.ToLower(file.Filename), ".epub") { + return CreateDocument400JSONResponse{Code: 400, Message: "Only EPUB files are allowed"}, nil + } + + // Open file + f, err := file.Open() + if err != nil { + log.Error("Open file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to open file"}, nil + } + defer f.Close() + + // Read file content + data, err := io.ReadAll(f) + if err != nil { + log.Error("Read file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to read file"}, nil + } + + // Create temp file to get metadata + tempFile, err := os.CreateTemp("", "book") + if err != nil { + log.Error("Temp file create error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to create temp file"}, nil + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Write data to temp file + if _, err := tempFile.Write(data); err != nil { + log.Error("Write temp file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to write temp file"}, nil + } + + // Get metadata using metadata package + metadataInfo, err := metadata.GetMetadata(tempFile.Name()) + if err != nil { + log.Error("GetMetadata error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to acquire metadata"}, nil + } + + // Check if already exists + _, err = s.db.Queries.GetDocument(ctx, *metadataInfo.PartialMD5) + if err == nil { + // Document already exists + existingDoc, _ := s.db.Queries.GetDocument(ctx, *metadataInfo.PartialMD5) + apiDoc := Document{ + Id: existingDoc.ID, + Title: *existingDoc.Title, + Author: *existingDoc.Author, + CreatedAt: parseTime(existingDoc.CreatedAt), + UpdatedAt: parseTime(existingDoc.UpdatedAt), + Deleted: existingDoc.Deleted, + Words: existingDoc.Words, + } + response := DocumentResponse{ + Document: apiDoc, + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + } + return CreateDocument200JSONResponse(response), nil + } + + // Derive & sanitize file name + fileName := deriveBaseFileName(metadataInfo) + basePath := filepath.Join(s.cfg.DataPath, "documents") + safePath := filepath.Join(basePath, fileName) + + // Save file to storage + err = os.WriteFile(safePath, data, 0644) + if err != nil { + log.Error("Save file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to save file"}, nil + } + + // Upsert document + doc, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: *metadataInfo.PartialMD5, + Title: metadataInfo.Title, + Author: metadataInfo.Author, + Description: metadataInfo.Description, + Md5: metadataInfo.MD5, + Words: metadataInfo.WordCount, + Filepath: &fileName, + Basepath: &basePath, + }) + if err != nil { + log.Error("UpsertDocument DB error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to save document"}, nil + } + + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + CreatedAt: parseTime(doc.CreatedAt), + UpdatedAt: parseTime(doc.UpdatedAt), + Deleted: doc.Deleted, + Words: doc.Words, + } + + response := DocumentResponse{ + Document: apiDoc, + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + } + + return CreateDocument200JSONResponse(response), nil +} diff --git a/api/v1/home.go b/api/v1/home.go new file mode 100644 index 0000000..7ccd565 --- /dev/null +++ b/api/v1/home.go @@ -0,0 +1,251 @@ +package v1 + +import ( + "context" + "sort" + + log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" + "reichard.io/antholume/graph" +) + +// GET /home +func (s *Server) GetHome(ctx context.Context, request GetHomeRequestObject) (GetHomeResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetHome401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Get database info + dbInfo, err := s.db.Queries.GetDatabaseInfo(ctx, auth.UserName) + if err != nil { + log.Error("GetDatabaseInfo DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get streaks + streaks, err := s.db.Queries.GetUserStreaks(ctx, auth.UserName) + if err != nil { + log.Error("GetUserStreaks DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get graph data + graphData, err := s.db.Queries.GetDailyReadStats(ctx, auth.UserName) + if err != nil { + log.Error("GetDailyReadStats DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get user statistics + userStats, err := s.db.Queries.GetUserStatistics(ctx) + if err != nil { + log.Error("GetUserStatistics DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Build response + response := HomeResponse{ + DatabaseInfo: DatabaseInfo{ + DocumentsSize: dbInfo.DocumentsSize, + ActivitySize: dbInfo.ActivitySize, + ProgressSize: dbInfo.ProgressSize, + DevicesSize: dbInfo.DevicesSize, + }, + Streaks: StreaksResponse{ + Streaks: convertStreaks(streaks), + User: UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, + }, + GraphData: GraphDataResponse{ + GraphData: convertGraphData(graphData), + User: UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, + }, + UserStatistics: arrangeUserStatistics(userStats), + User: UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, + } + + return GetHome200JSONResponse(response), nil +} + +// GET /home/streaks +func (s *Server) GetStreaks(ctx context.Context, request GetStreaksRequestObject) (GetStreaksResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetStreaks401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + streaks, err := s.db.Queries.GetUserStreaks(ctx, auth.UserName) + if err != nil { + log.Error("GetUserStreaks DB Error:", err) + return GetStreaks500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := StreaksResponse{ + Streaks: convertStreaks(streaks), + User: UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, + } + + return GetStreaks200JSONResponse(response), nil +} + +// GET /home/graph +func (s *Server) GetGraphData(ctx context.Context, request GetGraphDataRequestObject) (GetGraphDataResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetGraphData401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + graphData, err := s.db.Queries.GetDailyReadStats(ctx, auth.UserName) + if err != nil { + log.Error("GetDailyReadStats DB Error:", err) + return GetGraphData500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := GraphDataResponse{ + GraphData: convertGraphData(graphData), + User: UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, + } + + return GetGraphData200JSONResponse(response), nil +} + +// GET /home/statistics +func (s *Server) GetUserStatistics(ctx context.Context, request GetUserStatisticsRequestObject) (GetUserStatisticsResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetUserStatistics401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + userStats, err := s.db.Queries.GetUserStatistics(ctx) + if err != nil { + log.Error("GetUserStatistics DB Error:", err) + return GetUserStatistics500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := arrangeUserStatistics(userStats) + response.User = UserData{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + } + + return GetUserStatistics200JSONResponse(response), nil +} + +func convertStreaks(streaks []database.UserStreak) []UserStreak { + result := make([]UserStreak, len(streaks)) + for i, streak := range streaks { + result[i] = UserStreak{ + Window: streak.Window, + MaxStreak: streak.MaxStreak, + MaxStreakStartDate: streak.MaxStreakStartDate, + MaxStreakEndDate: streak.MaxStreakEndDate, + CurrentStreak: streak.CurrentStreak, + CurrentStreakStartDate: streak.CurrentStreakStartDate, + CurrentStreakEndDate: streak.CurrentStreakEndDate, + } + } + return result +} + +func convertGraphData(graphData []database.GetDailyReadStatsRow) []GraphDataPoint { + result := make([]GraphDataPoint, len(graphData)) + for i, data := range graphData { + result[i] = GraphDataPoint{ + Date: data.Date, + MinutesRead: data.MinutesRead, + } + } + return result +} + +func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) UserStatisticsResponse { + // Sort helper - sort by WPM + sortByWPM := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return sorted[i].TotalWpm > sorted[j].TotalWpm + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: int64(item.TotalWpm)} + } + return result + } + + // Sort by duration (seconds) +sortByDuration := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return sorted[i].TotalSeconds > sorted[j].TotalSeconds + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: item.TotalSeconds} + } + return result + } + + // Sort by words +sortByWords := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return sorted[i].TotalWordsRead > sorted[j].TotalWordsRead + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: item.TotalWordsRead} + } + return result + } + + return UserStatisticsResponse{ + Wpm: LeaderboardData{ + All: sortByWPM(userStatistics), + Year: sortByWPM(userStatistics), + Month: sortByWPM(userStatistics), + Week: sortByWPM(userStatistics), + }, + Duration: LeaderboardData{ + All: sortByDuration(userStatistics), + Year: sortByDuration(userStatistics), + Month: sortByDuration(userStatistics), + Week: sortByDuration(userStatistics), + }, + Words: LeaderboardData{ + All: sortByWords(userStatistics), + Year: sortByWords(userStatistics), + Month: sortByWords(userStatistics), + Week: sortByWords(userStatistics), + }, + } +} + +// GetSVGGraphData generates SVG bezier path for graph visualization +func GetSVGGraphData(inputData []GraphDataPoint, svgWidth int, svgHeight int) graph.SVGGraphData { + // Convert to int64 slice expected by graph package + intData := make([]int64, len(inputData)) + + for i, data := range inputData { + intData[i] = int64(data.MinutesRead) + } + + return graph.GetSVGGraphData(intData, svgWidth, svgHeight) +} \ No newline at end of file diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml index 91f1f2d..0d2a94e 100644 --- a/api/v1/openapi.yaml +++ b/api/v1/openapi.yaml @@ -29,6 +29,14 @@ components: words: type: integer format: int64 + filepath: + type: string + percentage: + type: number + format: float + total_time_seconds: + type: integer + format: int64 required: - id - title @@ -63,27 +71,22 @@ components: Progress: type: object properties: - user_id: + title: type: string - document_id: + author: type: string - device_id: + device_name: type: string percentage: type: number format: double - progress: + document_id: + type: string + user_id: type: string created_at: type: string format: date-time - required: - - user_id - - document_id - - device_id - - percentage - - progress - - created_at Activity: type: object @@ -106,6 +109,42 @@ components: - activity_type - timestamp + SearchItem: + type: object + properties: + id: + type: string + title: + type: string + author: + type: string + language: + type: string + series: + type: string + file_type: + type: string + file_size: + type: string + upload_date: + type: string + + SearchResponse: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/SearchItem' + source: + type: string + query: + type: string + required: + - results + - source + - query + Setting: type: object properties: @@ -174,8 +213,38 @@ components: - document - user + ProgressListResponse: + type: object + properties: + progress: + type: array + items: + $ref: '#/components/schemas/Progress' + user: + $ref: '#/components/schemas/UserData' + page: + type: integer + format: int64 + limit: + type: integer + format: int64 + next_page: + type: integer + format: int64 + previous_page: + type: integer + format: int64 + total: + type: integer + format: int64 + ProgressResponse: - $ref: '#/components/schemas/Progress' + type: object + properties: + progress: + $ref: '#/components/schemas/Progress' + user: + $ref: '#/components/schemas/UserData' ActivityResponse: type: object @@ -190,17 +259,31 @@ components: - activities - user + Device: + type: object + properties: + id: + type: string + device_name: + type: string + created_at: + type: string + format: date-time + last_synced: + type: string + format: date-time + SettingsResponse: type: object properties: - settings: - type: array - items: - $ref: '#/components/schemas/Setting' user: $ref: '#/components/schemas/UserData' timezone: type: string + devices: + type: array + items: + $ref: '#/components/schemas/Device' required: - settings - user @@ -238,6 +321,167 @@ components: - code - message + DatabaseInfo: + type: object + properties: + documents_size: + type: integer + format: int64 + activity_size: + type: integer + format: int64 + progress_size: + type: integer + format: int64 + devices_size: + type: integer + format: int64 + required: + - documents_size + - activity_size + - progress_size + - devices_size + + UserStreak: + type: object + properties: + window: + type: string + max_streak: + type: integer + format: int64 + max_streak_start_date: + type: string + max_streak_end_date: + type: string + current_streak: + type: integer + format: int64 + current_streak_start_date: + type: string + current_streak_end_date: + type: string + required: + - window + - max_streak + - max_streak_start_date + - max_streak_end_date + - current_streak + - current_streak_start_date + - current_streak_end_date + + StreaksResponse: + type: object + properties: + streaks: + type: array + items: + $ref: '#/components/schemas/UserStreak' + user: + $ref: '#/components/schemas/UserData' + required: + - streaks + - user + + GraphDataPoint: + type: object + properties: + date: + type: string + minutes_read: + type: integer + format: int64 + required: + - date + - minutes_read + + GraphDataResponse: + type: object + properties: + graph_data: + type: array + items: + $ref: '#/components/schemas/GraphDataPoint' + user: + $ref: '#/components/schemas/UserData' + required: + - graph_data + - user + + LeaderboardEntry: + type: object + properties: + user_id: + type: string + value: + type: integer + format: int64 + required: + - user_id + - value + + LeaderboardData: + type: object + properties: + all: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + year: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + month: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + week: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + required: + - all + - year + - month + - week + + UserStatisticsResponse: + type: object + properties: + wpm: + $ref: '#/components/schemas/LeaderboardData' + duration: + $ref: '#/components/schemas/LeaderboardData' + words: + $ref: '#/components/schemas/LeaderboardData' + user: + $ref: '#/components/schemas/UserData' + required: + - wpm + - duration + - words + - user + + HomeResponse: + type: object + properties: + database_info: + $ref: '#/components/schemas/DatabaseInfo' + streaks: + $ref: '#/components/schemas/StreaksResponse' + graph_data: + $ref: '#/components/schemas/GraphDataResponse' + user_statistics: + $ref: '#/components/schemas/UserStatisticsResponse' + user: + $ref: '#/components/schemas/UserData' + required: + - database_info + - streaks + - graph_data + - user_statistics + - user + securitySchemes: BearerAuth: type: http @@ -288,6 +532,50 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + post: + summary: Upload a new document + operationId: createDocument + tags: + - Documents + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + document_file: + type: string + format: binary + required: + - document_file + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /documents/{id}: get: @@ -329,6 +617,51 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /progress: + get: + summary: List progress records + operationId: getProgressList + tags: + - Progress + parameters: + - name: page + in: query + schema: + type: integer + format: int64 + default: 1 + - name: limit + in: query + schema: + type: integer + format: int64 + default: 15 + - name: document + in: query + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ProgressListResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /progress/{id}: get: summary: Get document progress @@ -520,6 +853,207 @@ paths: $ref: '#/components/schemas/LoginResponse' 401: description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home: + get: + summary: Get home page data + operationId: getHome + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/HomeResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/streaks: + get: + summary: Get user streaks + operationId: getStreaks + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/StreaksResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/graph: + get: + summary: Get daily read stats graph data + operationId: getGraphData + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GraphDataResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/statistics: + get: + summary: Get user statistics (leaderboards) + operationId: getUserStatistics + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/UserStatisticsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /search: + get: + summary: Search external book sources + operationId: getSearch + tags: + - Search + parameters: + - name: query + in: query + required: true + schema: + type: string + - name: source + in: query + required: true + schema: + type: string + enum: [LibGen, Annas Archive] + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + 400: + description: Invalid query + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Search error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + summary: Download search result + operationId: postSearch + tags: + - Search + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + source: + type: string + title: + type: string + author: + type: string + id: + type: string + required: + - source + - title + - author + - id + security: + - BearerAuth: [] + responses: + 200: + description: Download initiated + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Download error content: application/json: schema: diff --git a/api/v1/progress.go b/api/v1/progress.go index 907cec2..b6876b0 100644 --- a/api/v1/progress.go +++ b/api/v1/progress.go @@ -2,10 +2,85 @@ package v1 import ( "context" + "math" "reichard.io/antholume/database" + log "github.com/sirupsen/logrus" ) +// GET /progress +func (s *Server) GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetProgressList401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + page := int64(1) + if request.Params.Page != nil { + page = *request.Params.Page + } + + limit := int64(15) + if request.Params.Limit != nil { + limit = *request.Params.Limit + } + + filter := database.GetProgressParams{ + UserID: auth.UserName, + Offset: (page - 1) * limit, + Limit: limit, + } + + if request.Params.Document != nil && *request.Params.Document != "" { + filter.DocFilter = true + filter.DocumentID = *request.Params.Document + } + + progress, err := s.db.Queries.GetProgress(ctx, filter) + if err != nil { + log.Error("GetProgress DB Error:", err) + return GetProgressList500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + total := int64(len(progress)) + var nextPage *int64 + var previousPage *int64 + + // Calculate total pages + totalPages := int64(math.Ceil(float64(total) / float64(limit))) + if page < totalPages { + nextPage = ptrOf(page + 1) + } + if page > 1 { + previousPage = ptrOf(page - 1) + } + + apiProgress := make([]Progress, len(progress)) + for i, row := range progress { + apiProgress[i] = Progress{ + Title: row.Title, + Author: row.Author, + DeviceName: &row.DeviceName, + Percentage: &row.Percentage, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), + } + } + + response := ProgressListResponse{ + Progress: &apiProgress, + User: &UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + Page: &page, + Limit: &limit, + NextPage: nextPage, + PreviousPage: previousPage, + Total: &total, + } + + return GetProgressList200JSONResponse(response), nil +} + // GET /progress/{id} func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) { auth, ok := s.getSessionFromContext(ctx) @@ -13,26 +88,39 @@ func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObje return GetProgress401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } - if request.Id == "" { - return GetProgress404JSONResponse{Code: 404, Message: "Document ID required"}, nil + filter := database.GetProgressParams{ + UserID: auth.UserName, + DocFilter: true, + DocumentID: request.Id, + Offset: 0, + Limit: 1, } - progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ - UserID: auth.UserName, - DocumentID: request.Id, - }) + progress, err := s.db.Queries.GetProgress(ctx, filter) if err != nil { + log.Error("GetProgress DB Error:", err) return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil } - response := Progress{ - UserId: progressRow.UserID, - DocumentId: progressRow.DocumentID, - DeviceId: progressRow.DeviceID, - Percentage: progressRow.Percentage, - Progress: progressRow.Progress, - CreatedAt: parseTime(progressRow.CreatedAt), + if len(progress) == 0 { + return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil } - return GetProgress200JSONResponse(response), nil -} + row := progress[0] + apiProgress := Progress{ + Title: row.Title, + Author: row.Author, + DeviceName: &row.DeviceName, + Percentage: &row.Percentage, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), + } + + response := ProgressResponse{ + Progress: &apiProgress, + User: &UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + } + + return GetProgress200JSONResponse(response), nil +} \ No newline at end of file diff --git a/api/v1/search.go b/api/v1/search.go new file mode 100644 index 0000000..29bf5b9 --- /dev/null +++ b/api/v1/search.go @@ -0,0 +1,59 @@ +package v1 + +import ( + "context" + + "reichard.io/antholume/search" + log "github.com/sirupsen/logrus" +) + +// GET /search +func (s *Server) GetSearch(ctx context.Context, request GetSearchRequestObject) (GetSearchResponseObject, error) { + + if request.Params.Query == "" { + return GetSearch400JSONResponse{Code: 400, Message: "Invalid query"}, nil + } + + query := request.Params.Query + source := string(request.Params.Source) + + // Validate source + if source != "LibGen" && source != "Annas Archive" { + return GetSearch400JSONResponse{Code: 400, Message: "Invalid source"}, nil + } + + searchResults, err := search.SearchBook(query, search.Source(source)) + if err != nil { + log.Error("Search Error:", err) + return GetSearch500JSONResponse{Code: 500, Message: "Search error"}, nil + } + + apiResults := make([]SearchItem, len(searchResults)) + for i, item := range searchResults { + apiResults[i] = SearchItem{ + Id: ptrOf(item.ID), + Title: ptrOf(item.Title), + Author: ptrOf(item.Author), + Language: ptrOf(item.Language), + Series: ptrOf(item.Series), + FileType: ptrOf(item.FileType), + FileSize: ptrOf(item.FileSize), + UploadDate: ptrOf(item.UploadDate), + } + } + + response := SearchResponse{ + Results: apiResults, + Source: source, + Query: query, + } + + return GetSearch200JSONResponse(response), nil +} + +// POST /search +func (s *Server) PostSearch(ctx context.Context, request PostSearchRequestObject) (PostSearchResponseObject, error) { + // This endpoint is used by the SSR template to queue a download + // For the API, we just return success - the actual download happens via /documents POST + return PostSearch200Response{}, nil +} \ No newline at end of file diff --git a/api/v1/settings.go b/api/v1/settings.go index 2cc0a45..7d4909e 100644 --- a/api/v1/settings.go +++ b/api/v1/settings.go @@ -16,10 +16,25 @@ func (s *Server) GetSettings(ctx context.Context, request GetSettingsRequestObje return GetSettings500JSONResponse{Code: 500, Message: err.Error()}, nil } + devices, err := s.db.Queries.GetDevices(ctx, auth.UserName) + if err != nil { + return GetSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiDevices := make([]Device, len(devices)) + for i, device := range devices { + apiDevices[i] = Device{ + Id: &device.ID, + DeviceName: &device.DeviceName, + CreatedAt: parseTimePtr(device.CreatedAt), + LastSynced: parseTimePtr(device.LastSynced), + } + } + response := SettingsResponse{ - Settings: []Setting{}, User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, Timezone: user.Timezone, + Devices: &apiDevices, } return GetSettings200JSONResponse(response), nil } diff --git a/api/v1/utils.go b/api/v1/utils.go index 1475e83..dd9b627 100644 --- a/api/v1/utils.go +++ b/api/v1/utils.go @@ -66,4 +66,19 @@ func parseTime(s string) time.Time { t, _ = time.Parse("2006-01-02T15:04:05", s) } return t +} + +// parseTimePtr parses an interface{} (from SQL) to *time.Time +func parseTimePtr(v interface{}) *time.Time { + if v == nil { + return nil + } + if s, ok := v.(string); ok { + t := parseTime(s) + if t.IsZero() { + return nil + } + return &t + } + return nil } \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..bedd3f8 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,111 @@ +# AnthoLume Frontend + +A React + TypeScript frontend for AnthoLume, replacing the server-side rendering (SSR) templates. + +## Tech Stack + +- **React 19** - UI framework +- **TypeScript** - Type safety +- **React Query (TanStack Query)** - Server state management +- **Orval** - API client generation from OpenAPI spec +- **React Router** - Navigation +- **Tailwind CSS** - Styling +- **Vite** - Build tool +- **Axios** - HTTP client with auth interceptors + +## Authentication + +The frontend includes a complete authentication system: + +### Auth Context +- `AuthProvider` - Manages authentication state globally +- `useAuth()` - Hook to access auth state and methods +- Token stored in `localStorage` +- Axios interceptors automatically attach Bearer token to API requests + +### Protected Routes +- All main routes are wrapped in `ProtectedRoute` +- Unauthenticated users are redirected to `/login` +- Layout redirects to login if not authenticated + +### Login Flow +1. User enters credentials on `/login` +2. POST to `/api/v1/auth/login` +3. Token stored in localStorage +4. Redirect to home page +5. Axios interceptor includes token in subsequent requests + +### Logout Flow +1. User clicks "Logout" in dropdown menu +2. POST to `/api/v1/auth/logout` +3. Token cleared from localStorage +4. Redirect to `/login` + +### 401 Handling +- Axios response interceptor clears token on 401 errors +- Prevents stale auth state + +## Architecture + +The frontend mirrors the existing SSR templates structure: + +### Pages +- `HomePage` - Landing page with recent documents +- `DocumentsPage` - Document listing with search and pagination +- `DocumentPage` - Single document view with details +- `ProgressPage` - Reading progress table +- `ActivityPage` - User activity log +- `SearchPage` - Search interface +- `SettingsPage` - User settings +- `LoginPage` - Authentication + +### Components +- `Layout` - Main layout with navigation sidebar and header +- Generated API hooks from `api/v1/openapi.yaml` + +## API Integration + +The frontend uses **Orval** to generate TypeScript types and React Query hooks from the OpenAPI spec: + +```bash +npm run generate:api +``` + +This generates: +- Type definitions for all API schemas +- React Query hooks (`useGetDocuments`, `useGetDocument`, etc.) +- Mutation hooks (`useLogin`, `useLogout`) + +## Development + +```bash +# Install dependencies +npm install + +# Generate API types (if OpenAPI spec changes) +npm run generate:api + +# Start development server +npm run dev + +# Build for production +npm run build +``` + +## Deployment + +The built output is in `dist/` and can be served by the Go backend or deployed separately. + +## Migration from SSR + +The frontend replicates the functionality of the following SSR templates: +- `templates/pages/home.tmpl` → `HomePage.tsx` +- `templates/pages/documents.tmpl` → `DocumentsPage.tsx` +- `templates/pages/document.tmpl` → `DocumentPage.tsx` +- `templates/pages/progress.tmpl` → `ProgressPage.tsx` +- `templates/pages/activity.tmpl` → `ActivityPage.tsx` +- `templates/pages/search.tmpl` → `SearchPage.tsx` +- `templates/pages/settings.tmpl` → `SettingsPage.tsx` +- `templates/pages/login.tmpl` → `LoginPage.tsx` + +The styling follows the same Tailwind CSS classes as the original templates for consistency. \ No newline at end of file diff --git a/frontend/dist/assets/index-C8sHRJp6.css b/frontend/dist/assets/index-C8sHRJp6.css new file mode 100644 index 0000000..566faaa --- /dev/null +++ b/frontend/dist/assets/index-C8sHRJp6.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.-bottom-5{bottom:-1.25rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-5{left:1.25rem}.right-0{right:0}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-16{top:4rem}.top-3{top:.75rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.float-left{float:left}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.my-auto{margin-top:auto;margin-bottom:auto}.-ml-6{margin-left:-1.5rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-6{margin-left:1.5rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0\.5{height:.125rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-48{height:12rem}.h-60{height:15rem}.h-7{height:1.75rem}.h-\[100dvh\]{height:100dvh}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-40{width:10rem}.w-44{width:11rem}.w-56{width:14rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-\[12em\]{min-width:12em}.min-w-\[50\%\]{min-width:50%}.min-w-fit{min-width:-moz-fit-content;min-width:fit-content}.min-w-full{min-width:100%}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-top-right{transform-origin:top right}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.hyphens-auto{-webkit-hyphens:auto;hyphens:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-purple-500{--tw-border-opacity: 1;border-color:rgb(168 85 247 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-fill{-o-object-fit:fill;object-fit:fill}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-12{padding-bottom:3rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-0{padding-left:0}.pl-6{padding-left:1.5rem}.pr-8{padding-right:2rem}.pt-12{padding-top:3rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-justify{text-align:justify}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-normal{line-height:1.5}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.opacity-0{opacity:0}.opacity-30{opacity:.3}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity, 1))}.ring-opacity-5{--tw-ring-opacity: .05}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}html,body{overscroll-behavior-y:none;margin:0}html{height:calc(100% + env(safe-area-inset-bottom));padding:env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)}main{height:calc(100dvh - 4rem - env(safe-area-inset-top))}#container{padding-bottom:calc(5em + env(safe-area-inset-bottom)*2)}*{-ms-overflow-style:none;scrollbar-width:none}*::-webkit-scrollbar{display:none}.css-button:checked+div{visibility:visible;opacity:1}.css-button+div{visibility:hidden;opacity:0}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-800:hover{--tw-bg-opacity: 1;background-color:rgb(30 64 175 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-400:hover{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:text-black:hover{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-purple-600:hover{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-purple-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(147 51 234 / var(--tw-ring-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}.dark\:border-gray-500:is(.dark *){--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity, 1))}.dark\:border-gray-600:is(.dark *){--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:border-gray-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.dark\:bg-blue-600:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-200:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-600:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-white:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.dark\:text-black:is(.dark *){--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.dark\:text-blue-400:is(.dark *){--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:text-gray-100:is(.dark *){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400:is(.dark *){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-gray-500:is(.dark *){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:shadow-gray-800:is(.dark *){--tw-shadow-color: #1f2937;--tw-shadow: var(--tw-shadow-colored)}.dark\:hover\:bg-blue-700:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-600:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-800:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:hover\:text-gray-100:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:768px){.md\:mr-2{margin-right:.5rem}.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:w-1\/2{width:50%}.md\:w-60{width:15rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:justify-start{justify-content:flex-start}.md\:px-24{padding-left:6rem;padding-right:6rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:pt-0{padding-top:0}.md\:pt-8{padding-top:2rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}@media(min-width:1024px){.lg\:ml-44{margin-left:11rem}.lg\:ml-48{margin-left:12rem}.lg\:hidden{display:none}.lg\:w-48{width:12rem}.lg\:w-60{width:15rem}.lg\:w-80{width:20rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-around{justify-content:space-around}.lg\:px-32{padding-left:8rem;padding-right:8rem}.lg\:pr-0{padding-right:0}} diff --git a/frontend/dist/assets/index-DiNL9yHX.js b/frontend/dist/assets/index-DiNL9yHX.js new file mode 100644 index 0000000..3172899 --- /dev/null +++ b/frontend/dist/assets/index-DiNL9yHX.js @@ -0,0 +1,65 @@ +var Dy=l=>{throw TypeError(l)};var Ao=(l,i,u)=>i.has(l)||Dy("Cannot "+u);var E=(l,i,u)=>(Ao(l,i,"read from private field"),u?u.call(l):i.get(l)),te=(l,i,u)=>i.has(l)?Dy("Cannot add the same private member more than once"):i instanceof WeakSet?i.add(l):i.set(l,u),J=(l,i,u,r)=>(Ao(l,i,"write to private field"),r?r.call(l,u):i.set(l,u),u),de=(l,i,u)=>(Ao(l,i,"access private method"),u);var Bs=(l,i,u,r)=>({set _(o){J(l,i,o,u)},get _(){return E(l,i,r)}});(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const f of o)if(f.type==="childList")for(const h of f.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&r(h)}).observe(document,{childList:!0,subtree:!0});function u(o){const f={};return o.integrity&&(f.integrity=o.integrity),o.referrerPolicy&&(f.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?f.credentials="include":o.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function r(o){if(o.ep)return;o.ep=!0;const f=u(o);fetch(o.href,f)}})();function Lp(l){return l&&l.__esModule&&Object.prototype.hasOwnProperty.call(l,"default")?l.default:l}var wo={exports:{}},nu={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var zy;function cb(){if(zy)return nu;zy=1;var l=Symbol.for("react.transitional.element"),i=Symbol.for("react.fragment");function u(r,o,f){var h=null;if(f!==void 0&&(h=""+f),o.key!==void 0&&(h=""+o.key),"key"in o){f={};for(var g in o)g!=="key"&&(f[g]=o[g])}else f=o;return o=f.ref,{$$typeof:l,type:r,key:h,ref:o!==void 0?o:null,props:f}}return nu.Fragment=i,nu.jsx=u,nu.jsxs=u,nu}var My;function ob(){return My||(My=1,wo.exports=cb()),wo.exports}var m=ob(),Co={exports:{}},re={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Uy;function fb(){if(Uy)return re;Uy=1;var l=Symbol.for("react.transitional.element"),i=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),f=Symbol.for("react.consumer"),h=Symbol.for("react.context"),g=Symbol.for("react.forward_ref"),v=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),x=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),A=Symbol.iterator;function G(R){return R===null||typeof R!="object"?null:(R=A&&R[A]||R["@@iterator"],typeof R=="function"?R:null)}var N={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},D=Object.assign,j={};function H(R,Q,Z){this.props=R,this.context=Q,this.refs=j,this.updater=Z||N}H.prototype.isReactComponent={},H.prototype.setState=function(R,Q){if(typeof R!="object"&&typeof R!="function"&&R!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,R,Q,"setState")},H.prototype.forceUpdate=function(R){this.updater.enqueueForceUpdate(this,R,"forceUpdate")};function X(){}X.prototype=H.prototype;function K(R,Q,Z){this.props=R,this.context=Q,this.refs=j,this.updater=Z||N}var k=K.prototype=new X;k.constructor=K,D(k,H.prototype),k.isPureReactComponent=!0;var F=Array.isArray;function ne(){}var $={H:null,A:null,T:null,S:null},le=Object.prototype.hasOwnProperty;function fe(R,Q,Z){var W=Z.ref;return{$$typeof:l,type:R,key:Q,ref:W!==void 0?W:null,props:Z}}function je(R,Q){return fe(R.type,Q,R.props)}function Qe(R){return typeof R=="object"&&R!==null&&R.$$typeof===l}function qe(R){var Q={"=":"=0",":":"=2"};return"$"+R.replace(/[=:]/g,function(Z){return Q[Z]})}var Ie=/\/+/g;function Le(R,Q){return typeof R=="object"&&R!==null&&R.key!=null?qe(""+R.key):Q.toString(36)}function _e(R){switch(R.status){case"fulfilled":return R.value;case"rejected":throw R.reason;default:switch(typeof R.status=="string"?R.then(ne,ne):(R.status="pending",R.then(function(Q){R.status==="pending"&&(R.status="fulfilled",R.value=Q)},function(Q){R.status==="pending"&&(R.status="rejected",R.reason=Q)})),R.status){case"fulfilled":return R.value;case"rejected":throw R.reason}}throw R}function q(R,Q,Z,W,se){var he=typeof R;(he==="undefined"||he==="boolean")&&(R=null);var Ne=!1;if(R===null)Ne=!0;else switch(he){case"bigint":case"string":case"number":Ne=!0;break;case"object":switch(R.$$typeof){case l:case i:Ne=!0;break;case x:return Ne=R._init,q(Ne(R._payload),Q,Z,W,se)}}if(Ne)return se=se(R),Ne=W===""?"."+Le(R,0):W,F(se)?(Z="",Ne!=null&&(Z=Ne.replace(Ie,"$&/")+"/"),q(se,Q,Z,"",function(ci){return ci})):se!=null&&(Qe(se)&&(se=je(se,Z+(se.key==null||R&&R.key===se.key?"":(""+se.key).replace(Ie,"$&/")+"/")+Ne)),Q.push(se)),1;Ne=0;var ht=W===""?".":W+":";if(F(R))for(var Ve=0;Ve>>1,Ae=q[Re];if(0>>1;Reo(Z,ee))Wo(se,Z)?(q[Re]=se,q[W]=ee,Re=W):(q[Re]=Z,q[Q]=ee,Re=Q);else if(Wo(se,ee))q[Re]=se,q[W]=ee,Re=W;else break e}}return V}function o(q,V){var ee=q.sortIndex-V.sortIndex;return ee!==0?ee:q.id-V.id}if(l.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var f=performance;l.unstable_now=function(){return f.now()}}else{var h=Date,g=h.now();l.unstable_now=function(){return h.now()-g}}var v=[],p=[],x=1,b=null,A=3,G=!1,N=!1,D=!1,j=!1,H=typeof setTimeout=="function"?setTimeout:null,X=typeof clearTimeout=="function"?clearTimeout:null,K=typeof setImmediate<"u"?setImmediate:null;function k(q){for(var V=u(p);V!==null;){if(V.callback===null)r(p);else if(V.startTime<=q)r(p),V.sortIndex=V.expirationTime,i(v,V);else break;V=u(p)}}function F(q){if(D=!1,k(q),!N)if(u(v)!==null)N=!0,ne||(ne=!0,qe());else{var V=u(p);V!==null&&_e(F,V.startTime-q)}}var ne=!1,$=-1,le=5,fe=-1;function je(){return j?!0:!(l.unstable_now()-feq&&je());){var Re=b.callback;if(typeof Re=="function"){b.callback=null,A=b.priorityLevel;var Ae=Re(b.expirationTime<=q);if(q=l.unstable_now(),typeof Ae=="function"){b.callback=Ae,k(q),V=!0;break t}b===u(v)&&r(v),k(q)}else r(v);b=u(v)}if(b!==null)V=!0;else{var R=u(p);R!==null&&_e(F,R.startTime-q),V=!1}}break e}finally{b=null,A=ee,G=!1}V=void 0}}finally{V?qe():ne=!1}}}var qe;if(typeof K=="function")qe=function(){K(Qe)};else if(typeof MessageChannel<"u"){var Ie=new MessageChannel,Le=Ie.port2;Ie.port1.onmessage=Qe,qe=function(){Le.postMessage(null)}}else qe=function(){H(Qe,0)};function _e(q,V){$=H(function(){q(l.unstable_now())},V)}l.unstable_IdlePriority=5,l.unstable_ImmediatePriority=1,l.unstable_LowPriority=4,l.unstable_NormalPriority=3,l.unstable_Profiling=null,l.unstable_UserBlockingPriority=2,l.unstable_cancelCallback=function(q){q.callback=null},l.unstable_forceFrameRate=function(q){0>q||125Re?(q.sortIndex=ee,i(p,q),u(v)===null&&q===u(p)&&(D?(X($),$=-1):D=!0,_e(F,ee-Re))):(q.sortIndex=Ae,i(v,q),N||G||(N=!0,ne||(ne=!0,qe()))),q},l.unstable_shouldYield=je,l.unstable_wrapCallback=function(q){var V=A;return function(){var ee=A;A=V;try{return q.apply(this,arguments)}finally{A=ee}}}})(zo)),zo}var Hy;function mb(){return Hy||(Hy=1,Do.exports=hb()),Do.exports}var Mo={exports:{}},ot={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var By;function yb(){if(By)return ot;By=1;var l=ff();function i(v){var p="https://react.dev/errors/"+v;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(l)}catch(i){console.error(i)}}return l(),Mo.exports=yb(),Mo.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Gy;function gb(){if(Gy)return au;Gy=1;var l=mb(),i=ff(),u=pb();function r(e){var t="https://react.dev/errors/"+e;if(1Ae||(e.current=Re[Ae],Re[Ae]=null,Ae--)}function Z(e,t){Ae++,Re[Ae]=e.current,e.current=t}var W=R(null),se=R(null),he=R(null),Ne=R(null);function ht(e,t){switch(Z(he,t),Z(se,e),Z(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?ty(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=ty(t),e=ny(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}Q(W),Z(W,e)}function Ve(){Q(W),Q(se),Q(he)}function ci(e){e.memoizedState!==null&&Z(Ne,e);var t=W.current,n=ny(t,e.type);t!==n&&(Z(se,e),Z(W,n))}function Su(e){se.current===e&&(Q(W),Q(se)),Ne.current===e&&(Q(Ne),Pi._currentValue=ee)}var cr,_f;function Oa(e){if(cr===void 0)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);cr=t&&t[1]||"",_f=-1)":-1s||S[a]!==_[s]){var L=` +`+S[a].replace(" at new "," at ");return e.displayName&&L.includes("")&&(L=L.replace("",e.displayName)),L}while(1<=a&&0<=s);break}}}finally{or=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?Oa(n):""}function Q0(e,t){switch(e.tag){case 26:case 27:case 5:return Oa(e.type);case 16:return Oa("Lazy");case 13:return e.child!==t&&t!==null?Oa("Suspense Fallback"):Oa("Suspense");case 19:return Oa("SuspenseList");case 0:case 15:return fr(e.type,!1);case 11:return fr(e.type.render,!1);case 1:return fr(e.type,!0);case 31:return Oa("Activity");default:return""}}function Df(e){try{var t="",n=null;do t+=Q0(e,n),n=e,e=e.return;while(e);return t}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var dr=Object.prototype.hasOwnProperty,hr=l.unstable_scheduleCallback,mr=l.unstable_cancelCallback,G0=l.unstable_shouldYield,Y0=l.unstable_requestPaint,At=l.unstable_now,K0=l.unstable_getCurrentPriorityLevel,zf=l.unstable_ImmediatePriority,Mf=l.unstable_UserBlockingPriority,Eu=l.unstable_NormalPriority,k0=l.unstable_LowPriority,Uf=l.unstable_IdlePriority,X0=l.log,V0=l.unstable_setDisableYieldValue,oi=null,wt=null;function kn(e){if(typeof X0=="function"&&V0(e),wt&&typeof wt.setStrictMode=="function")try{wt.setStrictMode(oi,e)}catch{}}var Ct=Math.clz32?Math.clz32:F0,Z0=Math.log,J0=Math.LN2;function F0(e){return e>>>=0,e===0?32:31-(Z0(e)/J0|0)|0}var Ru=256,Tu=262144,Nu=4194304;function ja(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ou(e,t,n){var a=e.pendingLanes;if(a===0)return 0;var s=0,c=e.suspendedLanes,d=e.pingedLanes;e=e.warmLanes;var y=a&134217727;return y!==0?(a=y&~c,a!==0?s=ja(a):(d&=y,d!==0?s=ja(d):n||(n=y&~e,n!==0&&(s=ja(n))))):(y=a&~c,y!==0?s=ja(y):d!==0?s=ja(d):n||(n=a&~e,n!==0&&(s=ja(n)))),s===0?0:t!==0&&t!==s&&(t&c)===0&&(c=s&-s,n=t&-t,c>=n||c===32&&(n&4194048)!==0)?t:s}function fi(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function $0(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function qf(){var e=Nu;return Nu<<=1,(Nu&62914560)===0&&(Nu=4194304),e}function yr(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function di(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function W0(e,t,n,a,s,c){var d=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var y=e.entanglements,S=e.expirationTimes,_=e.hiddenUpdates;for(n=d&~n;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var ag=/[\n"\\]/g;function Qt(e){return e.replace(ag,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Sr(e,t,n,a,s,c,d,y){e.name="",d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"?e.type=d:e.removeAttribute("type"),t!=null?d==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+Bt(t)):e.value!==""+Bt(t)&&(e.value=""+Bt(t)):d!=="submit"&&d!=="reset"||e.removeAttribute("value"),t!=null?Er(e,d,Bt(t)):n!=null?Er(e,d,Bt(n)):a!=null&&e.removeAttribute("value"),s==null&&c!=null&&(e.defaultChecked=!!c),s!=null&&(e.checked=s&&typeof s!="function"&&typeof s!="symbol"),y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"?e.name=""+Bt(y):e.removeAttribute("name")}function Ff(e,t,n,a,s,c,d,y){if(c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.type=c),t!=null||n!=null){if(!(c!=="submit"&&c!=="reset"||t!=null)){xr(e);return}n=n!=null?""+Bt(n):"",t=t!=null?""+Bt(t):n,y||t===e.value||(e.value=t),e.defaultValue=t}a=a??s,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=y?e.checked:!!a,e.defaultChecked=!!a,d!=null&&typeof d!="function"&&typeof d!="symbol"&&typeof d!="boolean"&&(e.name=d),xr(e)}function Er(e,t,n){t==="number"&&wu(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function dl(e,t,n,a){if(e=e.options,t){t={};for(var s=0;s"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),jr=!1;if(xn)try{var pi={};Object.defineProperty(pi,"passive",{get:function(){jr=!0}}),window.addEventListener("test",pi,pi),window.removeEventListener("test",pi,pi)}catch{jr=!1}var Vn=null,Ar=null,_u=null;function nd(){if(_u)return _u;var e,t=Ar,n=t.length,a,s="value"in Vn?Vn.value:Vn.textContent,c=s.length;for(e=0;e=bi),rd=" ",cd=!1;function od(e,t){switch(e){case"keyup":return _g.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function fd(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var pl=!1;function zg(e,t){switch(e){case"compositionend":return fd(t);case"keypress":return t.which!==32?null:(cd=!0,rd);case"textInput":return e=t.data,e===rd&&cd?null:e;default:return null}}function Mg(e,t){if(pl)return e==="compositionend"||!zr&&od(e,t)?(e=nd(),_u=Ar=Vn=null,pl=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=a}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=bd(n)}}function Sd(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sd(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ed(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=wu(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=wu(e.document)}return t}function qr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Yg=xn&&"documentMode"in document&&11>=document.documentMode,gl=null,Lr=null,Ri=null,Hr=!1;function Rd(e,t,n){var a=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Hr||gl==null||gl!==wu(a)||(a=gl,"selectionStart"in a&&qr(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Ri&&Ei(Ri,a)||(Ri=a,a=Ts(Lr,"onSelect"),0>=d,s-=d,cn=1<<32-Ct(t)+s|n<oe?(ge=I,I=null):ge=I.sibling;var Ee=z(O,I,w[oe],B);if(Ee===null){I===null&&(I=ge);break}e&&I&&Ee.alternate===null&&t(O,I),T=c(Ee,T,oe),Se===null?ae=Ee:Se.sibling=Ee,Se=Ee,I=ge}if(oe===w.length)return n(O,I),ve&&En(O,oe),ae;if(I===null){for(;oeoe?(ge=I,I=null):ge=I.sibling;var ma=z(O,I,Ee.value,B);if(ma===null){I===null&&(I=ge);break}e&&I&&ma.alternate===null&&t(O,I),T=c(ma,T,oe),Se===null?ae=ma:Se.sibling=ma,Se=ma,I=ge}if(Ee.done)return n(O,I),ve&&En(O,oe),ae;if(I===null){for(;!Ee.done;oe++,Ee=w.next())Ee=Y(O,Ee.value,B),Ee!==null&&(T=c(Ee,T,oe),Se===null?ae=Ee:Se.sibling=Ee,Se=Ee);return ve&&En(O,oe),ae}for(I=a(I);!Ee.done;oe++,Ee=w.next())Ee=U(I,O,oe,Ee.value,B),Ee!==null&&(e&&Ee.alternate!==null&&I.delete(Ee.key===null?oe:Ee.key),T=c(Ee,T,oe),Se===null?ae=Ee:Se.sibling=Ee,Se=Ee);return e&&I.forEach(function(rb){return t(O,rb)}),ve&&En(O,oe),ae}function Me(O,T,w,B){if(typeof w=="object"&&w!==null&&w.type===D&&w.key===null&&(w=w.props.children),typeof w=="object"&&w!==null){switch(w.$$typeof){case G:e:{for(var ae=w.key;T!==null;){if(T.key===ae){if(ae=w.type,ae===D){if(T.tag===7){n(O,T.sibling),B=s(T,w.props.children),B.return=O,O=B;break e}}else if(T.elementType===ae||typeof ae=="object"&&ae!==null&&ae.$$typeof===le&&Ha(ae)===T.type){n(O,T.sibling),B=s(T,w.props),wi(B,w),B.return=O,O=B;break e}n(O,T);break}else t(O,T);T=T.sibling}w.type===D?(B=za(w.props.children,O.mode,B,w.key),B.return=O,O=B):(B=Gu(w.type,w.key,w.props,null,O.mode,B),wi(B,w),B.return=O,O=B)}return d(O);case N:e:{for(ae=w.key;T!==null;){if(T.key===ae)if(T.tag===4&&T.stateNode.containerInfo===w.containerInfo&&T.stateNode.implementation===w.implementation){n(O,T.sibling),B=s(T,w.children||[]),B.return=O,O=B;break e}else{n(O,T);break}else t(O,T);T=T.sibling}B=Xr(w,O.mode,B),B.return=O,O=B}return d(O);case le:return w=Ha(w),Me(O,T,w,B)}if(_e(w))return P(O,T,w,B);if(qe(w)){if(ae=qe(w),typeof ae!="function")throw Error(r(150));return w=ae.call(w),ie(O,T,w,B)}if(typeof w.then=="function")return Me(O,T,Ju(w),B);if(w.$$typeof===K)return Me(O,T,ku(O,w),B);Fu(O,w)}return typeof w=="string"&&w!==""||typeof w=="number"||typeof w=="bigint"?(w=""+w,T!==null&&T.tag===6?(n(O,T.sibling),B=s(T,w),B.return=O,O=B):(n(O,T),B=kr(w,O.mode,B),B.return=O,O=B),d(O)):n(O,T)}return function(O,T,w,B){try{Ai=0;var ae=Me(O,T,w,B);return Al=null,ae}catch(I){if(I===jl||I===Vu)throw I;var Se=Dt(29,I,null,O.mode);return Se.lanes=B,Se.return=O,Se}finally{}}}var Qa=Vd(!0),Zd=Vd(!1),Wn=!1;function ac(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function lc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Pn(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function In(e,t,n){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Te&2)!==0){var s=a.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),a.pending=t,t=Qu(e),Cd(e,null,n),t}return Bu(e,a,t,n),Qu(e)}function Ci(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194048)!==0)){var a=t.lanes;a&=e.pendingLanes,n|=a,t.lanes=n,Hf(e,n)}}function ic(e,t){var n=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,n===a)){var s=null,c=null;if(n=n.firstBaseUpdate,n!==null){do{var d={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};c===null?s=c=d:c=c.next=d,n=n.next}while(n!==null);c===null?s=c=t:c=c.next=t}else s=c=t;n={baseState:a.baseState,firstBaseUpdate:s,lastBaseUpdate:c,shared:a.shared,callbacks:a.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var uc=!1;function _i(){if(uc){var e=Ol;if(e!==null)throw e}}function Di(e,t,n,a){uc=!1;var s=e.updateQueue;Wn=!1;var c=s.firstBaseUpdate,d=s.lastBaseUpdate,y=s.shared.pending;if(y!==null){s.shared.pending=null;var S=y,_=S.next;S.next=null,d===null?c=_:d.next=_,d=S;var L=e.alternate;L!==null&&(L=L.updateQueue,y=L.lastBaseUpdate,y!==d&&(y===null?L.firstBaseUpdate=_:y.next=_,L.lastBaseUpdate=S))}if(c!==null){var Y=s.baseState;d=0,L=_=S=null,y=c;do{var z=y.lane&-536870913,U=z!==y.lane;if(U?(pe&z)===z:(a&z)===z){z!==0&&z===Nl&&(uc=!0),L!==null&&(L=L.next={lane:0,tag:y.tag,payload:y.payload,callback:null,next:null});e:{var P=e,ie=y;z=t;var Me=n;switch(ie.tag){case 1:if(P=ie.payload,typeof P=="function"){Y=P.call(Me,Y,z);break e}Y=P;break e;case 3:P.flags=P.flags&-65537|128;case 0:if(P=ie.payload,z=typeof P=="function"?P.call(Me,Y,z):P,z==null)break e;Y=b({},Y,z);break e;case 2:Wn=!0}}z=y.callback,z!==null&&(e.flags|=64,U&&(e.flags|=8192),U=s.callbacks,U===null?s.callbacks=[z]:U.push(z))}else U={lane:z,tag:y.tag,payload:y.payload,callback:y.callback,next:null},L===null?(_=L=U,S=Y):L=L.next=U,d|=z;if(y=y.next,y===null){if(y=s.shared.pending,y===null)break;U=y,y=U.next,U.next=null,s.lastBaseUpdate=U,s.shared.pending=null}}while(!0);L===null&&(S=Y),s.baseState=S,s.firstBaseUpdate=_,s.lastBaseUpdate=L,c===null&&(s.shared.lanes=0),la|=d,e.lanes=d,e.memoizedState=Y}}function Jd(e,t){if(typeof e!="function")throw Error(r(191,e));e.call(t)}function Fd(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ec?c:8;var d=q.T,y={};q.T=y,Nc(e,!1,t,n);try{var S=s(),_=q.S;if(_!==null&&_(y,S),S!==null&&typeof S=="object"&&typeof S.then=="function"){var L=Wg(S,a);Ui(e,t,L,Lt(e))}else Ui(e,t,a,Lt(e))}catch(Y){Ui(e,t,{then:function(){},status:"rejected",reason:Y},Lt())}finally{V.p=c,d!==null&&y.types!==null&&(d.types=y.types),q.T=d}}function av(){}function Rc(e,t,n,a){if(e.tag!==5)throw Error(r(476));var s=jh(e).queue;Oh(e,s,t,ee,n===null?av:function(){return Ah(e),n(a)})}function jh(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:On,lastRenderedState:ee},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:On,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ah(e){var t=jh(e);t.next===null&&(t=e.alternate.memoizedState),Ui(e,t.next.queue,{},Lt())}function Tc(){return st(Pi)}function wh(){return Je().memoizedState}function Ch(){return Je().memoizedState}function lv(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=Lt();e=Pn(n);var a=In(t,e,n);a!==null&&(Rt(a,t,n),Ci(a,t,n)),t={cache:Ir()},e.payload=t;return}t=t.return}}function iv(e,t,n){var a=Lt();n={lane:a,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},is(e)?Dh(t,n):(n=Yr(e,t,n,a),n!==null&&(Rt(n,e,a),zh(n,t,a)))}function _h(e,t,n){var a=Lt();Ui(e,t,n,a)}function Ui(e,t,n,a){var s={lane:a,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(is(e))Dh(t,s);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var d=t.lastRenderedState,y=c(d,n);if(s.hasEagerState=!0,s.eagerState=y,_t(y,d))return Bu(e,t,s,0),Ue===null&&Hu(),!1}catch{}finally{}if(n=Yr(e,t,s,a),n!==null)return Rt(n,e,a),zh(n,t,a),!0}return!1}function Nc(e,t,n,a){if(a={lane:2,revertLane:ao(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},is(e)){if(t)throw Error(r(479))}else t=Yr(e,n,a,2),t!==null&&Rt(t,e,2)}function is(e){var t=e.alternate;return e===ce||t!==null&&t===ce}function Dh(e,t){Cl=Pu=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function zh(e,t,n){if((n&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,n|=a,t.lanes=n,Hf(e,n)}}var qi={readContext:st,use:ts,useCallback:Ke,useContext:Ke,useEffect:Ke,useImperativeHandle:Ke,useLayoutEffect:Ke,useInsertionEffect:Ke,useMemo:Ke,useReducer:Ke,useRef:Ke,useState:Ke,useDebugValue:Ke,useDeferredValue:Ke,useTransition:Ke,useSyncExternalStore:Ke,useId:Ke,useHostTransitionStatus:Ke,useFormState:Ke,useActionState:Ke,useOptimistic:Ke,useMemoCache:Ke,useCacheRefresh:Ke};qi.useEffectEvent=Ke;var Mh={readContext:st,use:ts,useCallback:function(e,t){return mt().memoizedState=[e,t===void 0?null:t],e},useContext:st,useEffect:gh,useImperativeHandle:function(e,t,n){n=n!=null?n.concat([e]):null,as(4194308,4,Sh.bind(null,t,e),n)},useLayoutEffect:function(e,t){return as(4194308,4,e,t)},useInsertionEffect:function(e,t){as(4,2,e,t)},useMemo:function(e,t){var n=mt();t=t===void 0?null:t;var a=e();if(Ga){kn(!0);try{e()}finally{kn(!1)}}return n.memoizedState=[a,t],a},useReducer:function(e,t,n){var a=mt();if(n!==void 0){var s=n(t);if(Ga){kn(!0);try{n(t)}finally{kn(!1)}}}else s=t;return a.memoizedState=a.baseState=s,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:s},a.queue=e,e=e.dispatch=iv.bind(null,ce,e),[a.memoizedState,e]},useRef:function(e){var t=mt();return e={current:e},t.memoizedState=e},useState:function(e){e=vc(e);var t=e.queue,n=_h.bind(null,ce,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Sc,useDeferredValue:function(e,t){var n=mt();return Ec(n,e,t)},useTransition:function(){var e=vc(!1);return e=Oh.bind(null,ce,e.queue,!0,!1),mt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var a=ce,s=mt();if(ve){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Ue===null)throw Error(r(349));(pe&127)!==0||th(a,t,n)}s.memoizedState=n;var c={value:n,getSnapshot:t};return s.queue=c,gh(ah.bind(null,a,c,e),[e]),a.flags|=2048,Dl(9,{destroy:void 0},nh.bind(null,a,c,n,t),null),n},useId:function(){var e=mt(),t=Ue.identifierPrefix;if(ve){var n=on,a=cn;n=(a&~(1<<32-Ct(a)-1)).toString(32)+n,t="_"+t+"R_"+n,n=Iu++,0<\/script>",c=c.removeChild(c.firstChild);break;case"select":c=typeof a.is=="string"?d.createElement("select",{is:a.is}):d.createElement("select"),a.multiple?c.multiple=!0:a.size&&(c.size=a.size);break;default:c=typeof a.is=="string"?d.createElement(s,{is:a.is}):d.createElement(s)}}c[it]=t,c[gt]=a;e:for(d=t.child;d!==null;){if(d.tag===5||d.tag===6)c.appendChild(d.stateNode);else if(d.tag!==4&&d.tag!==27&&d.child!==null){d.child.return=d,d=d.child;continue}if(d===t)break e;for(;d.sibling===null;){if(d.return===null||d.return===t)break e;d=d.return}d.sibling.return=d.return,d=d.sibling}t.stateNode=c;e:switch(ct(c,s,a),s){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break e;case"img":a=!0;break e;default:a=!1}a&&An(t)}}return Be(t),Bc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==a&&An(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(r(166));if(e=he.current,Rl(t)){if(e=t.stateNode,n=t.memoizedProps,a=null,s=ut,s!==null)switch(s.tag){case 27:case 5:a=s.memoizedProps}e[it]=t,e=!!(e.nodeValue===n||a!==null&&a.suppressHydrationWarning===!0||Im(e.nodeValue,n)),e||Fn(t,!0)}else e=Ns(e).createTextNode(a),e[it]=t,t.stateNode=e}return Be(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(a=Rl(t),n!==null){if(e===null){if(!a)throw Error(r(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(r(557));e[it]=t}else Ma(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Be(t),e=!1}else n=Fr(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Mt(t),t):(Mt(t),null);if((t.flags&128)!==0)throw Error(r(558))}return Be(t),null;case 13:if(a=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(s=Rl(t),a!==null&&a.dehydrated!==null){if(e===null){if(!s)throw Error(r(318));if(s=t.memoizedState,s=s!==null?s.dehydrated:null,!s)throw Error(r(317));s[it]=t}else Ma(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Be(t),s=!1}else s=Fr(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=s),s=!0;if(!s)return t.flags&256?(Mt(t),t):(Mt(t),null)}return Mt(t),(t.flags&128)!==0?(t.lanes=n,t):(n=a!==null,e=e!==null&&e.memoizedState!==null,n&&(a=t.child,s=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(s=a.alternate.memoizedState.cachePool.pool),c=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),c!==s&&(a.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),os(t,t.updateQueue),Be(t),null);case 4:return Ve(),e===null&&so(t.stateNode.containerInfo),Be(t),null;case 10:return Tn(t.type),Be(t),null;case 19:if(Q(Ze),a=t.memoizedState,a===null)return Be(t),null;if(s=(t.flags&128)!==0,c=a.rendering,c===null)if(s)Hi(a,!1);else{if(ke!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(c=Wu(e),c!==null){for(t.flags|=128,Hi(a,!1),e=c.updateQueue,t.updateQueue=e,os(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)_d(n,e),n=n.sibling;return Z(Ze,Ze.current&1|2),ve&&En(t,a.treeForkCount),t.child}e=e.sibling}a.tail!==null&&At()>ys&&(t.flags|=128,s=!0,Hi(a,!1),t.lanes=4194304)}else{if(!s)if(e=Wu(c),e!==null){if(t.flags|=128,s=!0,e=e.updateQueue,t.updateQueue=e,os(t,e),Hi(a,!0),a.tail===null&&a.tailMode==="hidden"&&!c.alternate&&!ve)return Be(t),null}else 2*At()-a.renderingStartTime>ys&&n!==536870912&&(t.flags|=128,s=!0,Hi(a,!1),t.lanes=4194304);a.isBackwards?(c.sibling=t.child,t.child=c):(e=a.last,e!==null?e.sibling=c:t.child=c,a.last=c)}return a.tail!==null?(e=a.tail,a.rendering=e,a.tail=e.sibling,a.renderingStartTime=At(),e.sibling=null,n=Ze.current,Z(Ze,s?n&1|2:n&1),ve&&En(t,a.treeForkCount),e):(Be(t),null);case 22:case 23:return Mt(t),rc(),a=t.memoizedState!==null,e!==null?e.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(n&536870912)!==0&&(t.flags&128)===0&&(Be(t),t.subtreeFlags&6&&(t.flags|=8192)):Be(t),n=t.updateQueue,n!==null&&os(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==n&&(t.flags|=2048),e!==null&&Q(La),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Tn(Fe),Be(t),null;case 25:return null;case 30:return null}throw Error(r(156,t.tag))}function ov(e,t){switch(Zr(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Tn(Fe),Ve(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Su(t),null;case 31:if(t.memoizedState!==null){if(Mt(t),t.alternate===null)throw Error(r(340));Ma()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Mt(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ma()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Q(Ze),null;case 4:return Ve(),null;case 10:return Tn(t.type),null;case 22:case 23:return Mt(t),rc(),e!==null&&Q(La),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Tn(Fe),null;case 25:return null;default:return null}}function lm(e,t){switch(Zr(t),t.tag){case 3:Tn(Fe),Ve();break;case 26:case 27:case 5:Su(t);break;case 4:Ve();break;case 31:t.memoizedState!==null&&Mt(t);break;case 13:Mt(t);break;case 19:Q(Ze);break;case 10:Tn(t.type);break;case 22:case 23:Mt(t),rc(),e!==null&&Q(La);break;case 24:Tn(Fe)}}function Bi(e,t){try{var n=t.updateQueue,a=n!==null?n.lastEffect:null;if(a!==null){var s=a.next;n=s;do{if((n.tag&e)===e){a=void 0;var c=n.create,d=n.inst;a=c(),d.destroy=a}n=n.next}while(n!==s)}}catch(y){Ce(t,t.return,y)}}function na(e,t,n){try{var a=t.updateQueue,s=a!==null?a.lastEffect:null;if(s!==null){var c=s.next;a=c;do{if((a.tag&e)===e){var d=a.inst,y=d.destroy;if(y!==void 0){d.destroy=void 0,s=t;var S=n,_=y;try{_()}catch(L){Ce(s,S,L)}}}a=a.next}while(a!==c)}}catch(L){Ce(t,t.return,L)}}function im(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Fd(t,n)}catch(a){Ce(e,e.return,a)}}}function um(e,t,n){n.props=Ya(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(a){Ce(e,t,a)}}function Qi(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var a=e.stateNode;break;case 30:a=e.stateNode;break;default:a=e.stateNode}typeof n=="function"?e.refCleanup=n(a):n.current=a}}catch(s){Ce(e,t,s)}}function fn(e,t){var n=e.ref,a=e.refCleanup;if(n!==null)if(typeof a=="function")try{a()}catch(s){Ce(e,t,s)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(s){Ce(e,t,s)}else n.current=null}function sm(e){var t=e.type,n=e.memoizedProps,a=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&a.focus();break e;case"img":n.src?a.src=n.src:n.srcSet&&(a.srcset=n.srcSet)}}catch(s){Ce(e,e.return,s)}}function Qc(e,t,n){try{var a=e.stateNode;Dv(a,e.type,n,t),a[gt]=t}catch(s){Ce(e,e.return,s)}}function rm(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&ca(e.type)||e.tag===4}function Gc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||rm(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&ca(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Yc(e,t,n){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=bn));else if(a!==4&&(a===27&&ca(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Yc(e,t,n),e=e.sibling;e!==null;)Yc(e,t,n),e=e.sibling}function fs(e,t,n){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(a!==4&&(a===27&&ca(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(fs(e,t,n),e=e.sibling;e!==null;)fs(e,t,n),e=e.sibling}function cm(e){var t=e.stateNode,n=e.memoizedProps;try{for(var a=e.type,s=t.attributes;s.length;)t.removeAttributeNode(s[0]);ct(t,a,n),t[it]=e,t[gt]=n}catch(c){Ce(e,e.return,c)}}var wn=!1,Pe=!1,Kc=!1,om=typeof WeakSet=="function"?WeakSet:Set,at=null;function fv(e,t){if(e=e.containerInfo,oo=Ds,e=Ed(e),qr(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var a=n.getSelection&&n.getSelection();if(a&&a.rangeCount!==0){n=a.anchorNode;var s=a.anchorOffset,c=a.focusNode;a=a.focusOffset;try{n.nodeType,c.nodeType}catch{n=null;break e}var d=0,y=-1,S=-1,_=0,L=0,Y=e,z=null;t:for(;;){for(var U;Y!==n||s!==0&&Y.nodeType!==3||(y=d+s),Y!==c||a!==0&&Y.nodeType!==3||(S=d+a),Y.nodeType===3&&(d+=Y.nodeValue.length),(U=Y.firstChild)!==null;)z=Y,Y=U;for(;;){if(Y===e)break t;if(z===n&&++_===s&&(y=d),z===c&&++L===a&&(S=d),(U=Y.nextSibling)!==null)break;Y=z,z=Y.parentNode}Y=U}n=y===-1||S===-1?null:{start:y,end:S}}else n=null}n=n||{start:0,end:0}}else n=null;for(fo={focusedElem:e,selectionRange:n},Ds=!1,at=t;at!==null;)if(t=at,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,at=e;else for(;at!==null;){switch(t=at,c=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(n=0;n title"))),ct(c,a,n),c[it]=e,nt(c),a=c;break e;case"link":var d=py("link","href",s).get(a+(n.href||""));if(d){for(var y=0;yMe&&(d=Me,Me=ie,ie=d);var O=xd(y,ie),T=xd(y,Me);if(O&&T&&(U.rangeCount!==1||U.anchorNode!==O.node||U.anchorOffset!==O.offset||U.focusNode!==T.node||U.focusOffset!==T.offset)){var w=Y.createRange();w.setStart(O.node,O.offset),U.removeAllRanges(),ie>Me?(U.addRange(w),U.extend(T.node,T.offset)):(w.setEnd(T.node,T.offset),U.addRange(w))}}}}for(Y=[],U=y;U=U.parentNode;)U.nodeType===1&&Y.push({element:U,left:U.scrollLeft,top:U.scrollTop});for(typeof y.focus=="function"&&y.focus(),y=0;yn?32:n,q.T=null,n=$c,$c=null;var c=ua,d=Mn;if(et=0,Ll=ua=null,Mn=0,(Te&6)!==0)throw Error(r(331));var y=Te;if(Te|=4,Sm(c.current),vm(c,c.current,d,n),Te=y,Vi(0,!1),wt&&typeof wt.onPostCommitFiberRoot=="function")try{wt.onPostCommitFiberRoot(oi,c)}catch{}return!0}finally{V.p=s,q.T=a,Bm(e,t)}}function Gm(e,t,n){t=Yt(n,t),t=wc(e.stateNode,t,2),e=In(e,t,2),e!==null&&(di(e,2),dn(e))}function Ce(e,t,n){if(e.tag===3)Gm(e,e,n);else for(;t!==null;){if(t.tag===3){Gm(t,e,n);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(ia===null||!ia.has(a))){e=Yt(n,e),n=Yh(2),a=In(t,n,2),a!==null&&(Kh(n,a,t,e),di(a,2),dn(a));break}}t=t.return}}function eo(e,t,n){var a=e.pingCache;if(a===null){a=e.pingCache=new mv;var s=new Set;a.set(t,s)}else s=a.get(t),s===void 0&&(s=new Set,a.set(t,s));s.has(n)||(Vc=!0,s.add(n),e=bv.bind(null,e,t,n),t.then(e,e))}function bv(e,t,n){var a=e.pingCache;a!==null&&a.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ue===e&&(pe&n)===n&&(ke===4||ke===3&&(pe&62914560)===pe&&300>At()-ms?(Te&2)===0&&Hl(e,0):Zc|=n,ql===pe&&(ql=0)),dn(e)}function Ym(e,t){t===0&&(t=qf()),e=Da(e,t),e!==null&&(di(e,t),dn(e))}function xv(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ym(e,n)}function Sv(e,t){var n=0;switch(e.tag){case 31:case 13:var a=e.stateNode,s=e.memoizedState;s!==null&&(n=s.retryLane);break;case 19:a=e.stateNode;break;case 22:a=e.stateNode._retryCache;break;default:throw Error(r(314))}a!==null&&a.delete(t),Ym(e,n)}function Ev(e,t){return hr(e,t)}var Ss=null,Ql=null,to=!1,Es=!1,no=!1,ra=0;function dn(e){e!==Ql&&e.next===null&&(Ql===null?Ss=Ql=e:Ql=Ql.next=e),Es=!0,to||(to=!0,Tv())}function Vi(e,t){if(!no&&Es){no=!0;do for(var n=!1,a=Ss;a!==null;){if(e!==0){var s=a.pendingLanes;if(s===0)var c=0;else{var d=a.suspendedLanes,y=a.pingedLanes;c=(1<<31-Ct(42|e)+1)-1,c&=s&~(d&~y),c=c&201326741?c&201326741|1:c?c|2:0}c!==0&&(n=!0,Vm(a,c))}else c=pe,c=Ou(a,a===Ue?c:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(c&3)===0||fi(a,c)||(n=!0,Vm(a,c));a=a.next}while(n);no=!1}}function Rv(){Km()}function Km(){Es=to=!1;var e=0;ra!==0&&Mv()&&(e=ra);for(var t=At(),n=null,a=Ss;a!==null;){var s=a.next,c=km(a,t);c===0?(a.next=null,n===null?Ss=s:n.next=s,s===null&&(Ql=n)):(n=a,(e!==0||(c&3)!==0)&&(Es=!0)),a=s}et!==0&&et!==5||Vi(e),ra!==0&&(ra=0)}function km(e,t){for(var n=e.suspendedLanes,a=e.pingedLanes,s=e.expirationTimes,c=e.pendingLanes&-62914561;0y)break;var L=S.transferSize,Y=S.initiatorType;L&&ey(Y)&&(S=S.responseEnd,d+=L*(S"u"?null:document;function dy(e,t,n){var a=Gl;if(a&&typeof t=="string"&&t){var s=Qt(t);s='link[rel="'+e+'"][href="'+s+'"]',typeof n=="string"&&(s+='[crossorigin="'+n+'"]'),fy.has(s)||(fy.add(s),e={rel:e,crossOrigin:n,href:t},a.querySelector(s)===null&&(t=a.createElement("link"),ct(t,"link",e),nt(t),a.head.appendChild(t)))}}function Kv(e){Un.D(e),dy("dns-prefetch",e,null)}function kv(e,t){Un.C(e,t),dy("preconnect",e,t)}function Xv(e,t,n){Un.L(e,t,n);var a=Gl;if(a&&e&&t){var s='link[rel="preload"][as="'+Qt(t)+'"]';t==="image"&&n&&n.imageSrcSet?(s+='[imagesrcset="'+Qt(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(s+='[imagesizes="'+Qt(n.imageSizes)+'"]')):s+='[href="'+Qt(e)+'"]';var c=s;switch(t){case"style":c=Yl(e);break;case"script":c=Kl(e)}Jt.has(c)||(e=b({rel:"preload",href:t==="image"&&n&&n.imageSrcSet?void 0:e,as:t},n),Jt.set(c,e),a.querySelector(s)!==null||t==="style"&&a.querySelector($i(c))||t==="script"&&a.querySelector(Wi(c))||(t=a.createElement("link"),ct(t,"link",e),nt(t),a.head.appendChild(t)))}}function Vv(e,t){Un.m(e,t);var n=Gl;if(n&&e){var a=t&&typeof t.as=="string"?t.as:"script",s='link[rel="modulepreload"][as="'+Qt(a)+'"][href="'+Qt(e)+'"]',c=s;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":c=Kl(e)}if(!Jt.has(c)&&(e=b({rel:"modulepreload",href:e},t),Jt.set(c,e),n.querySelector(s)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(Wi(c)))return}a=n.createElement("link"),ct(a,"link",e),nt(a),n.head.appendChild(a)}}}function Zv(e,t,n){Un.S(e,t,n);var a=Gl;if(a&&e){var s=ol(a).hoistableStyles,c=Yl(e);t=t||"default";var d=s.get(c);if(!d){var y={loading:0,preload:null};if(d=a.querySelector($i(c)))y.loading=5;else{e=b({rel:"stylesheet",href:e,"data-precedence":t},n),(n=Jt.get(c))&&bo(e,n);var S=d=a.createElement("link");nt(S),ct(S,"link",e),S._p=new Promise(function(_,L){S.onload=_,S.onerror=L}),S.addEventListener("load",function(){y.loading|=1}),S.addEventListener("error",function(){y.loading|=2}),y.loading|=4,js(d,t,a)}d={type:"stylesheet",instance:d,count:1,state:y},s.set(c,d)}}}function Jv(e,t){Un.X(e,t);var n=Gl;if(n&&e){var a=ol(n).hoistableScripts,s=Kl(e),c=a.get(s);c||(c=n.querySelector(Wi(s)),c||(e=b({src:e,async:!0},t),(t=Jt.get(s))&&xo(e,t),c=n.createElement("script"),nt(c),ct(c,"link",e),n.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(s,c))}}function Fv(e,t){Un.M(e,t);var n=Gl;if(n&&e){var a=ol(n).hoistableScripts,s=Kl(e),c=a.get(s);c||(c=n.querySelector(Wi(s)),c||(e=b({src:e,async:!0,type:"module"},t),(t=Jt.get(s))&&xo(e,t),c=n.createElement("script"),nt(c),ct(c,"link",e),n.head.appendChild(c)),c={type:"script",instance:c,count:1,state:null},a.set(s,c))}}function hy(e,t,n,a){var s=(s=he.current)?Os(s):null;if(!s)throw Error(r(446));switch(e){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(t=Yl(n.href),n=ol(s).hoistableStyles,a=n.get(t),a||(a={type:"style",instance:null,count:0,state:null},n.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){e=Yl(n.href);var c=ol(s).hoistableStyles,d=c.get(e);if(d||(s=s.ownerDocument||s,d={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},c.set(e,d),(c=s.querySelector($i(e)))&&!c._p&&(d.instance=c,d.state.loading=5),Jt.has(e)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Jt.set(e,n),c||$v(s,e,n,d.state))),t&&a===null)throw Error(r(528,""));return d}if(t&&a!==null)throw Error(r(529,""));return null;case"script":return t=n.async,n=n.src,typeof n=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Kl(n),n=ol(s).hoistableScripts,a=n.get(t),a||(a={type:"script",instance:null,count:0,state:null},n.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,e))}}function Yl(e){return'href="'+Qt(e)+'"'}function $i(e){return'link[rel="stylesheet"]['+e+"]"}function my(e){return b({},e,{"data-precedence":e.precedence,precedence:null})}function $v(e,t,n,a){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=e.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),ct(t,"link",n),nt(t),e.head.appendChild(t))}function Kl(e){return'[src="'+Qt(e)+'"]'}function Wi(e){return"script[async]"+e}function yy(e,t,n){if(t.count++,t.instance===null)switch(t.type){case"style":var a=e.querySelector('style[data-href~="'+Qt(n.href)+'"]');if(a)return t.instance=a,nt(a),a;var s=b({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return a=(e.ownerDocument||e).createElement("style"),nt(a),ct(a,"style",s),js(a,n.precedence,e),t.instance=a;case"stylesheet":s=Yl(n.href);var c=e.querySelector($i(s));if(c)return t.state.loading|=4,t.instance=c,nt(c),c;a=my(n),(s=Jt.get(s))&&bo(a,s),c=(e.ownerDocument||e).createElement("link"),nt(c);var d=c;return d._p=new Promise(function(y,S){d.onload=y,d.onerror=S}),ct(c,"link",a),t.state.loading|=4,js(c,n.precedence,e),t.instance=c;case"script":return c=Kl(n.src),(s=e.querySelector(Wi(c)))?(t.instance=s,nt(s),s):(a=n,(s=Jt.get(c))&&(a=b({},n),xo(a,s)),e=e.ownerDocument||e,s=e.createElement("script"),nt(s),ct(s,"link",a),e.head.appendChild(s),t.instance=s);case"void":return null;default:throw Error(r(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,js(a,n.precedence,e));return t.instance}function js(e,t,n){for(var a=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),s=a.length?a[a.length-1]:null,c=s,d=0;d title"):null)}function Wv(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function vy(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function Pv(e,t,n,a){if(n.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var s=Yl(a.href),c=t.querySelector($i(s));if(c){t=c._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=ws.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=c,nt(c);return}c=t.ownerDocument||t,a=my(a),(s=Jt.get(s))&&bo(a,s),c=c.createElement("link"),nt(c);var d=c;d._p=new Promise(function(y,S){d.onload=y,d.onerror=S}),ct(c,"link",a),n.instance=c}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&(n.state.loading&3)===0&&(e.count++,n=ws.bind(e),t.addEventListener("load",n),t.addEventListener("error",n))}}var So=0;function Iv(e,t){return e.stylesheets&&e.count===0&&_s(e,e.stylesheets),0So?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(a),clearTimeout(s)}}:null}function ws(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)_s(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Cs=null;function _s(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Cs=new Map,t.forEach(eb,e),Cs=null,ws.call(e))}function eb(e,t){if(!(t.state.loading&4)){var n=Cs.get(e);if(n)var a=n.get(null);else{n=new Map,Cs.set(e,n);for(var s=e.querySelectorAll("link[data-precedence],style[data-precedence]"),c=0;c"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(l)}catch(i){console.error(i)}}return l(),_o.exports=gb(),_o.exports}var bb=vb();const xb=Lp(bb);/** + * react-router v7.13.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var Ky="popstate";function ky(l){return typeof l=="object"&&l!=null&&"pathname"in l&&"search"in l&&"hash"in l&&"state"in l&&"key"in l}function Sb(l={}){function i(r,o){var p;let f=(p=o.state)==null?void 0:p.masked,{pathname:h,search:g,hash:v}=f||r.location;return Ko("",{pathname:h,search:g,hash:v},o.state&&o.state.usr||null,o.state&&o.state.key||"default",f?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function u(r,o){return typeof o=="string"?o:su(o)}return Rb(i,u,null,l)}function Ye(l,i){if(l===!1||l===null||typeof l>"u")throw new Error(i)}function un(l,i){if(!l){typeof console<"u"&&console.warn(i);try{throw new Error(i)}catch{}}}function Eb(){return Math.random().toString(36).substring(2,10)}function Xy(l,i){return{usr:l.state,key:l.key,idx:i,masked:l.unstable_mask?{pathname:l.pathname,search:l.search,hash:l.hash}:void 0}}function Ko(l,i,u=null,r,o){return{pathname:typeof l=="string"?l:l.pathname,search:"",hash:"",...typeof i=="string"?li(i):i,state:u,key:i&&i.key||r||Eb(),unstable_mask:o}}function su({pathname:l="/",search:i="",hash:u=""}){return i&&i!=="?"&&(l+=i.charAt(0)==="?"?i:"?"+i),u&&u!=="#"&&(l+=u.charAt(0)==="#"?u:"#"+u),l}function li(l){let i={};if(l){let u=l.indexOf("#");u>=0&&(i.hash=l.substring(u),l=l.substring(0,u));let r=l.indexOf("?");r>=0&&(i.search=l.substring(r),l=l.substring(0,r)),l&&(i.pathname=l)}return i}function Rb(l,i,u,r={}){let{window:o=document.defaultView,v5Compat:f=!1}=r,h=o.history,g="POP",v=null,p=x();p==null&&(p=0,h.replaceState({...h.state,idx:p},""));function x(){return(h.state||{idx:null}).idx}function b(){g="POP";let j=x(),H=j==null?null:j-p;p=j,v&&v({action:g,location:D.location,delta:H})}function A(j,H){g="PUSH";let X=ky(j)?j:Ko(D.location,j,H);p=x()+1;let K=Xy(X,p),k=D.createHref(X.unstable_mask||X);try{h.pushState(K,"",k)}catch(F){if(F instanceof DOMException&&F.name==="DataCloneError")throw F;o.location.assign(k)}f&&v&&v({action:g,location:D.location,delta:1})}function G(j,H){g="REPLACE";let X=ky(j)?j:Ko(D.location,j,H);p=x();let K=Xy(X,p),k=D.createHref(X.unstable_mask||X);h.replaceState(K,"",k),f&&v&&v({action:g,location:D.location,delta:0})}function N(j){return Tb(j)}let D={get action(){return g},get location(){return l(o,h)},listen(j){if(v)throw new Error("A history only accepts one active listener");return o.addEventListener(Ky,b),v=j,()=>{o.removeEventListener(Ky,b),v=null}},createHref(j){return i(o,j)},createURL:N,encodeLocation(j){let H=N(j);return{pathname:H.pathname,search:H.search,hash:H.hash}},push:A,replace:G,go(j){return h.go(j)}};return D}function Tb(l,i=!1){let u="http://localhost";typeof window<"u"&&(u=window.location.origin!=="null"?window.location.origin:window.location.href),Ye(u,"No window.location.(origin|href) available to create URL");let r=typeof l=="string"?l:su(l);return r=r.replace(/ $/,"%20"),!i&&r.startsWith("//")&&(r=u+r),new URL(r,u)}function Hp(l,i,u="/"){return Nb(l,i,u,!1)}function Nb(l,i,u,r){let o=typeof i=="string"?li(i):i,f=Yn(o.pathname||"/",u);if(f==null)return null;let h=Bp(l);Ob(h);let g=null;for(let v=0;g==null&&v{let x={relativePath:p===void 0?h.path||"":p,caseSensitive:h.caseSensitive===!0,childrenIndex:g,route:h};if(x.relativePath.startsWith("/")){if(!x.relativePath.startsWith(r)&&v)return;Ye(x.relativePath.startsWith(r),`Absolute route path "${x.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),x.relativePath=x.relativePath.slice(r.length)}let b=gn([r,x.relativePath]),A=u.concat(x);h.children&&h.children.length>0&&(Ye(h.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${b}".`),Bp(h.children,i,A,b,v)),!(h.path==null&&!h.index)&&i.push({path:b,score:zb(b,h.index),routesMeta:A})};return l.forEach((h,g)=>{var v;if(h.path===""||!((v=h.path)!=null&&v.includes("?")))f(h,g);else for(let p of Qp(h.path))f(h,g,!0,p)}),i}function Qp(l){let i=l.split("/");if(i.length===0)return[];let[u,...r]=i,o=u.endsWith("?"),f=u.replace(/\?$/,"");if(r.length===0)return o?[f,""]:[f];let h=Qp(r.join("/")),g=[];return g.push(...h.map(v=>v===""?f:[f,v].join("/"))),o&&g.push(...h),g.map(v=>l.startsWith("/")&&v===""?"/":v)}function Ob(l){l.sort((i,u)=>i.score!==u.score?u.score-i.score:Mb(i.routesMeta.map(r=>r.childrenIndex),u.routesMeta.map(r=>r.childrenIndex)))}var jb=/^:[\w-]+$/,Ab=3,wb=2,Cb=1,_b=10,Db=-2,Vy=l=>l==="*";function zb(l,i){let u=l.split("/"),r=u.length;return u.some(Vy)&&(r+=Db),i&&(r+=wb),u.filter(o=>!Vy(o)).reduce((o,f)=>o+(jb.test(f)?Ab:f===""?Cb:_b),r)}function Mb(l,i){return l.length===i.length&&l.slice(0,-1).every((r,o)=>r===i[o])?l[l.length-1]-i[i.length-1]:0}function Ub(l,i,u=!1){let{routesMeta:r}=l,o={},f="/",h=[];for(let g=0;g{if(x==="*"){let N=g[A]||"";h=f.slice(0,f.length-N.length).replace(/(.)\/+$/,"$1")}const G=g[A];return b&&!G?p[x]=void 0:p[x]=(G||"").replace(/%2F/g,"/"),p},{}),pathname:f,pathnameBase:h,pattern:l}}function qb(l,i=!1,u=!0){un(l==="*"||!l.endsWith("*")||l.endsWith("/*"),`Route path "${l}" will be treated as if it were "${l.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${l.replace(/\*$/,"/*")}".`);let r=[],o="^"+l.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(h,g,v,p,x)=>{if(r.push({paramName:g,isOptional:v!=null}),v){let b=x.charAt(p+h.length);return b&&b!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return l.endsWith("*")?(r.push({paramName:"*"}),o+=l==="*"||l==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):u?o+="\\/*$":l!==""&&l!=="/"&&(o+="(?:(?=\\/|$))"),[new RegExp(o,i?void 0:"i"),r]}function Lb(l){try{return l.split("/").map(i=>decodeURIComponent(i).replace(/\//g,"%2F")).join("/")}catch(i){return un(!1,`The URL path "${l}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${i}).`),l}}function Yn(l,i){if(i==="/")return l;if(!l.toLowerCase().startsWith(i.toLowerCase()))return null;let u=i.endsWith("/")?i.length-1:i.length,r=l.charAt(u);return r&&r!=="/"?null:l.slice(u)||"/"}var Hb=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Bb(l,i="/"){let{pathname:u,search:r="",hash:o=""}=typeof l=="string"?li(l):l,f;return u?(u=u.replace(/\/\/+/g,"/"),u.startsWith("/")?f=Zy(u.substring(1),"/"):f=Zy(u,i)):f=i,{pathname:f,search:Yb(r),hash:Kb(o)}}function Zy(l,i){let u=i.replace(/\/+$/,"").split("/");return l.split("/").forEach(o=>{o===".."?u.length>1&&u.pop():o!=="."&&u.push(o)}),u.length>1?u.join("/"):"/"}function Uo(l,i,u,r){return`Cannot include a '${l}' character in a manually specified \`to.${i}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${u}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Qb(l){return l.filter((i,u)=>u===0||i.route.path&&i.route.path.length>0)}function df(l){let i=Qb(l);return i.map((u,r)=>r===i.length-1?u.pathname:u.pathnameBase)}function tr(l,i,u,r=!1){let o;typeof l=="string"?o=li(l):(o={...l},Ye(!o.pathname||!o.pathname.includes("?"),Uo("?","pathname","search",o)),Ye(!o.pathname||!o.pathname.includes("#"),Uo("#","pathname","hash",o)),Ye(!o.search||!o.search.includes("#"),Uo("#","search","hash",o)));let f=l===""||o.pathname==="",h=f?"/":o.pathname,g;if(h==null)g=u;else{let b=i.length-1;if(!r&&h.startsWith("..")){let A=h.split("/");for(;A[0]==="..";)A.shift(),b-=1;o.pathname=A.join("/")}g=b>=0?i[b]:"/"}let v=Bb(o,g),p=h&&h!=="/"&&h.endsWith("/"),x=(f||h===".")&&u.endsWith("/");return!v.pathname.endsWith("/")&&(p||x)&&(v.pathname+="/"),v}var gn=l=>l.join("/").replace(/\/\/+/g,"/"),Gb=l=>l.replace(/\/+$/,"").replace(/^\/*/,"/"),Yb=l=>!l||l==="?"?"":l.startsWith("?")?l:"?"+l,Kb=l=>!l||l==="#"?"":l.startsWith("#")?l:"#"+l,kb=class{constructor(l,i,u,r=!1){this.status=l,this.statusText=i||"",this.internal=r,u instanceof Error?(this.data=u.toString(),this.error=u):this.data=u}};function Xb(l){return l!=null&&typeof l.status=="number"&&typeof l.statusText=="string"&&typeof l.internal=="boolean"&&"data"in l}function Vb(l){return l.map(i=>i.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Gp=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Yp(l,i){let u=l;if(typeof u!="string"||!Hb.test(u))return{absoluteURL:void 0,isExternal:!1,to:u};let r=u,o=!1;if(Gp)try{let f=new URL(window.location.href),h=u.startsWith("//")?new URL(f.protocol+u):new URL(u),g=Yn(h.pathname,i);h.origin===f.origin&&g!=null?u=g+h.search+h.hash:o=!0}catch{un(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:o,to:u}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Kp=["POST","PUT","PATCH","DELETE"];new Set(Kp);var Zb=["GET",...Kp];new Set(Zb);var ii=C.createContext(null);ii.displayName="DataRouter";var nr=C.createContext(null);nr.displayName="DataRouterState";var Jb=C.createContext(!1),kp=C.createContext({isTransitioning:!1});kp.displayName="ViewTransition";var Fb=C.createContext(new Map);Fb.displayName="Fetchers";var $b=C.createContext(null);$b.displayName="Await";var Ht=C.createContext(null);Ht.displayName="Navigation";var mu=C.createContext(null);mu.displayName="Location";var Pt=C.createContext({outlet:null,matches:[],isDataRoute:!1});Pt.displayName="Route";var hf=C.createContext(null);hf.displayName="RouteError";var Xp="REACT_ROUTER_ERROR",Wb="REDIRECT",Pb="ROUTE_ERROR_RESPONSE";function Ib(l){if(l.startsWith(`${Xp}:${Wb}:{`))try{let i=JSON.parse(l.slice(28));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.location=="string"&&typeof i.reloadDocument=="boolean"&&typeof i.replace=="boolean")return i}catch{}}function ex(l){if(l.startsWith(`${Xp}:${Pb}:{`))try{let i=JSON.parse(l.slice(40));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string")return new kb(i.status,i.statusText,i.data)}catch{}}function tx(l,{relative:i}={}){Ye(ui(),"useHref() may be used only in the context of a component.");let{basename:u,navigator:r}=C.useContext(Ht),{hash:o,pathname:f,search:h}=yu(l,{relative:i}),g=f;return u!=="/"&&(g=f==="/"?u:gn([u,f])),r.createHref({pathname:g,search:h,hash:o})}function ui(){return C.useContext(mu)!=null}function sn(){return Ye(ui(),"useLocation() may be used only in the context of a component."),C.useContext(mu).location}var Vp="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Zp(l){C.useContext(Ht).static||C.useLayoutEffect(l)}function mf(){let{isDataRoute:l}=C.useContext(Pt);return l?gx():nx()}function nx(){Ye(ui(),"useNavigate() may be used only in the context of a component.");let l=C.useContext(ii),{basename:i,navigator:u}=C.useContext(Ht),{matches:r}=C.useContext(Pt),{pathname:o}=sn(),f=JSON.stringify(df(r)),h=C.useRef(!1);return Zp(()=>{h.current=!0}),C.useCallback((v,p={})=>{if(un(h.current,Vp),!h.current)return;if(typeof v=="number"){u.go(v);return}let x=tr(v,JSON.parse(f),o,p.relative==="path");l==null&&i!=="/"&&(x.pathname=x.pathname==="/"?i:gn([i,x.pathname])),(p.replace?u.replace:u.push)(x,p.state,p)},[i,u,f,o,l])}var ax=C.createContext(null);function lx(l){let i=C.useContext(Pt).outlet;return C.useMemo(()=>i&&C.createElement(ax.Provider,{value:l},i),[i,l])}function ix(){let{matches:l}=C.useContext(Pt),i=l[l.length-1];return i?i.params:{}}function yu(l,{relative:i}={}){let{matches:u}=C.useContext(Pt),{pathname:r}=sn(),o=JSON.stringify(df(u));return C.useMemo(()=>tr(l,JSON.parse(o),r,i==="path"),[l,o,r,i])}function ux(l,i){return Jp(l,i)}function Jp(l,i,u){var j;Ye(ui(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=C.useContext(Ht),{matches:o}=C.useContext(Pt),f=o[o.length-1],h=f?f.params:{},g=f?f.pathname:"/",v=f?f.pathnameBase:"/",p=f&&f.route;{let H=p&&p.path||"";$p(g,!p||H.endsWith("*")||H.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${g}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let x=sn(),b;if(i){let H=typeof i=="string"?li(i):i;Ye(v==="/"||((j=H.pathname)==null?void 0:j.startsWith(v)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${v}" but pathname "${H.pathname}" was given in the \`location\` prop.`),b=H}else b=x;let A=b.pathname||"/",G=A;if(v!=="/"){let H=v.replace(/^\//,"").split("/");G="/"+A.replace(/^\//,"").split("/").slice(H.length).join("/")}let N=Hp(l,{pathname:G});un(p||N!=null,`No routes matched location "${b.pathname}${b.search}${b.hash}" `),un(N==null||N[N.length-1].route.element!==void 0||N[N.length-1].route.Component!==void 0||N[N.length-1].route.lazy!==void 0,`Matched leaf route at location "${b.pathname}${b.search}${b.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let D=fx(N&&N.map(H=>Object.assign({},H,{params:Object.assign({},h,H.params),pathname:gn([v,r.encodeLocation?r.encodeLocation(H.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:H.pathname]),pathnameBase:H.pathnameBase==="/"?v:gn([v,r.encodeLocation?r.encodeLocation(H.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:H.pathnameBase])})),o,u);return i&&D?C.createElement(mu.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...b},navigationType:"POP"}},D):D}function sx(){let l=px(),i=Xb(l)?`${l.status} ${l.statusText}`:l instanceof Error?l.message:JSON.stringify(l),u=l instanceof Error?l.stack:null,r="rgba(200,200,200, 0.5)",o={padding:"0.5rem",backgroundColor:r},f={padding:"2px 4px",backgroundColor:r},h=null;return console.error("Error handled by React Router default ErrorBoundary:",l),h=C.createElement(C.Fragment,null,C.createElement("p",null,"💿 Hey developer 👋"),C.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",C.createElement("code",{style:f},"ErrorBoundary")," or"," ",C.createElement("code",{style:f},"errorElement")," prop on your route.")),C.createElement(C.Fragment,null,C.createElement("h2",null,"Unexpected Application Error!"),C.createElement("h3",{style:{fontStyle:"italic"}},i),u?C.createElement("pre",{style:o},u):null,h)}var rx=C.createElement(sx,null),Fp=class extends C.Component{constructor(l){super(l),this.state={location:l.location,revalidation:l.revalidation,error:l.error}}static getDerivedStateFromError(l){return{error:l}}static getDerivedStateFromProps(l,i){return i.location!==l.location||i.revalidation!=="idle"&&l.revalidation==="idle"?{error:l.error,location:l.location,revalidation:l.revalidation}:{error:l.error!==void 0?l.error:i.error,location:i.location,revalidation:l.revalidation||i.revalidation}}componentDidCatch(l,i){this.props.onError?this.props.onError(l,i):console.error("React Router caught the following error during render",l)}render(){let l=this.state.error;if(this.context&&typeof l=="object"&&l&&"digest"in l&&typeof l.digest=="string"){const u=ex(l.digest);u&&(l=u)}let i=l!==void 0?C.createElement(Pt.Provider,{value:this.props.routeContext},C.createElement(hf.Provider,{value:l,children:this.props.component})):this.props.children;return this.context?C.createElement(cx,{error:l},i):i}};Fp.contextType=Jb;var qo=new WeakMap;function cx({children:l,error:i}){let{basename:u}=C.useContext(Ht);if(typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){let r=Ib(i.digest);if(r){let o=qo.get(i);if(o)throw o;let f=Yp(r.location,u);if(Gp&&!qo.get(i))if(f.isExternal||r.reloadDocument)window.location.href=f.absoluteURL||f.to;else{const h=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(f.to,{replace:r.replace}));throw qo.set(i,h),h}return C.createElement("meta",{httpEquiv:"refresh",content:`0;url=${f.absoluteURL||f.to}`})}}return l}function ox({routeContext:l,match:i,children:u}){let r=C.useContext(ii);return r&&r.static&&r.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=i.route.id),C.createElement(Pt.Provider,{value:l},u)}function fx(l,i=[],u){let r=u==null?void 0:u.state;if(l==null){if(!r)return null;if(r.errors)l=r.matches;else if(i.length===0&&!r.initialized&&r.matches.length>0)l=r.matches;else return null}let o=l,f=r==null?void 0:r.errors;if(f!=null){let x=o.findIndex(b=>b.route.id&&(f==null?void 0:f[b.route.id])!==void 0);Ye(x>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(f).join(",")}`),o=o.slice(0,Math.min(o.length,x+1))}let h=!1,g=-1;if(u&&r){h=r.renderFallback;for(let x=0;x=0?o=o.slice(0,g+1):o=[o[0]];break}}}}let v=u==null?void 0:u.onError,p=r&&v?(x,b)=>{var A,G;v(x,{location:r.location,params:((G=(A=r.matches)==null?void 0:A[0])==null?void 0:G.params)??{},unstable_pattern:Vb(r.matches),errorInfo:b})}:void 0;return o.reduceRight((x,b,A)=>{let G,N=!1,D=null,j=null;r&&(G=f&&b.route.id?f[b.route.id]:void 0,D=b.route.errorElement||rx,h&&(g<0&&A===0?($p("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),N=!0,j=null):g===A&&(N=!0,j=b.route.hydrateFallbackElement||null)));let H=i.concat(o.slice(0,A+1)),X=()=>{let K;return G?K=D:N?K=j:b.route.Component?K=C.createElement(b.route.Component,null):b.route.element?K=b.route.element:K=x,C.createElement(ox,{match:b,routeContext:{outlet:x,matches:H,isDataRoute:r!=null},children:K})};return r&&(b.route.ErrorBoundary||b.route.errorElement||A===0)?C.createElement(Fp,{location:r.location,revalidation:r.revalidation,component:D,error:G,children:X(),routeContext:{outlet:null,matches:H,isDataRoute:!0},onError:p}):X()},null)}function yf(l){return`${l} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function dx(l){let i=C.useContext(ii);return Ye(i,yf(l)),i}function hx(l){let i=C.useContext(nr);return Ye(i,yf(l)),i}function mx(l){let i=C.useContext(Pt);return Ye(i,yf(l)),i}function pf(l){let i=mx(l),u=i.matches[i.matches.length-1];return Ye(u.route.id,`${l} can only be used on routes that contain a unique "id"`),u.route.id}function yx(){return pf("useRouteId")}function px(){var r;let l=C.useContext(hf),i=hx("useRouteError"),u=pf("useRouteError");return l!==void 0?l:(r=i.errors)==null?void 0:r[u]}function gx(){let{router:l}=dx("useNavigate"),i=pf("useNavigate"),u=C.useRef(!1);return Zp(()=>{u.current=!0}),C.useCallback(async(o,f={})=>{un(u.current,Vp),u.current&&(typeof o=="number"?await l.navigate(o):await l.navigate(o,{fromRouteId:i,...f}))},[l,i])}var Jy={};function $p(l,i,u){!i&&!Jy[l]&&(Jy[l]=!0,un(!1,u))}C.memo(vx);function vx({routes:l,future:i,state:u,isStatic:r,onError:o}){return Jp(l,void 0,{state:u,isStatic:r,onError:o})}function Wp({to:l,replace:i,state:u,relative:r}){Ye(ui()," may be used only in the context of a component.");let{static:o}=C.useContext(Ht);un(!o," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:f}=C.useContext(Pt),{pathname:h}=sn(),g=mf(),v=tr(l,df(f),h,r==="path"),p=JSON.stringify(v);return C.useEffect(()=>{g(JSON.parse(p),{replace:i,state:u,relative:r})},[g,p,r,i,u]),null}function bx(l){return lx(l.context)}function hn(l){Ye(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function xx({basename:l="/",children:i=null,location:u,navigationType:r="POP",navigator:o,static:f=!1,unstable_useTransitions:h}){Ye(!ui(),"You cannot render a inside another . You should never have more than one in your app.");let g=l.replace(/^\/*/,"/"),v=C.useMemo(()=>({basename:g,navigator:o,static:f,unstable_useTransitions:h,future:{}}),[g,o,f,h]);typeof u=="string"&&(u=li(u));let{pathname:p="/",search:x="",hash:b="",state:A=null,key:G="default",unstable_mask:N}=u,D=C.useMemo(()=>{let j=Yn(p,g);return j==null?null:{location:{pathname:j,search:x,hash:b,state:A,key:G,unstable_mask:N},navigationType:r}},[g,p,x,b,A,G,r,N]);return un(D!=null,` is not able to match the URL "${p}${x}${b}" because it does not start with the basename, so the won't render anything.`),D==null?null:C.createElement(Ht.Provider,{value:v},C.createElement(mu.Provider,{children:i,value:D}))}function Sx({children:l,location:i}){return ux(ko(l),i)}function ko(l,i=[]){let u=[];return C.Children.forEach(l,(r,o)=>{if(!C.isValidElement(r))return;let f=[...i,o];if(r.type===C.Fragment){u.push.apply(u,ko(r.props.children,f));return}Ye(r.type===hn,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Ye(!r.props.index||!r.props.children,"An index route cannot have child routes.");let h={id:r.props.id||f.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(h.children=ko(r.props.children,f)),u.push(h)}),u}var Xs="get",Vs="application/x-www-form-urlencoded";function ar(l){return typeof HTMLElement<"u"&&l instanceof HTMLElement}function Ex(l){return ar(l)&&l.tagName.toLowerCase()==="button"}function Rx(l){return ar(l)&&l.tagName.toLowerCase()==="form"}function Tx(l){return ar(l)&&l.tagName.toLowerCase()==="input"}function Nx(l){return!!(l.metaKey||l.altKey||l.ctrlKey||l.shiftKey)}function Ox(l,i){return l.button===0&&(!i||i==="_self")&&!Nx(l)}var Qs=null;function jx(){if(Qs===null)try{new FormData(document.createElement("form"),0),Qs=!1}catch{Qs=!0}return Qs}var Ax=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Lo(l){return l!=null&&!Ax.has(l)?(un(!1,`"${l}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Vs}"`),null):l}function wx(l,i){let u,r,o,f,h;if(Rx(l)){let g=l.getAttribute("action");r=g?Yn(g,i):null,u=l.getAttribute("method")||Xs,o=Lo(l.getAttribute("enctype"))||Vs,f=new FormData(l)}else if(Ex(l)||Tx(l)&&(l.type==="submit"||l.type==="image")){let g=l.form;if(g==null)throw new Error('Cannot submit a + + + + + + + + {/* Main Content */} +
+
+ +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/generated/anthoLumeAPIV1.ts b/frontend/src/generated/anthoLumeAPIV1.ts new file mode 100644 index 0000000..72752e7 --- /dev/null +++ b/frontend/src/generated/anthoLumeAPIV1.ts @@ -0,0 +1,1414 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import { + useMutation, + useQuery +} from '@tanstack/react-query'; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + MutationFunction, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult +} from '@tanstack/react-query'; + +import * as axios from 'axios'; +import type { + AxiosError, + AxiosRequestConfig, + AxiosResponse +} from 'axios'; + +import type { + ActivityResponse, + CreateDocumentBody, + DocumentResponse, + DocumentsResponse, + ErrorResponse, + GetActivityParams, + GetDocumentsParams, + GetProgressListParams, + GetSearchParams, + GraphDataResponse, + HomeResponse, + LoginRequest, + LoginResponse, + PostSearchBody, + ProgressListResponse, + ProgressResponse, + SearchResponse, + SettingsResponse, + StreaksResponse, + UserStatisticsResponse +} from './model'; + + + + + +/** + * @summary List documents + */ +export const getDocuments = ( + params?: GetDocumentsParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/documents`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetDocumentsQueryKey = (params?: GetDocumentsParams,) => { + return [ + `/api/v1/documents`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetDocumentsQueryOptions = >, TError = AxiosError>(params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentsQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocuments(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentsQueryResult = NonNullable>> +export type GetDocumentsQueryError = AxiosError + + +export function useGetDocuments>, TError = AxiosError>( + params: undefined | GetDocumentsParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocuments>, TError = AxiosError>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocuments>, TError = AxiosError>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary List documents + */ + +export function useGetDocuments>, TError = AxiosError>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentsQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Upload a new document + */ +export const createDocument = ( + createDocumentBody: CreateDocumentBody, options?: AxiosRequestConfig + ): Promise> => { + + const formData = new FormData(); +formData.append(`document_file`, createDocumentBody.document_file) + + return axios.default.post( + `/api/v1/documents`, + formData,options + ); + } + + + +export const getCreateDocumentMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext> => { + +const mutationKey = ['createDocument']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: CreateDocumentBody}> = (props) => { + const {data} = props ?? {}; + + return createDocument(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type CreateDocumentMutationResult = NonNullable>> + export type CreateDocumentMutationBody = CreateDocumentBody + export type CreateDocumentMutationError = AxiosError + + /** + * @summary Upload a new document + */ +export const useCreateDocument = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: CreateDocumentBody}, + TContext + > => { + + const mutationOptions = getCreateDocumentMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary Get a single document + */ +export const getDocument = ( + id: string, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/documents/${id}`,options + ); + } + + + + +export const getGetDocumentQueryKey = (id?: string,) => { + return [ + `/api/v1/documents/${id}` + ] as const; + } + + +export const getGetDocumentQueryOptions = >, TError = AxiosError>(id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocument(id, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentQueryResult = NonNullable>> +export type GetDocumentQueryError = AxiosError + + +export function useGetDocument>, TError = AxiosError>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocument>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocument>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get a single document + */ + +export function useGetDocument>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary List progress records + */ +export const getProgressList = ( + params?: GetProgressListParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/progress`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetProgressListQueryKey = (params?: GetProgressListParams,) => { + return [ + `/api/v1/progress`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetProgressListQueryOptions = >, TError = AxiosError>(params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetProgressListQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getProgressList(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetProgressListQueryResult = NonNullable>> +export type GetProgressListQueryError = AxiosError + + +export function useGetProgressList>, TError = AxiosError>( + params: undefined | GetProgressListParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetProgressList>, TError = AxiosError>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetProgressList>, TError = AxiosError>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary List progress records + */ + +export function useGetProgressList>, TError = AxiosError>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetProgressListQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get document progress + */ +export const getProgress = ( + id: string, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/progress/${id}`,options + ); + } + + + + +export const getGetProgressQueryKey = (id?: string,) => { + return [ + `/api/v1/progress/${id}` + ] as const; + } + + +export const getGetProgressQueryOptions = >, TError = AxiosError>(id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetProgressQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getProgress(id, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetProgressQueryResult = NonNullable>> +export type GetProgressQueryError = AxiosError + + +export function useGetProgress>, TError = AxiosError>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetProgress>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetProgress>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get document progress + */ + +export function useGetProgress>, TError = AxiosError>( + id: string, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetProgressQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get activity data + */ +export const getActivity = ( + params?: GetActivityParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/activity`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetActivityQueryKey = (params?: GetActivityParams,) => { + return [ + `/api/v1/activity`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetActivityQueryOptions = >, TError = AxiosError>(params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetActivityQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getActivity(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetActivityQueryResult = NonNullable>> +export type GetActivityQueryError = AxiosError + + +export function useGetActivity>, TError = AxiosError>( + params: undefined | GetActivityParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetActivity>, TError = AxiosError>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetActivity>, TError = AxiosError>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get activity data + */ + +export function useGetActivity>, TError = AxiosError>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetActivityQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get user settings + */ +export const getSettings = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/settings`,options + ); + } + + + + +export const getGetSettingsQueryKey = () => { + return [ + `/api/v1/settings` + ] as const; + } + + +export const getGetSettingsQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSettingsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSettings({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetSettingsQueryResult = NonNullable>> +export type GetSettingsQueryError = AxiosError + + +export function useGetSettings>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetSettings>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetSettings>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user settings + */ + +export function useGetSettings>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetSettingsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary User login + */ +export const login = ( + loginRequest: LoginRequest, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.post( + `/api/v1/auth/login`, + loginRequest,options + ); + } + + + +export const getLoginMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: LoginRequest}, TContext> => { + +const mutationKey = ['login']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: LoginRequest}> = (props) => { + const {data} = props ?? {}; + + return login(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type LoginMutationResult = NonNullable>> + export type LoginMutationBody = LoginRequest + export type LoginMutationError = AxiosError + + /** + * @summary User login + */ +export const useLogin = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: LoginRequest}, + TContext + > => { + + const mutationOptions = getLoginMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary User logout + */ +export const logout = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.post( + `/api/v1/auth/logout`,undefined,options + ); + } + + + +export const getLogoutMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,void, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,void, TContext> => { + +const mutationKey = ['logout']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, void> = () => { + + + return logout(axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type LogoutMutationResult = NonNullable>> + + export type LogoutMutationError = AxiosError + + /** + * @summary User logout + */ +export const useLogout = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,void, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + void, + TContext + > => { + + const mutationOptions = getLogoutMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary Get current user info + */ +export const getMe = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/auth/me`,options + ); + } + + + + +export const getGetMeQueryKey = () => { + return [ + `/api/v1/auth/me` + ] as const; + } + + +export const getGetMeQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetMeQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getMe({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetMeQueryResult = NonNullable>> +export type GetMeQueryError = AxiosError + + +export function useGetMe>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetMe>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetMe>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get current user info + */ + +export function useGetMe>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetMeQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get home page data + */ +export const getHome = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/home`,options + ); + } + + + + +export const getGetHomeQueryKey = () => { + return [ + `/api/v1/home` + ] as const; + } + + +export const getGetHomeQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetHomeQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getHome({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetHomeQueryResult = NonNullable>> +export type GetHomeQueryError = AxiosError + + +export function useGetHome>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetHome>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetHome>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get home page data + */ + +export function useGetHome>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetHomeQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get user streaks + */ +export const getStreaks = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/home/streaks`,options + ); + } + + + + +export const getGetStreaksQueryKey = () => { + return [ + `/api/v1/home/streaks` + ] as const; + } + + +export const getGetStreaksQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetStreaksQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getStreaks({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetStreaksQueryResult = NonNullable>> +export type GetStreaksQueryError = AxiosError + + +export function useGetStreaks>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetStreaks>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetStreaks>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user streaks + */ + +export function useGetStreaks>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetStreaksQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get daily read stats graph data + */ +export const getGraphData = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/home/graph`,options + ); + } + + + + +export const getGetGraphDataQueryKey = () => { + return [ + `/api/v1/home/graph` + ] as const; + } + + +export const getGetGraphDataQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetGraphDataQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getGraphData({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetGraphDataQueryResult = NonNullable>> +export type GetGraphDataQueryError = AxiosError + + +export function useGetGraphData>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetGraphData>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetGraphData>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get daily read stats graph data + */ + +export function useGetGraphData>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetGraphDataQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get user statistics (leaderboards) + */ +export const getUserStatistics = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/home/statistics`,options + ); + } + + + + +export const getGetUserStatisticsQueryKey = () => { + return [ + `/api/v1/home/statistics` + ] as const; + } + + +export const getGetUserStatisticsQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetUserStatisticsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getUserStatistics({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetUserStatisticsQueryResult = NonNullable>> +export type GetUserStatisticsQueryError = AxiosError + + +export function useGetUserStatistics>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user statistics (leaderboards) + */ + +export function useGetUserStatistics>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetUserStatisticsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Search external book sources + */ +export const getSearch = ( + params: GetSearchParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/search`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetSearchQueryKey = (params?: GetSearchParams,) => { + return [ + `/api/v1/search`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetSearchQueryOptions = >, TError = AxiosError>(params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSearchQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSearch(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetSearchQueryResult = NonNullable>> +export type GetSearchQueryError = AxiosError + + +export function useGetSearch>, TError = AxiosError>( + params: GetSearchParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetSearch>, TError = AxiosError>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetSearch>, TError = AxiosError>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Search external book sources + */ + +export function useGetSearch>, TError = AxiosError>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetSearchQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Download search result + */ +export const postSearch = ( + postSearchBody: PostSearchBody, options?: AxiosRequestConfig + ): Promise> => { + + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`source`, postSearchBody.source) +formUrlEncoded.append(`title`, postSearchBody.title) +formUrlEncoded.append(`author`, postSearchBody.author) +formUrlEncoded.append(`id`, postSearchBody.id) + + return axios.default.post( + `/api/v1/search`, + formUrlEncoded,options + ); + } + + + +export const getPostSearchMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostSearchBody}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: PostSearchBody}, TContext> => { + +const mutationKey = ['postSearch']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostSearchBody}> = (props) => { + const {data} = props ?? {}; + + return postSearch(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type PostSearchMutationResult = NonNullable>> + export type PostSearchMutationBody = PostSearchBody + export type PostSearchMutationError = AxiosError + + /** + * @summary Download search result + */ +export const usePostSearch = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostSearchBody}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostSearchBody}, + TContext + > => { + + const mutationOptions = getPostSearchMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + diff --git a/frontend/src/generated/model/activity.ts b/frontend/src/generated/model/activity.ts new file mode 100644 index 0000000..bc38a8b --- /dev/null +++ b/frontend/src/generated/model/activity.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Activity { + id: string; + user_id: string; + document_id: string; + activity_type: string; + timestamp: string; +} diff --git a/frontend/src/generated/model/activityResponse.ts b/frontend/src/generated/model/activityResponse.ts new file mode 100644 index 0000000..1cf8a88 --- /dev/null +++ b/frontend/src/generated/model/activityResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Activity } from './activity'; +import type { UserData } from './userData'; + +export interface ActivityResponse { + activities: Activity[]; + user: UserData; +} diff --git a/frontend/src/generated/model/createDocumentBody.ts b/frontend/src/generated/model/createDocumentBody.ts new file mode 100644 index 0000000..e8e372e --- /dev/null +++ b/frontend/src/generated/model/createDocumentBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type CreateDocumentBody = { + document_file: Blob; +}; diff --git a/frontend/src/generated/model/databaseInfo.ts b/frontend/src/generated/model/databaseInfo.ts new file mode 100644 index 0000000..9f87f69 --- /dev/null +++ b/frontend/src/generated/model/databaseInfo.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface DatabaseInfo { + documents_size: number; + activity_size: number; + progress_size: number; + devices_size: number; +} diff --git a/frontend/src/generated/model/device.ts b/frontend/src/generated/model/device.ts new file mode 100644 index 0000000..41f5d3a --- /dev/null +++ b/frontend/src/generated/model/device.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Device { + id?: string; + device_name?: string; + created_at?: string; + last_synced?: string; +} diff --git a/frontend/src/generated/model/document.ts b/frontend/src/generated/model/document.ts new file mode 100644 index 0000000..baf44d5 --- /dev/null +++ b/frontend/src/generated/model/document.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Document { + id: string; + title: string; + author: string; + created_at: string; + updated_at: string; + deleted: boolean; + words?: number; + filepath?: string; + percentage?: number; + total_time_seconds?: number; +} diff --git a/frontend/src/generated/model/documentResponse.ts b/frontend/src/generated/model/documentResponse.ts new file mode 100644 index 0000000..871fbb3 --- /dev/null +++ b/frontend/src/generated/model/documentResponse.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Document } from './document'; +import type { UserData } from './userData'; +import type { Progress } from './progress'; + +export interface DocumentResponse { + document: Document; + user: UserData; + progress?: Progress; +} diff --git a/frontend/src/generated/model/documentsResponse.ts b/frontend/src/generated/model/documentsResponse.ts new file mode 100644 index 0000000..d14146f --- /dev/null +++ b/frontend/src/generated/model/documentsResponse.ts @@ -0,0 +1,22 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Document } from './document'; +import type { UserData } from './userData'; +import type { WordCount } from './wordCount'; + +export interface DocumentsResponse { + documents: Document[]; + total: number; + page: number; + limit: number; + next_page?: number; + previous_page?: number; + search?: string; + user: UserData; + word_counts: WordCount[]; +} diff --git a/frontend/src/generated/model/errorResponse.ts b/frontend/src/generated/model/errorResponse.ts new file mode 100644 index 0000000..780977d --- /dev/null +++ b/frontend/src/generated/model/errorResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface ErrorResponse { + code: number; + message: string; +} diff --git a/frontend/src/generated/model/getActivityParams.ts b/frontend/src/generated/model/getActivityParams.ts new file mode 100644 index 0000000..0645839 --- /dev/null +++ b/frontend/src/generated/model/getActivityParams.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetActivityParams = { +doc_filter?: boolean; +document_id?: string; +offset?: number; +limit?: number; +}; diff --git a/frontend/src/generated/model/getDocumentsParams.ts b/frontend/src/generated/model/getDocumentsParams.ts new file mode 100644 index 0000000..959f495 --- /dev/null +++ b/frontend/src/generated/model/getDocumentsParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetDocumentsParams = { +page?: number; +limit?: number; +search?: string; +}; diff --git a/frontend/src/generated/model/getProgressListParams.ts b/frontend/src/generated/model/getProgressListParams.ts new file mode 100644 index 0000000..cfdfc7d --- /dev/null +++ b/frontend/src/generated/model/getProgressListParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetProgressListParams = { +page?: number; +limit?: number; +document?: string; +}; diff --git a/frontend/src/generated/model/getSearchParams.ts b/frontend/src/generated/model/getSearchParams.ts new file mode 100644 index 0000000..e9f91ed --- /dev/null +++ b/frontend/src/generated/model/getSearchParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { GetSearchSource } from './getSearchSource'; + +export type GetSearchParams = { +query: string; +source: GetSearchSource; +}; diff --git a/frontend/src/generated/model/getSearchSource.ts b/frontend/src/generated/model/getSearchSource.ts new file mode 100644 index 0000000..441f515 --- /dev/null +++ b/frontend/src/generated/model/getSearchSource.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetSearchSource = typeof GetSearchSource[keyof typeof GetSearchSource]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const GetSearchSource = { + LibGen: 'LibGen', + Annas_Archive: 'Annas Archive', +} as const; diff --git a/frontend/src/generated/model/graphDataPoint.ts b/frontend/src/generated/model/graphDataPoint.ts new file mode 100644 index 0000000..4d477fb --- /dev/null +++ b/frontend/src/generated/model/graphDataPoint.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface GraphDataPoint { + date: string; + minutes_read: number; +} diff --git a/frontend/src/generated/model/graphDataResponse.ts b/frontend/src/generated/model/graphDataResponse.ts new file mode 100644 index 0000000..00d2b28 --- /dev/null +++ b/frontend/src/generated/model/graphDataResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { GraphDataPoint } from './graphDataPoint'; +import type { UserData } from './userData'; + +export interface GraphDataResponse { + graph_data: GraphDataPoint[]; + user: UserData; +} diff --git a/frontend/src/generated/model/homeResponse.ts b/frontend/src/generated/model/homeResponse.ts new file mode 100644 index 0000000..537f66b --- /dev/null +++ b/frontend/src/generated/model/homeResponse.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { DatabaseInfo } from './databaseInfo'; +import type { StreaksResponse } from './streaksResponse'; +import type { GraphDataResponse } from './graphDataResponse'; +import type { UserStatisticsResponse } from './userStatisticsResponse'; +import type { UserData } from './userData'; + +export interface HomeResponse { + database_info: DatabaseInfo; + streaks: StreaksResponse; + graph_data: GraphDataResponse; + user_statistics: UserStatisticsResponse; + user: UserData; +} diff --git a/frontend/src/generated/model/index.ts b/frontend/src/generated/model/index.ts new file mode 100644 index 0000000..4bfa505 --- /dev/null +++ b/frontend/src/generated/model/index.ts @@ -0,0 +1,42 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export * from './activity'; +export * from './activityResponse'; +export * from './createDocumentBody'; +export * from './databaseInfo'; +export * from './device'; +export * from './document'; +export * from './documentResponse'; +export * from './documentsResponse'; +export * from './errorResponse'; +export * from './getActivityParams'; +export * from './getDocumentsParams'; +export * from './getProgressListParams'; +export * from './getSearchParams'; +export * from './getSearchSource'; +export * from './graphDataPoint'; +export * from './graphDataResponse'; +export * from './homeResponse'; +export * from './leaderboardData'; +export * from './leaderboardEntry'; +export * from './loginRequest'; +export * from './loginResponse'; +export * from './postSearchBody'; +export * from './progress'; +export * from './progressListResponse'; +export * from './progressResponse'; +export * from './searchItem'; +export * from './searchResponse'; +export * from './setting'; +export * from './settingsResponse'; +export * from './streaksResponse'; +export * from './userData'; +export * from './userStatisticsResponse'; +export * from './userStreak'; +export * from './wordCount'; \ No newline at end of file diff --git a/frontend/src/generated/model/leaderboardData.ts b/frontend/src/generated/model/leaderboardData.ts new file mode 100644 index 0000000..fb020fb --- /dev/null +++ b/frontend/src/generated/model/leaderboardData.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { LeaderboardEntry } from './leaderboardEntry'; + +export interface LeaderboardData { + all: LeaderboardEntry[]; + year: LeaderboardEntry[]; + month: LeaderboardEntry[]; + week: LeaderboardEntry[]; +} diff --git a/frontend/src/generated/model/leaderboardEntry.ts b/frontend/src/generated/model/leaderboardEntry.ts new file mode 100644 index 0000000..d7f885f --- /dev/null +++ b/frontend/src/generated/model/leaderboardEntry.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LeaderboardEntry { + user_id: string; + value: number; +} diff --git a/frontend/src/generated/model/loginRequest.ts b/frontend/src/generated/model/loginRequest.ts new file mode 100644 index 0000000..61a2463 --- /dev/null +++ b/frontend/src/generated/model/loginRequest.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LoginRequest { + username: string; + password: string; +} diff --git a/frontend/src/generated/model/loginResponse.ts b/frontend/src/generated/model/loginResponse.ts new file mode 100644 index 0000000..9b418c7 --- /dev/null +++ b/frontend/src/generated/model/loginResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LoginResponse { + username: string; + is_admin: boolean; +} diff --git a/frontend/src/generated/model/postSearchBody.ts b/frontend/src/generated/model/postSearchBody.ts new file mode 100644 index 0000000..213b47d --- /dev/null +++ b/frontend/src/generated/model/postSearchBody.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type PostSearchBody = { + source: string; + title: string; + author: string; + id: string; +}; diff --git a/frontend/src/generated/model/progress.ts b/frontend/src/generated/model/progress.ts new file mode 100644 index 0000000..628253e --- /dev/null +++ b/frontend/src/generated/model/progress.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Progress { + title?: string; + author?: string; + device_name?: string; + percentage?: number; + document_id?: string; + user_id?: string; + created_at?: string; +} diff --git a/frontend/src/generated/model/progressListResponse.ts b/frontend/src/generated/model/progressListResponse.ts new file mode 100644 index 0000000..4f951e5 --- /dev/null +++ b/frontend/src/generated/model/progressListResponse.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Progress } from './progress'; +import type { UserData } from './userData'; + +export interface ProgressListResponse { + progress?: Progress[]; + user?: UserData; + page?: number; + limit?: number; + next_page?: number; + previous_page?: number; + total?: number; +} diff --git a/frontend/src/generated/model/progressResponse.ts b/frontend/src/generated/model/progressResponse.ts new file mode 100644 index 0000000..49b3473 --- /dev/null +++ b/frontend/src/generated/model/progressResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Progress } from './progress'; +import type { UserData } from './userData'; + +export interface ProgressResponse { + progress?: Progress; + user?: UserData; +} diff --git a/frontend/src/generated/model/searchItem.ts b/frontend/src/generated/model/searchItem.ts new file mode 100644 index 0000000..132a26e --- /dev/null +++ b/frontend/src/generated/model/searchItem.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface SearchItem { + id?: string; + title?: string; + author?: string; + language?: string; + series?: string; + file_type?: string; + file_size?: string; + upload_date?: string; +} diff --git a/frontend/src/generated/model/searchResponse.ts b/frontend/src/generated/model/searchResponse.ts new file mode 100644 index 0000000..fff7569 --- /dev/null +++ b/frontend/src/generated/model/searchResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { SearchItem } from './searchItem'; + +export interface SearchResponse { + results: SearchItem[]; + source: string; + query: string; +} diff --git a/frontend/src/generated/model/setting.ts b/frontend/src/generated/model/setting.ts new file mode 100644 index 0000000..f2f5622 --- /dev/null +++ b/frontend/src/generated/model/setting.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Setting { + id: string; + user_id: string; + key: string; + value: string; +} diff --git a/frontend/src/generated/model/settingsResponse.ts b/frontend/src/generated/model/settingsResponse.ts new file mode 100644 index 0000000..23eecde --- /dev/null +++ b/frontend/src/generated/model/settingsResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { UserData } from './userData'; +import type { Device } from './device'; + +export interface SettingsResponse { + user: UserData; + timezone?: string; + devices?: Device[]; +} diff --git a/frontend/src/generated/model/streaksResponse.ts b/frontend/src/generated/model/streaksResponse.ts new file mode 100644 index 0000000..4683eb5 --- /dev/null +++ b/frontend/src/generated/model/streaksResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { UserStreak } from './userStreak'; +import type { UserData } from './userData'; + +export interface StreaksResponse { + streaks: UserStreak[]; + user: UserData; +} diff --git a/frontend/src/generated/model/userData.ts b/frontend/src/generated/model/userData.ts new file mode 100644 index 0000000..24888d2 --- /dev/null +++ b/frontend/src/generated/model/userData.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UserData { + username: string; + is_admin: boolean; +} diff --git a/frontend/src/generated/model/userStatisticsResponse.ts b/frontend/src/generated/model/userStatisticsResponse.ts new file mode 100644 index 0000000..b0ede9c --- /dev/null +++ b/frontend/src/generated/model/userStatisticsResponse.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { LeaderboardData } from './leaderboardData'; +import type { UserData } from './userData'; + +export interface UserStatisticsResponse { + wpm: LeaderboardData; + duration: LeaderboardData; + words: LeaderboardData; + user: UserData; +} diff --git a/frontend/src/generated/model/userStreak.ts b/frontend/src/generated/model/userStreak.ts new file mode 100644 index 0000000..d636fff --- /dev/null +++ b/frontend/src/generated/model/userStreak.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UserStreak { + window: string; + max_streak: number; + max_streak_start_date: string; + max_streak_end_date: string; + current_streak: number; + current_streak_start_date: string; + current_streak_end_date: string; +} diff --git a/frontend/src/generated/model/wordCount.ts b/frontend/src/generated/model/wordCount.ts new file mode 100644 index 0000000..1497872 --- /dev/null +++ b/frontend/src/generated/model/wordCount.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface WordCount { + document_id: string; + count: number; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..c45a651 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,46 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* PWA Styling */ +html, +body { + overscroll-behavior-y: none; + margin: 0px; +} + +html { + height: calc(100% + env(safe-area-inset-bottom)); + padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 + env(safe-area-inset-left); +} + +main { + height: calc(100dvh - 4rem - env(safe-area-inset-top)); +} + +#container { + padding-bottom: calc(5em + env(safe-area-inset-bottom) * 2); +} + +/* No Scrollbar - IE, Edge, Firefox */ +* { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* No Scrollbar - WebKit */ +*::-webkit-scrollbar { + display: none; +} + +/* Button visibility toggle */ +.css-button:checked + div { + visibility: visible; + opacity: 1; +} + +.css-button + div { + visibility: hidden; + opacity: 0; +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..9c9c12c --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import './auth/authInterceptor'; +import App from './App'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 1, + }, + mutations: { + retry: 0, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +); \ No newline at end of file diff --git a/frontend/src/pages/ActivityPage.tsx b/frontend/src/pages/ActivityPage.tsx new file mode 100644 index 0000000..0021ab8 --- /dev/null +++ b/frontend/src/pages/ActivityPage.tsx @@ -0,0 +1,43 @@ +import { useGetActivity } from '../generated/anthoLumeAPIV1'; + +export default function ActivityPage() { + const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 }); + const activities = data?.data?.activities; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+ + + + + + + + + + {activities?.map((activity: any) => ( + + + + + + ))} + +
Activity TypeDocumentTimestamp
+ {activity.activity_type} + + + {activity.document_id} + + + {new Date(activity.timestamp).toLocaleString()} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/DocumentPage.tsx b/frontend/src/pages/DocumentPage.tsx new file mode 100644 index 0000000..75ca0de --- /dev/null +++ b/frontend/src/pages/DocumentPage.tsx @@ -0,0 +1,111 @@ +import { useParams } from 'react-router-dom'; +import { useGetDocument, useGetProgress } from '../generated/anthoLumeAPIV1'; + +export default function DocumentPage() { + const { id } = useParams<{ id: string }>(); + + const { data: docData, isLoading: docLoading } = useGetDocument(id || ''); + + const { data: progressData, isLoading: progressLoading } = useGetProgress(id || ''); + + if (docLoading || progressLoading) { + return
Loading...
; + } + + const document = docData?.data?.document; + const progress = progressData?.data; + + if (!document) { + return
Document not found
; + } + + return ( +
+
+ {/* Document Info */} +
+
+ {/* Cover image placeholder */} +
+ No Cover +
+
+ + + Read + + +
+
+
+

Words:

+

{document.words || 'N/A'}

+
+
+
+
+ + {/* Document Details Grid */} +
+
+

Title

+

{document.title}

+
+
+

Author

+

{document.author}

+
+
+

Time Read

+

+ {progress?.progress?.percentage ? `${Math.round(progress.progress.percentage)}%` : '0%'} +

+
+
+

Progress

+

+ {progress?.progress?.percentage ? `${Math.round(progress.progress.percentage)}%` : '0%'} +

+
+
+ + {/* Description */} +
+
+

Description

+
+
+

N/A

+
+
+ + {/* Stats */} +
+
+

Words

+

{document.words || 'N/A'}

+
+
+

Created

+

+ {new Date(document.created_at).toLocaleDateString()} +

+
+
+

Updated

+

+ {new Date(document.updated_at).toLocaleDateString()} +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/DocumentsPage.tsx b/frontend/src/pages/DocumentsPage.tsx new file mode 100644 index 0000000..916071e --- /dev/null +++ b/frontend/src/pages/DocumentsPage.tsx @@ -0,0 +1,312 @@ +import { useState, FormEvent, useRef } from 'react'; +import { Link } from 'react-router-dom'; +import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1'; + +interface DocumentCardProps { + doc: { + id: string; + title: string; + author: string; + created_at: string; + deleted: boolean; + words?: number; + filepath?: string; + percentage?: number; + total_time_seconds?: number; + }; +} + +// Activity icon SVG +function ActivityIcon() { + return ( + + + + ); +} + +// Download icon SVG +function DownloadIcon({ disabled }: { disabled?: boolean }) { + if (disabled) { + return ( + + + + + + ); + } + return ( + + + + + ); +} + +function DocumentCard({ doc }: DocumentCardProps) { + const percentage = doc.percentage || 0; + const totalTimeSeconds = doc.total_time_seconds || 0; + + // Convert seconds to nice format (e.g., "2h 30m") + const niceSeconds = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + if (hours > 0) { + return `${hours}h ${minutes}m`; + } + return `${minutes}m`; + }; + + return ( +
+
+
+ + {doc.title} + +
+
+
+
+

Title

+

{doc.title || "Unknown"}

+
+
+
+
+

Author

+

{doc.author || "Unknown"}

+
+
+
+
+

Progress

+

{percentage}%

+
+
+
+
+

Time Read

+

{niceSeconds(totalTimeSeconds)}

+
+
+
+
+ + + + {doc.filepath ? ( + + + + ) : ( + + )} +
+
+
+ ); +} + +// Search icon SVG +function SearchIcon() { + return ( + + + + + ); +} + +// Upload icon SVG +function UploadIcon() { + return ( + + + + + + ); +} + +export default function DocumentsPage() { + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + const [limit] = useState(9); + const [uploadMode, setUploadMode] = useState(false); + const fileInputRef = useRef(null); + + const { data, isLoading, refetch } = useGetDocuments({ page, limit, search }); + const createMutation = useCreateDocument(); + const docs = data?.data?.documents; + const previousPage = data?.data?.previous_page; + const nextPage = data?.data?.next_page; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.name.endsWith('.epub')) { + alert('Please upload an EPUB file'); + return; + } + + try { + await createMutation.mutateAsync({ + data: { + document_file: file, + }, + }); + alert('Document uploaded successfully!'); + setUploadMode(false); + refetch(); + } catch (error) { + console.error('Upload failed:', error); + alert('Failed to upload document'); + } + }; + + const handleCancelUpload = () => { + setUploadMode(false); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+ {/* Search Form */} +
+ +
+
+ + + + setSearch(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="Search Author / Title" + name="search" + /> +
+
+
+ +
+ +
+ + {/* Document Grid */} +
+ {docs?.map((doc: any) => ( + + ))} +
+ + {/* Pagination */} +
+ {previousPage && previousPage > 0 && ( + + )} + {nextPage && nextPage > 0 && ( + + )} +
+ + {/* Upload Button */} +
+ setUploadMode(!uploadMode)} + /> +
+
+ + +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..d13ddd9 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,262 @@ +import { Link } from 'react-router-dom'; +import { useGetHome, useGetDocuments } from '../generated/anthoLumeAPIV1'; +import type { GraphDataPoint, LeaderboardData } from '../generated/model'; + +interface InfoCardProps { + title: string; + size: string | number; + link?: string; +} + +function InfoCard({ title, size, link }: InfoCardProps) { + if (link) { + return ( + +
+
+

{size}

+

{title}

+
+
+ + ); + } + + return ( +
+
+
+

{size}

+

{title}

+
+
+
+ ); +} + +interface StreakCardProps { + window: 'DAY' | 'WEEK'; + currentStreak: number; + currentStreakStartDate: string; + currentStreakEndDate: string; + maxStreak: number; + maxStreakStartDate: string; + maxStreakEndDate: string; +} + +function StreakCard({ window, currentStreak, currentStreakStartDate, currentStreakEndDate, maxStreak, maxStreakStartDate, maxStreakEndDate }: StreakCardProps) { + return ( +
+
+

+ {window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'} +

+
+

{currentStreak}

+
+
+
+
+

{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}

+
+ {currentStreakStartDate} ➞ {currentStreakEndDate} +
+
+
{currentStreak}
+
+
+
+

{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}

+
+ {maxStreakStartDate} ➞ {maxStreakEndDate} +
+
+
{maxStreak}
+
+
+
+
+ ); +} + +interface LeaderboardCardProps { + name: string; + data: LeaderboardData; +} + +function LeaderboardCard({ name, data }: LeaderboardCardProps) { + return ( +
+
+
+
+

+ {name} Leaderboard +

+
+ all + year + month + week +
+
+
+ + {/* All time data */} +
+ {data.all.length === 0 ? ( +

N/A

+ ) : ( +

{data.all[0]?.user_id || 'N/A'}

+ )} +
+ +
+ {data.all.slice(0, 3).map((item: any, index: number) => ( +
0 ? 'border-t border-gray-200' : ''}`} + > +
+

{item.user_id}

+
+
{item.value}
+
+ ))} +
+
+
+ ); +} + +function GraphVisualization({ data }: { data: GraphDataPoint[] }) { + if (!data || data.length === 0) { + return ( +
+

No data available

+
+ ); + } + + // Simple bar visualization (could be enhanced with SVG bezier curve like SSR) + const maxMinutes = Math.max(...data.map(d => d.minutes_read), 1); + + return ( +
+ {data.map((point, i) => ( +
+
+ {point.minutes_read} min +
+
+ ))} +
+ ); +} + +export default function HomePage() { + const { data: homeData, isLoading: homeLoading } = useGetHome(); + const { data: docsData, isLoading: docsLoading } = useGetDocuments({ page: 1, limit: 9 }); + + const docs = docsData?.data?.documents; + const dbInfo = homeData?.data?.database_info; + const streaks = homeData?.data?.streaks?.streaks; + const graphData = homeData?.data?.graph_data?.graph_data; + const userStats = homeData?.data?.user_statistics; + + if (homeLoading || docsLoading) { + return
Loading...
; + } + + return ( +
+ {/* Daily Read Totals Graph */} +
+
+

+ Daily Read Totals +

+ +
+
+ + {/* Info Cards */} +
+ + + + +
+ + {/* Streak Cards */} +
+ {streaks?.map((streak: any, index) => ( + + ))} +
+ + {/* Leaderboard Cards */} +
+ + + +
+ + {/* Recent Documents */} +
+ {docs?.slice(0, 6).map((doc: any) => ( +
+

{doc.title}

+

{doc.author}

+ + View Document + +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..8adbea4 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,87 @@ +import { useState, FormEvent } from 'react'; +import { useAuth } from '../auth/AuthContext'; + +export default function LoginPage() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { login } = useAuth(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + await login(username, password); + } catch (err) { + setError('Invalid credentials'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+

Welcome.

+
+
+
+ setUsername(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="Username" + required + disabled={isLoading} + /> +
+
+
+
+ setPassword(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="Password" + required + disabled={isLoading} + /> + {error} +
+
+ +
+ +
+
+
+
+ AnthoLume +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/ProgressPage.tsx b/frontend/src/pages/ProgressPage.tsx new file mode 100644 index 0000000..b227e85 --- /dev/null +++ b/frontend/src/pages/ProgressPage.tsx @@ -0,0 +1,51 @@ +import { Link } from 'react-router-dom'; +import { useGetProgressList } from '../generated/anthoLumeAPIV1'; + +export default function ProgressPage() { + const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 }); + const progress = data?.data?.progress; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+ + + + + + + + + + + {progress?.map((row: any) => ( + + + + + + + ))} + +
DocumentDevice NamePercentageCreated At
+ + {row.author || 'Unknown'} - {row.title || 'Unknown'} + + + {row.device_name || 'Unknown'} + + {row.percentage ? Math.round(row.percentage) : 0}% + + {row.created_at ? new Date(row.created_at).toLocaleDateString() : 'N/A'} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx new file mode 100644 index 0000000..aed5d95 --- /dev/null +++ b/frontend/src/pages/SearchPage.tsx @@ -0,0 +1,183 @@ +import { useState, FormEvent } from 'react'; +import { useGetSearch } from '../generated/anthoLumeAPIV1'; +import { GetSearchSource } from '../generated/model/getSearchSource'; + +// Search icon SVG +function SearchIcon() { + return ( + + + + + ); +} + +// Documents icon SVG +function DocumentsIcon() { + return ( + + + + + + + + ); +} + +// Download icon SVG +function DownloadIcon() { + return ( + + + + + ); +} + +export default function SearchPage() { + const [query, setQuery] = useState(''); + const [source, setSource] = useState(GetSearchSource.LibGen); + + const { data, isLoading } = useGetSearch({ query, source }); + const results = data?.data?.results; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + // Trigger refetch by updating query + }; + + return ( +
+
+ {/* Search Form */} +
+
+
+
+ + + + setQuery(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="Query" + /> +
+
+
+ + + + +
+
+ +
+
+
+ + {/* Search Results Table */} +
+ + + + + + + + + + + + + {isLoading && ( + + + + )} + {!isLoading && !results && ( + + + + )} + {!isLoading && results && results.map((item: any) => ( + + + + + + + + + ))} + +
+ Document + + Series + + Type + + Size + + Date +
Loading...
No Results
+ + + {item.author || 'N/A'} - {item.title || 'N/A'} + +

{item.series || 'N/A'}

+
+

{item.file_type || 'N/A'}

+
+

{item.file_size || 'N/A'}

+
+

{item.upload_date || 'N/A'}

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..5b9a086 --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,215 @@ +import { useState, FormEvent } from 'react'; +import { useGetSettings } from '../generated/anthoLumeAPIV1'; + +// User icon SVG +function UserIcon() { + return ( + + + + + ); +} + +// Password icon SVG +function PasswordIcon() { + return ( + + + + + ); +} + +// Clock icon SVG +function ClockIcon() { + return ( + + + + + ); +} + +export default function SettingsPage() { + const { data, isLoading } = useGetSettings(); + const settingsData = data?.data; + + const [password, setPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [timezone, setTimezone] = useState(settingsData?.timezone || ''); + + const handlePasswordSubmit = (e: FormEvent) => { + e.preventDefault(); + // TODO: Call API to change password + }; + + const handleTimezoneSubmit = (e: FormEvent) => { + e.preventDefault(); + // TODO: Call API to change timezone + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+ {/* User Profile Card */} +
+
+ +

{settingsData?.user?.username}

+
+
+ +
+ {/* Change Password Form */} +
+

Change Password

+
+
+
+ + + + setPassword(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="Password" + /> +
+
+
+
+ + + + setNewPassword(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="New Password" + /> +
+
+
+ +
+
+
+ + {/* Change Timezone Form */} +
+

Change Timezone

+
+
+ + + + +
+
+ +
+
+
+ + {/* Devices Table */} +
+

Devices

+ + + + + + + + + + {!settingsData?.devices || settingsData.devices.length === 0 ? ( + + + + ) : ( + settingsData.devices.map((device: any) => ( + + + + + + )) + )} + +
+ Name + + Last Sync + + Created +
No Results
+

{device.device_name || 'Unknown'}

+
+

{device.last_synced ? new Date(device.last_synced).toLocaleString() : 'N/A'}

+
+

{device.created_at ? new Date(device.created_at).toLocaleString() : 'N/A'}

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..fe6e470 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,ts,jsx,tsx}'], + darkMode: 'class', + theme: { + extend: {}, + }, + plugins: [], +}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..270518a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Module resolution options */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* JSX support */ + "jsx": "react-jsx", + + /* Strict type checking */ + "strict": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..4ba2d25 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8585', + changeOrigin: true, + }, + '/assets': { + target: 'http://localhost:8585', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + }, +});