From eb9e57396fc39fb0ee0e7e0f454e7f6786b0dc64 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 16 Jun 2025 15:57:33 +0530 Subject: [PATCH 1/6] feat: setup docker for production --- .github/workflows/docker.yml | 6 +++- .github/workflows/frontend-test.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/prettier.yml | 2 +- docker-compose.yml | 4 +-- production/docker-compose.yml | 48 +++++++++++++++++++++++++++++ production/example.backend.env | 7 +++++ 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 production/docker-compose.yml create mode 100644 production/example.backend.env diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a571210c..2c238d4d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,7 @@ name: Build and Push Docker Images to GHCR on: push: branches: - - main + - docker-prod jobs: build-and-push-frontend: @@ -36,6 +36,10 @@ jobs: ghcr.io/ccextractor/frontend:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + VITE_BACKEND_URL=http://localhost:8000 + VITE_FRONTEND_URL=http://localhost:80 + VITE_CONTAINER_ORIGIN=http://localhost:8080 build-and-push-backend: runs-on: ubuntu-latest diff --git a/.github/workflows/frontend-test.yml b/.github/workflows/frontend-test.yml index 9670bc1e..9766054d 100644 --- a/.github/workflows/frontend-test.yml +++ b/.github/workflows/frontend-test.yml @@ -5,7 +5,7 @@ on: - "main" pull_request: branches: - - "*" + - ["main", "dev"] jobs: prettier: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5ec302a4..4cd9eebe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: - "main" pull_request: branches: - - "*" + - ["main", "dev"] jobs: gofmt: diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 7bd57e3e..1210d5df 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -5,7 +5,7 @@ on: - "main" pull_request: branches: - - "*" + - ["main", "dev"] jobs: prettier: runs-on: ubuntu-latest diff --git a/docker-compose.yml b/docker-compose.yml index 6973b92f..fa528610 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: depends_on: - backend env_file: - - ./.frontend.env + - ./frontend/.env healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80"] interval: 30s @@ -28,7 +28,7 @@ services: depends_on: - syncserver env_file: - - ./.backend.env + - ./backend/.env healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s diff --git a/production/docker-compose.yml b/production/docker-compose.yml new file mode 100644 index 00000000..434216b5 --- /dev/null +++ b/production/docker-compose.yml @@ -0,0 +1,48 @@ +services: + frontend: + image: ghcr.io/ccextractor/frontend:latest + ports: + - "80:80" + networks: + - tasknetwork + depends_on: + - backend + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + + backend: + image: ghcr.io/ccextractor/backend:latest + ports: + - "8000:8000" + networks: + - tasknetwork + depends_on: + - syncserver + env_file: + - .backend.env + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + volumes: + - ./backend/data:/app/data + + syncserver: + image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:latest + ports: + - "8080:8080" + networks: + - tasknetwork + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + tasknetwork: + driver: bridge diff --git a/production/example.backend.env b/production/example.backend.env new file mode 100644 index 00000000..2e3a01db --- /dev/null +++ b/production/example.backend.env @@ -0,0 +1,7 @@ +REDIRECT_URL_DEV="http://localhost:8000/auth/callback" +SESSION_KEY="Random key" +CLIENT_SEC="Via Google Oauth" +CLIENT_ID="Via Google Oauth" +FRONTEND_ORIGIN_DEV="http://localhost" +CONTAINER_ORIGIN="http://ccsync-syncserver-1:8080/" +PORT=8000 From 18739b2054046fabeb4d579c51dfbdf13ed97512 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 16 Jun 2025 16:02:30 +0530 Subject: [PATCH 2/6] fix: update docker-prod testing condition --- frontend/src/components/utils/URLs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/utils/URLs.ts b/frontend/src/components/utils/URLs.ts index d307d9bb..290c1eca 100644 --- a/frontend/src/components/utils/URLs.ts +++ b/frontend/src/components/utils/URLs.ts @@ -1,4 +1,4 @@ -const isTesting = true; +const isTesting = false; export const url = isTesting ? { From 153bc7af7afae8407b554a03b89adf94f6a179cc Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 16 Jun 2025 16:06:55 +0530 Subject: [PATCH 3/6] fix: update URLs.ts for passing build args --- frontend/src/components/utils/URLs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/utils/URLs.ts b/frontend/src/components/utils/URLs.ts index 290c1eca..f657742c 100644 --- a/frontend/src/components/utils/URLs.ts +++ b/frontend/src/components/utils/URLs.ts @@ -12,9 +12,9 @@ export const url = isTesting taskchampionSyncServerURL: '', } : { - backendURL: import.meta.env.VITE_BACKEND_URL, - frontendURL: import.meta.env.VITE_FRONTEND_URL, - containerOrigin: import.meta.env.VITE_CONTAINER_ORIGIN, + backendURL: 'http://localhost:8000/', + frontendURL: 'http://localhost:80', + containerOrigin: 'http://localhost:8080/', githubRepoURL: 'https://github.com/CCExtractor/ccsync', githubDocsURL: 'https://its-me-abhishek.github.io/ccsync-docs/', zulipURL: 'https://ccextractor.org/public/general/support/', From bc8c9aa1ad2600c5bea1e60cd2887474693fa54c Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 16 Jun 2025 16:17:01 +0530 Subject: [PATCH 4/6] feat: prepare Docker images for production --- production/example.backend.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/example.backend.env b/production/example.backend.env index 2e3a01db..e01b533f 100644 --- a/production/example.backend.env +++ b/production/example.backend.env @@ -3,5 +3,5 @@ SESSION_KEY="Random key" CLIENT_SEC="Via Google Oauth" CLIENT_ID="Via Google Oauth" FRONTEND_ORIGIN_DEV="http://localhost" -CONTAINER_ORIGIN="http://ccsync-syncserver-1:8080/" +CONTAINER_ORIGIN="http://production-syncserver-1:8080/" PORT=8000 From 591935b2806ef92792820a2f600c694e12b5850e Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 2 Jul 2025 23:10:36 +0530 Subject: [PATCH 5/6] feat: update workflow working --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2c238d4d..dd7af716 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,7 @@ name: Build and Push Docker Images to GHCR on: push: branches: - - docker-prod + - main jobs: build-and-push-frontend: From 2ba1b3de39c2036d37bbf89c36e9910d6c7cc1a8 Mon Sep 17 00:00:00 2001 From: Bhoomi Mahna Date: Sat, 27 Dec 2025 11:47:05 +0530 Subject: [PATCH 6/6] feat: add pin/unpin option to keep important tasks at top --- backend/controllers/modify_task.go | 51 +++++ backend/main.go | 2 +- backend/models/request_body.go | 1 + backend/models/task.go | 1 + backend/utils/tw/add_task.go | 1 + backend/utils/tw/modify_task.go | 59 ++++- frontend/package-lock.json | 212 +++++++++++++++++- frontend/package.json | 1 + .../components/HomeComponents/Tasks/Tasks.tsx | 133 ++++++++--- .../HomeComponents/Tasks/tasks-utils.ts | 42 ++++ frontend/src/components/utils/types.ts | 1 + package-lock.json | 6 + production/example.backend.env | 9 +- 13 files changed, 475 insertions(+), 44 deletions(-) create mode 100644 package-lock.json diff --git a/backend/controllers/modify_task.go b/backend/controllers/modify_task.go index f4993aea..a8e1332f 100644 --- a/backend/controllers/modify_task.go +++ b/backend/controllers/modify_task.go @@ -64,3 +64,54 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { } http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) } +func TogglePinHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Invalid request method; POST required", http.StatusMethodNotAllowed) + return + } + + var payload struct { + Email string `json:"email"` + EncryptionSecret string `json:"encryptionSecret"` + UserUUID string `json:"UUID"` + TaskUUID string `json:"taskuuid"` + TaskID string `json:"taskid"` + IsPinned *bool `json:"isPinned"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest) + return + } + + taskUUID := payload.TaskUUID + if taskUUID == "" { + taskUUID = payload.TaskID + } + + if taskUUID == "" { + http.Error(w, "missing task identifier (taskuuid/taskid)", http.StatusBadRequest) + return + } + if payload.Email == "" || payload.EncryptionSecret == "" || payload.UserUUID == "" { + http.Error(w, "missing email, encryptionSecret or UUID", http.StatusBadRequest) + return + } + + isPinned := false + if payload.IsPinned != nil { + isPinned = *payload.IsPinned + } + + if err := ModifyTaskPinStatus(payload.Email, payload.EncryptionSecret, payload.UserUUID, taskUUID, isPinned); err != nil { + fmt.Printf("TogglePinHandler error: %v\n", err) + http.Error(w, fmt.Sprintf("failed to update pin: %v", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} +func ModifyTaskPinStatus(email, encryptionSecret, userUUID, taskUUID string, isPinned bool) error { + return tw.ModifyTaskPin(email, encryptionSecret, userUUID, taskUUID, isPinned) +} + diff --git a/backend/main.go b/backend/main.go index ffbdb36b..e0644418 100644 --- a/backend/main.go +++ b/backend/main.go @@ -64,7 +64,7 @@ func main() { mux.Handle("/modify-task", rateLimitedHandler(http.HandlerFunc(controllers.ModifyTaskHandler))) mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler))) mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler))) - + mux.Handle("/toggle-pin", rateLimitedHandler(http.HandlerFunc(controllers.TogglePinHandler))) mux.HandleFunc("/ws", controllers.WebSocketHandler) go controllers.JobStatusManager() diff --git a/backend/models/request_body.go b/backend/models/request_body.go index cdd3b13e..613508ea 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -22,6 +22,7 @@ type ModifyTaskRequestBody struct { Status string `json:"status"` Due string `json:"due"` Tags []string `json:"tags"` + IsPinned bool `json:"isPinned"` } type EditTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/models/task.go b/backend/models/task.go index 49484a7f..7aaaa15e 100644 --- a/backend/models/task.go +++ b/backend/models/task.go @@ -26,4 +26,5 @@ type Task struct { RType string `json:"rtype"` Recur string `json:"recur"` Annotations []Annotation `json:"annotations"` + IsPinned bool `json:"isPinned"` } diff --git a/backend/utils/tw/add_task.go b/backend/utils/tw/add_task.go index 16105f7b..ffc15708 100644 --- a/backend/utils/tw/add_task.go +++ b/backend/utils/tw/add_task.go @@ -55,6 +55,7 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p // Sync Taskwarrior again if err := SyncTaskwarrior(tempDir); err != nil { + fmt.Print("Sync after adding task failed") return err } diff --git a/backend/utils/tw/modify_task.go b/backend/utils/tw/modify_task.go index 286a435e..7c4302c2 100644 --- a/backend/utils/tw/modify_task.go +++ b/backend/utils/tw/modify_task.go @@ -56,9 +56,10 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, } // escapedStatus := fmt.Sprintf(`status:%s`, strings.ReplaceAll(status, `"`, `\"`)) - if status == "completed" { + switch status { + case "completed": utils.ExecCommand("task", taskID, "done", "rc.confirmation=off") - } else if status == "deleted" { + case "deleted": utils.ExecCommand("task", taskID, "delete", "rc.confirmation=off") } @@ -93,3 +94,57 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, return nil } + +/* + func ModifyTaskPin(uuid string, isPinned bool) error { + var tagAction string + if isPinned { + tagAction = "+pinned" // Add the tag + } else { + tagAction = "-pinned" // Remove the tag + } + + // This runs: task modify +pinned + cmd := exec.Command("task", uuid, "modify", tagAction) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("taskwarrior error: %s", string(output)) + } + return nil + } +*/ +func ModifyTaskPin(email, encryptionSecret, uuid, taskuuid string, isPinned bool) error { + if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { + return fmt.Errorf("error deleting Taskwarrior data: %v", err) + } + + tempDir, err := os.MkdirTemp("", "taskwarrior-"+email) + if err != nil { + return fmt.Errorf("failed to create temporary directory: %v", err) + } + defer os.RemoveAll(tempDir) + + origin := os.Getenv("CONTAINER_ORIGIN") + if err := SetTaskwarriorConfig(tempDir, encryptionSecret, origin, uuid); err != nil { + return err + } + + if err := SyncTaskwarrior(tempDir); err != nil { + return err + } + + tagAction := "+pinned" + if !isPinned { + tagAction = "-pinned" + } + + if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "modify", tagAction); err != nil { + return fmt.Errorf("failed to modify task pin: %v", err) + } + + if err := SyncTaskwarrior(tempDir); err != nil { + return err + } + + return nil +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8bf6aed7..1e35da91 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "aria-query": "^5.3.0", "array-union": "^2.1.0", "autoprefixer": "^10.4.16", + "axios": "^1.13.2", "babel-jest": "^29.7.0", "babel-plugin-istanbul": "^6.1.1", "babel-plugin-jest-hoist": "^29.6.3", @@ -4693,8 +4694,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { "version": "10.4.19", @@ -4732,6 +4732,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5035,6 +5046,19 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5229,7 +5253,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5503,7 +5526,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5601,6 +5623,20 @@ "node": ">=12" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5647,6 +5683,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -6153,6 +6234,26 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -6180,13 +6281,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -6271,6 +6374,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -6287,6 +6414,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -6356,6 +6496,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6380,6 +6532,33 @@ "node": ">=4" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -8695,6 +8874,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8724,7 +8912,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -8733,7 +8920,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9360,6 +9546,12 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d96c0893..487e301b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "aria-query": "^5.3.0", "array-union": "^2.1.0", "autoprefixer": "^10.4.16", + "axios": "^1.13.2", "babel-jest": "^29.7.0", "babel-plugin-istanbul": "^6.1.1", "babel-plugin-jest-hoist": "^29.6.3", diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index dc0a14ca..0853e76d 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import axios from 'axios'; import { Task } from '../../utils/types'; import { Table, @@ -31,6 +32,7 @@ import { Tag, Trash2Icon, XIcon, + Pin, } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -235,7 +237,7 @@ export const Tasks = ( console.error('Failed to edit task:', error); } } - + const handleIdSort = () => { const newOrder = idSortOrder === 'asc' ? 'desc' : 'asc'; setIdSortOrder(newOrder); @@ -247,6 +249,61 @@ export const Tasks = ( setSortOrder(newOrder); setTasks(sortTasks([...tasks], newOrder)); }; + const handleTogglePin = async (task: Task) => { + const isCurrentlyPinned = task.tags?.includes('pinned'); + const newPinStatus = !isCurrentlyPinned; + + + const updatedTags = newPinStatus + ? Array.from(new Set([...(task.tags || []), 'pinned'])) + : (task.tags || []).filter((t) => t !== 'pinned'); + + + setTasks((prev) => + prev.map((t) => + t.uuid === task.uuid ? { ...t, tags: updatedTags, isPinned: newPinStatus } : t + ) + ); + + try { + + await axios.post( + `${url.backendURL}toggle-pin`, + { + email: props.email, + encryptionSecret: props.encryptionSecret, + UUID: props.UUID, + taskuuid: task.uuid, + isPinned: newPinStatus, + }, + { + headers: { 'Content-Type': 'application/json' }, + } + ); + + + await db.tasks.update(task.uuid, { tags: updatedTags, isPinned: newPinStatus }); + + toast.success(newPinStatus ? 'Task pinned' : 'Task unpinned', { + position: 'bottom-left', + }); + } catch (error: any) { + console.error('Pin toggle failed:', error?.response?.status, error?.message); + toast.error('Could not toggle pin'); + + + setTasks((prev) => prev.map((t) => (t.uuid === task.uuid ? task : t))); + + + try { + const tasksFromDB = await db.tasks.where('email').equals(props.email).toArray(); + setTasks(sortTasksById(tasksFromDB, 'desc')); + setTempTasks(sortTasksById(tasksFromDB, 'desc')); + } catch (dbErr) { + console.error('Failed to re-sync tasks from DB after rollback:', dbErr); + } + } +}; const handleEditClick = (description: string) => { setIsEditing(true); @@ -301,32 +358,35 @@ export const Tasks = ( }); }; - // useEffect to update tempTasks whenever selectedProject changes - useEffect(() => { - if (selectedProject === 'all') { - setTempTasks(tasks); - } else { - const filteredTasks = tasks.filter( - (task) => task.project === selectedProject - ); - setTempTasks(sortTasksById(filteredTasks, 'desc')); - } - }, [selectedProject, tasks]); const handleStatusChange = (value: string) => { setSelectedStatus(value); }; - useEffect(() => { - if (selectedStatus === 'all') { - setTempTasks(tasks); - } else { - const filteredTasks = tasks.filter( - (task) => task.status === selectedStatus - ); - setTempTasks(sortTasksById(filteredTasks, 'desc')); - } - }, [selectedStatus, tasks]); + +useEffect(() => { + let filtered = [...tasks]; + + + if (selectedProject !== 'all') { + filtered = filtered.filter((task) => task.project === selectedProject); + } + + if (selectedStatus !== 'all') { + filtered = filtered.filter((task) => task.status === selectedStatus); + } + + + const sorted = filtered.sort((a, b) => { + + if (a.isPinned && !b.isPinned) return -1; + if (!a.isPinned && b.isPinned) return 1; + + return b.id - a.id; + }); + + setTempTasks(sorted); +}, [selectedProject, selectedStatus, tasks]); const handleEditTagsClick = (task: Task) => { setEditedTags(task.tags || []); @@ -655,22 +715,43 @@ export const Tasks = ( + + {task.priority === 'H' && (
)} {task.priority === 'M' && (
)} - {task.priority != 'H' && task.priority != 'M' && ( + {task.priority !== 'H' && task.priority !== 'M' && (
)} + {task.description} - {task.project != '' && ( + + {task.project !== '' && ( - - {task.project === '' ? '' : task.project} + + {task.project} )}
diff --git a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts index ace7fc9e..c2b90708 100644 --- a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts +++ b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts @@ -12,6 +12,8 @@ export type Props = { export const sortTasks = (tasks: Task[], order: 'asc' | 'desc') => { return tasks.sort((a, b) => { + if (a.isPinned && !b.isPinned) return -1; + if (!a.isPinned && b.isPinned) return 1; if (a.status < b.status) return order === 'asc' ? -1 : 1; if (a.status > b.status) return order === 'asc' ? 1 : -1; return 0; @@ -104,6 +106,9 @@ export const formattedDate = (dateString: string) => { export const sortTasksById = (tasks: Task[], order: 'asc' | 'desc') => { return tasks.sort((a, b) => { + if (a.isPinned && !b.isPinned) return -1; + if (!a.isPinned && b.isPinned) return 1; + if (order === 'asc') { return a.id < b.id ? -1 : 1; } else { @@ -143,3 +148,40 @@ export const handleDate = (v: string) => { } return true; }; + +export const toggleTaskPin =async ( + email: string, + encryptionSecret: string, + UUID: string, + taskuuid: string, + isPinned: boolean +) => { + try { + const backendURL = url.backendURL+`toggle-pin`; + + const response = await fetch(backendURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + encryptionSecret: encryptionSecret, + UUID: UUID, + taskuuid: taskuuid, + isPinned: isPinned, + }), + }); + + if (response.ok) { + toast.success(isPinned ? "Task Pinned" : "Task Unpinned", { position: 'bottom-left' }); + return true; + } else { + console.error('Failed to toggle pin'); + return false; + } + } catch (error) { + console.error('Error toggling pin:', error); + return false; + } +} \ No newline at end of file diff --git a/frontend/src/components/utils/types.ts b/frontend/src/components/utils/types.ts index f297bd14..e2efd7e0 100644 --- a/frontend/src/components/utils/types.ts +++ b/frontend/src/components/utils/types.ts @@ -34,4 +34,5 @@ export interface Task { rtype: string; recur: string; email: string; + isPinned?: boolean; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..5e71b3c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "ccsync", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/production/example.backend.env b/production/example.backend.env index e01b533f..05f90bf2 100644 --- a/production/example.backend.env +++ b/production/example.backend.env @@ -1,7 +1,6 @@ +CLIENT_ID="dummy" +CLIENT_SEC="dummy" REDIRECT_URL_DEV="http://localhost:8000/auth/callback" -SESSION_KEY="Random key" -CLIENT_SEC="Via Google Oauth" -CLIENT_ID="Via Google Oauth" +SESSION_KEY="1234567890abcdef1234567890abcdef" FRONTEND_ORIGIN_DEV="http://localhost" -CONTAINER_ORIGIN="http://production-syncserver-1:8080/" -PORT=8000 +CONTAINER_ORIGIN="http://production-syncserver-1:8080/" \ No newline at end of file