From 9bad31f6811c1fdbc85fce93a8ca83cfdf6c39e4 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 10:28:49 +0200 Subject: [PATCH 01/11] build action script --- .github/workflows/build-app.prod.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/build-app.prod.yml diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml new file mode 100644 index 0000000..c0b16a7 --- /dev/null +++ b/.github/workflows/build-app.prod.yml @@ -0,0 +1,21 @@ +on: push + +jobs: + build: + name: Build application + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: 🧰 Install + run: npm ci --production-only + + - name: 📦 build + run: npm run build From ca9927b486e7ff57209f79b73966e3741880a976 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 10:50:40 +0200 Subject: [PATCH 02/11] Add linter to action --- .github/workflows/build-app.prod.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml index c0b16a7..0985691 100644 --- a/.github/workflows/build-app.prod.yml +++ b/.github/workflows/build-app.prod.yml @@ -17,5 +17,8 @@ jobs: - name: 🧰 Install run: npm ci --production-only + - name: 🧹 Run lint + run: npm run lint + - name: 📦 build run: npm run build From 9fb688f222582307f3727ad9da190502cbc438c3 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 11:21:58 +0200 Subject: [PATCH 03/11] Add eslint rule and fix eslint errors --- data/db.json | 48 ++++++++++++++++++++++++++ eslint.config.mjs | 1 + next-env.d.ts | 2 +- src/components/forms/MemberForm.tsx | 3 +- src/components/forms/TimeEntryForm.tsx | 3 +- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/data/db.json b/data/db.json index acdfd82..15936b6 100644 --- a/data/db.json +++ b/data/db.json @@ -39,6 +39,54 @@ "department": "design", "startTimestamp": "2026-03-26T09:30:00.000Z", "stopTimestamp": "2026-03-26T10:00:00.000Z" + }, + { + "id": "98bd", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-04-08T12:14:00.000Z", + "stopTimestamp": "2026-04-08T15:21:00.000Z" + }, + { + "id": "870d", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-04-08T12:12:00.000Z", + "stopTimestamp": "2026-04-08T15:12:00.000Z" + }, + { + "id": "5ccc", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-04-08T12:04:00.000Z", + "stopTimestamp": "2026-04-08T12:57:00.000Z" + }, + { + "id": "dece", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-04-08T12:14:00.000Z", + "stopTimestamp": "2026-04-08T15:16:00.000Z" + }, + { + "id": "61fc", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-04-08T12:14:00.000Z", + "stopTimestamp": "2026-04-08T15:16:00.000Z" + }, + { + "id": "9d40", + "billable": true, + "client": "Heineken", + "department": "design", + "startTimestamp": "2026-05-09T12:12:00.000Z", + "stopTimestamp": "2026-05-09T15:15:00.000Z" } ], "members": [ diff --git a/eslint.config.mjs b/eslint.config.mjs index 28ba164..9faef66 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -23,6 +23,7 @@ const eslintConfig = defineConfig([ "no-console": ["warn", { allow: ["warn", "error"] }], "no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-imports": "error", + "react-hooks/set-state-in-effect": ["warn", { allow: ["warn", "error"] }], "unused-imports/no-unused-vars": [ "warn", { diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/components/forms/MemberForm.tsx b/src/components/forms/MemberForm.tsx index 0e115cb..709479d 100644 --- a/src/components/forms/MemberForm.tsx +++ b/src/components/forms/MemberForm.tsx @@ -45,11 +45,10 @@ export const MemberForm = ({ modalRef, memberData }: MemberFormProps) => { memberData ? editMemberEvent : createMemberEvent, initialState, ); - const isModalOpen = modalRef.current?.open; const closeModal = () => modalRef.current?.close(); useEffect(() => { - if (pending || !isModalOpen) return; + if (pending || !modalRef.current?.open) return; if (Object.keys(state.errors).length !== 0) { showCreatedToast("toastFailure", state.message); } else { diff --git a/src/components/forms/TimeEntryForm.tsx b/src/components/forms/TimeEntryForm.tsx index a0b844a..06bc33d 100644 --- a/src/components/forms/TimeEntryForm.tsx +++ b/src/components/forms/TimeEntryForm.tsx @@ -45,7 +45,6 @@ export const TimeEntryForm = ({ modalRef }: TimeEntryFormProps) => { createCalendarEvent, initialState, ); - const isModalOpen = modalRef.current?.open; const closeModal = () => modalRef.current?.close(); function handleChange(event: React.SyntheticEvent) { @@ -60,7 +59,7 @@ export const TimeEntryForm = ({ modalRef }: TimeEntryFormProps) => { } useEffect(() => { - if (pending || !isModalOpen) return; + if (pending || !modalRef.current?.open) return; if (Object.keys(state.errors).length !== 0) { showCreatedToast("toastFailure", state.message); } else { From aab268674a02c24369f0bd17def7e7e9a5c3187a Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 11:46:05 +0200 Subject: [PATCH 04/11] Rename build job to include linting step --- .github/workflows/build-app.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml index 0985691..3272786 100644 --- a/.github/workflows/build-app.prod.yml +++ b/.github/workflows/build-app.prod.yml @@ -2,7 +2,7 @@ on: push jobs: build: - name: Build application + name: Lint and build application runs-on: ubuntu-latest steps: - name: Check out repository From 3fab832b4266f8a362db18f405d672abaecad73d Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 11:58:41 +0200 Subject: [PATCH 05/11] Include lint in separate job --- .github/workflows/build-app.prod.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml index 3272786..0adb160 100644 --- a/.github/workflows/build-app.prod.yml +++ b/.github/workflows/build-app.prod.yml @@ -1,8 +1,11 @@ +name: CI + on: push jobs: - build: - name: Lint and build application + lint: + name: Lint + needs: install runs-on: ubuntu-latest steps: - name: Check out repository @@ -19,6 +22,22 @@ jobs: - name: 🧹 Run lint run: npm run lint + build: + name: Build application + needs: lint + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: 🧰 Install + run: npm ci --production-only - name: 📦 build run: npm run build From 521c90989a9579036b0e82f9dfe7fa877de453e3 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 11:59:16 +0200 Subject: [PATCH 06/11] Remove lint dependency --- .github/workflows/build-app.prod.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml index 0adb160..2df1b21 100644 --- a/.github/workflows/build-app.prod.yml +++ b/.github/workflows/build-app.prod.yml @@ -5,7 +5,6 @@ on: push jobs: lint: name: Lint - needs: install runs-on: ubuntu-latest steps: - name: Check out repository From f2c824fe73fb62ca0f0f406ab7fcf95223910a43 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 16:24:42 +0200 Subject: [PATCH 07/11] Refactor fetch and query params functions to use postgREST --- package-lock.json | 154 ++++++++++++++++++++++++++++++++++++++- package.json | 2 + services/members.ts | 56 +++++++------- services/queries.ts | 4 + services/timeEntries.ts | 62 +++++++++------- src/app/page.tsx | 5 ++ utils/supabase/client.ts | 7 ++ utils/supabase/server.ts | 24 ++++++ utils/utils.ts | 41 ++++++++--- 9 files changed, 285 insertions(+), 70 deletions(-) create mode 100644 utils/supabase/client.ts create mode 100644 utils/supabase/server.ts diff --git a/package-lock.json b/package-lock.json index 67b8673..5bda92b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "taskflow", "version": "0.1.0", "dependencies": { + "@supabase/ssr": "^0.10.0", + "@supabase/supabase-js": "^2.102.1", "eslint-plugin-unused-imports": "^4.4.1", "next": "16.1.6", "react": "19.2.3", @@ -2808,6 +2810,104 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.102.1.tgz", + "integrity": "sha512-2uH2WB0H98TOGDtaFWhxIcR42Dro/VB7VDZanz/4bVJsqioIue1m3TUqu3xciDm2W9r+1LXQvYNsYbQfWmD+uQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.102.1.tgz", + "integrity": "sha512-UcrcKTPnAIo+Yp9Jjq9XXwFbsmgRYY637mwka9ZjmTIWcX/xr1pote4OVvaGQycVY1KTiQgjMvpC0Q0yJhRq3w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.102.1.tgz", + "integrity": "sha512-InLvXKAYf8BIqiv9jWOYudWB3rU8A9uMbcip5BQ5sLLNPrbO1Ekkr79OvlhZBgMNSppxVyC7wPPGzLxMcTZhlA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.102.1.tgz", + "integrity": "sha512-h2fCumib/v6u7XMwSPgxnpfimjX4xCEayUHrxWLC7UurfQjUZJ0pmJDgm6yj80DnUerxuulRghwm5zXYysFG/Q==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.0.tgz", + "integrity": "sha512-36jIu+DuKzg5EgA3fnH+zHvwASvpKcL4zPgmHoZaULroS5Q4mzeHcM69zJ0sXUHddO5IcHjQNZJ9Vyhl/DdbRw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.100.1" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.102.1.tgz", + "integrity": "sha512-eCL9T4Xpe40nmKlkUJ7Zq/hk34db1xPiT0WL3Iv5MbJqHuCAe5TxhV8Rjqd6DNZrzjtfYObZtYl9jKJaHrivqw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.102.1.tgz", + "integrity": "sha512-bChxPVeLDnYN9M2d/u4fXsvylwSQG5grAl+HN8f+ZD9a9PuVU+Ru+xGmEsk+b9Iz3rJC9ZQnQUJYQ28fApdWYA==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.102.1", + "@supabase/functions-js": "2.102.1", + "@supabase/postgrest-js": "2.102.1", + "@supabase/realtime-js": "2.102.1", + "@supabase/storage-js": "2.102.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -3173,7 +3273,6 @@ "version": "25.3.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -3218,6 +3317,15 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4399,6 +4507,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/core-js-compat": { "version": "3.48.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", @@ -5916,6 +6037,15 @@ "react-is": "^16.7.0" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8377,7 +8507,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -8624,6 +8753,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 00fd74a..849c428 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "lint": "eslint" }, "dependencies": { + "@supabase/ssr": "^0.10.0", + "@supabase/supabase-js": "^2.102.1", "eslint-plugin-unused-imports": "^4.4.1", "next": "16.1.6", "react": "19.2.3", diff --git a/services/members.ts b/services/members.ts index 26d157b..6784ebd 100644 --- a/services/members.ts +++ b/services/members.ts @@ -12,17 +12,21 @@ class NotFoundError extends Error { } } +const REST_URL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/members`; +const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!; + +const restHeaders = { + apikey: API_KEY, + Authorization: `Bearer ${API_KEY}`, + "Content-Type": "application/json", +}; + export const getPositions = async (): Promise => { try { - const response = await fetch( - "http://localhost:3004/members?_sort=position", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, - ); + const response = await fetch(`${REST_URL}?order=position`, { + method: "GET", + headers: restHeaders, + }); const result = (await response.json()) as CreatedMember[]; return [...new Set(result.map((entry) => entry.position))]; @@ -34,11 +38,9 @@ export const getPositions = async (): Promise => { export const getClientsFromMembers = async (): Promise => { try { - const response = await fetch("http://localhost:3004/members?_sort=client", { + const response = await fetch(`${REST_URL}?order=client`, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: restHeaders, }); const result = (await response.json()) as CreatedMember[]; @@ -52,15 +54,12 @@ export const getClientsFromMembers = async (): Promise => { export const getMembers = async ( searchParams?: Promise<{ [key: string]: string }>, ): Promise => { - const baseURL = `http://localhost:3004/members`; const queryParams = buildQueryParams(await searchParams); try { - const response = await fetch(`${baseURL}?${queryParams}`, { + const response = await fetch(`${REST_URL}?${queryParams}`, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: restHeaders, }); if (response.status === 404) { throw new NotFoundError("Members not found!"); @@ -76,11 +75,9 @@ export const createMember = async ( member: MemberData & { fullName: string }, ): Promise<{ message: string; errors: {} }> => { try { - const requestResult = await fetch("http://localhost:3004/members", { + const requestResult = await fetch(`${REST_URL}`, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: restHeaders, body: JSON.stringify(member), }); if (!requestResult.ok) { @@ -111,16 +108,13 @@ export const editMember = async ( member: CreatedMember, ): Promise<{ message: string; errors: {} }> => { try { - const requestResult = await fetch( - `http://localhost:3004/members/${member.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(member), + const requestResult = await fetch(`${REST_URL}/${member.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify(member), + }); if (!requestResult.ok) { const resultText = await requestResult.text(); return { diff --git a/services/queries.ts b/services/queries.ts index 080c2a6..6a1e3e0 100644 --- a/services/queries.ts +++ b/services/queries.ts @@ -3,21 +3,25 @@ export const membersSortByOptions = [ value: "startingDateDESC", placeholder: "Starting date new-old", query: "-startingDate", + postgRESTQuery: "startingDate.desc", }, { value: "startingDateASC", placeholder: "Starting date old-new", query: "startingDate", + postgRESTQuery: "startingDate.asc", }, { value: "nameASC", placeholder: "Name A-Z", query: "fullName", + postgRESTQuery: "fullName.asc", }, { value: "nameDESC", placeholder: "Name Z-A", query: "-fullName", + postgRESTQuery: "fullName.desc", }, ]; diff --git a/services/timeEntries.ts b/services/timeEntries.ts index 3761fc8..7fb9735 100644 --- a/services/timeEntries.ts +++ b/services/timeEntries.ts @@ -1,9 +1,11 @@ "use server"; import { revalidatePath } from "next/cache"; +import { cookies } from "next/headers"; import { CreatedTimeEntry, TimeEntryData } from "@/types/dataTypes"; import { buildQueryParams } from "@/utils/utils"; +import { createClient } from "@/utils/supabase/server"; class NotFoundError extends Error { constructor(message: string) { @@ -13,31 +15,29 @@ class NotFoundError extends Error { } export const getClientsFromTimeEntries = async (): Promise => { - try { - const response = await fetch( - "http://localhost:3004/time-entries?_sort=client", - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, - ); - const result = (await response.json()) as CreatedTimeEntry[]; + const cookieStore = await cookies(); + const supabase = createClient(cookieStore); - return [...new Set(result.map((entry) => entry.client))]; - } catch (error) { - console.error(error); - return []; - } + const { data, error } = await supabase.from("time-entries").select(); + if (error) throw error; + + return [...new Set(data.map((entry) => entry.client))]; }; export const getTimeEntries = async ( searchParams?: Promise<{ [key: string]: string }>, ): Promise => { - const baseURL = `http://localhost:3004/time-entries`; + const cookieStore = await cookies(); + const supabase = createClient(cookieStore); + + const baseURL = `https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries`; const queryParams = buildQueryParams(await searchParams); + const { data, error } = await supabase.from("time-entries").select(); + + if (error) throw error; + return data ?? []; + try { const response = await fetch(`${baseURL}?${queryParams}`, { method: "GET", @@ -57,12 +57,15 @@ export const getTimeEntries = async ( export async function deleteTimeEntry(id: string) { try { - const response = await fetch(`http://localhost:3004/time-entries/${id}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, }, - }); + ); if (response.status === 404) { throw new NotFoundError("Time entry not found!"); } @@ -78,13 +81,16 @@ export async function createTimeEntry( timeEntry: TimeEntryData, ): Promise<{ message: string; errors: {} }> { try { - const requestResult = await fetch("http://localhost:3004/time-entries", { - method: "POST", - headers: { - "Content-Type": "application/json", + const requestResult = await fetch( + "https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(timeEntry), }, - body: JSON.stringify(timeEntry), - }); + ); if (!requestResult.ok) { const resultText = await requestResult.text(); return { diff --git a/src/app/page.tsx b/src/app/page.tsx index a36d7ea..e73a518 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,8 @@ import { } from "@/services/timeEntries"; import { Subheader } from "@/components/subheader/Subheader"; import { TimeEntries } from "@/components/time-entries/TimeEntries"; +import { createClient } from "@/utils/supabase/server"; +import { cookies } from "next/headers"; interface CalendarPageProps { searchParams: Promise<{ @@ -18,6 +20,9 @@ interface CalendarPageProps { export default async function CalendarPage({ searchParams, }: CalendarPageProps) { + const cookieStore = await cookies(); + const supabase = createClient(cookieStore); + const timeEntries = await getTimeEntries(searchParams); const clients = await getClientsFromTimeEntries(); const filtersAmountActive = Object.values(await searchParams).length; diff --git a/utils/supabase/client.ts b/utils/supabase/client.ts new file mode 100644 index 0000000..371933d --- /dev/null +++ b/utils/supabase/client.ts @@ -0,0 +1,7 @@ +import { createBrowserClient } from "@supabase/ssr"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY; + +export const createClient = () => + createBrowserClient(supabaseUrl!, supabaseKey!); diff --git a/utils/supabase/server.ts b/utils/supabase/server.ts new file mode 100644 index 0000000..79e42ba --- /dev/null +++ b/utils/supabase/server.ts @@ -0,0 +1,24 @@ +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY; + +export const createClient = ( + cookieStore: Awaited>, +) => { + return createServerClient(supabaseUrl!, supabaseKey!, { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options), + ); + } catch {} + }, + }, + }); +}; diff --git a/utils/utils.ts b/utils/utils.ts index eb4ab49..243e1ec 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,4 +1,4 @@ -import { filterOptions, membersSortByOptions } from "@/services/queries"; +import { membersSortByOptions } from "@/services/queries"; export const getElapsedTime = (startDate: Date, stopDate: Date): number => { const totalMinutes = Math.max( @@ -67,18 +67,41 @@ export const buildQueryParams = (inputParams?: { const params = new URLSearchParams(); for (const [param, value] of Object.entries(inputParams ?? {})) { + if (!value) continue; if (param === "sortBy") { - const query = membersSortByOptions.find( - (option) => option.value === value, - )?.query; - query && params.set("_sort", query); - } else { - const query = filterOptions.find((query) => query.value === param)?.query; - query && params.append(query, value); + const sort = membersSortByOptions.find( + (o) => o.value === value, + )?.postgRESTQuery; + if (sort) params.set("order", sort); + continue; + } + if (param === "client") { + const values = value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + if (values.length) params.append("client", `in.(${values.join(",")})`); + continue; + } + if (param === "position") { + const values = value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + if (values.length) params.append("position", `in.(${values.join(",")})`); + continue; + } + if (param === "searchMember") { + params.append("fullName", `ilike.*${value}*`); + continue; + } + if (param === "startingDate") { + params.append("startingDate", `gte.${encodeURIComponent(value)}`); } } - params.append("_sort", "startingDate"); + if (!Object.keys(inputParams ?? {}).length) + params.append("order", "startingDate.desc"); return params.toString(); }; From fd5ac562f8f4a6a0c845e163344b72795f685ba0 Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 8 Apr 2026 16:54:32 +0200 Subject: [PATCH 08/11] Fix filters of member page --- services/members.ts | 6 +++--- services/timeEntries.ts | 43 +++++++++++++++++++++-------------------- src/app/page.tsx | 5 ----- utils/utils.ts | 3 --- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/services/members.ts b/services/members.ts index 6784ebd..2cc0b11 100644 --- a/services/members.ts +++ b/services/members.ts @@ -12,8 +12,8 @@ class NotFoundError extends Error { } } -const REST_URL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/members`; -const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!; +const REST_URL = `${process.env.SUPABASE_URL}/rest/v1/members`; +const API_KEY = process.env.SUPABASE_PUBLISHABLE_DEFAULT_KEY!; const restHeaders = { apikey: API_KEY, @@ -23,7 +23,7 @@ const restHeaders = { export const getPositions = async (): Promise => { try { - const response = await fetch(`${REST_URL}?order=position`, { + const response = await fetch(`${REST_URL}`, { method: "GET", headers: restHeaders, }); diff --git a/services/timeEntries.ts b/services/timeEntries.ts index 7fb9735..7baaeb0 100644 --- a/services/timeEntries.ts +++ b/services/timeEntries.ts @@ -1,11 +1,18 @@ "use server"; import { revalidatePath } from "next/cache"; -import { cookies } from "next/headers"; import { CreatedTimeEntry, TimeEntryData } from "@/types/dataTypes"; import { buildQueryParams } from "@/utils/utils"; -import { createClient } from "@/utils/supabase/server"; + +const REST_URL = `${process.env.SUPABASE_URL}/rest/v1/time-entries`; +const API_KEY = process.env.SUPABASE_PUBLISHABLE_DEFAULT_KEY!; + +const restHeaders = { + apikey: API_KEY, + Authorization: `Bearer ${API_KEY}`, + "Content-Type": "application/json", +}; class NotFoundError extends Error { constructor(message: string) { @@ -15,35 +22,29 @@ class NotFoundError extends Error { } export const getClientsFromTimeEntries = async (): Promise => { - const cookieStore = await cookies(); - const supabase = createClient(cookieStore); - - const { data, error } = await supabase.from("time-entries").select(); - if (error) throw error; + try { + const response = await fetch(`${REST_URL}?order=client`, { + method: "GET", + headers: restHeaders, + }); + const result = (await response.json()) as CreatedTimeEntry[]; - return [...new Set(data.map((entry) => entry.client))]; + return [...new Set(result.map((entry) => entry.client))]; + } catch (error) { + console.error(error); + return []; + } }; export const getTimeEntries = async ( searchParams?: Promise<{ [key: string]: string }>, ): Promise => { - const cookieStore = await cookies(); - const supabase = createClient(cookieStore); - - const baseURL = `https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries`; const queryParams = buildQueryParams(await searchParams); - const { data, error } = await supabase.from("time-entries").select(); - - if (error) throw error; - return data ?? []; - try { - const response = await fetch(`${baseURL}?${queryParams}`, { + const response = await fetch(`${REST_URL}?${queryParams}`, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: restHeaders, }); if (response.status === 404) { throw new NotFoundError("Time entry not found!"); diff --git a/src/app/page.tsx b/src/app/page.tsx index e73a518..a36d7ea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,8 +5,6 @@ import { } from "@/services/timeEntries"; import { Subheader } from "@/components/subheader/Subheader"; import { TimeEntries } from "@/components/time-entries/TimeEntries"; -import { createClient } from "@/utils/supabase/server"; -import { cookies } from "next/headers"; interface CalendarPageProps { searchParams: Promise<{ @@ -20,9 +18,6 @@ interface CalendarPageProps { export default async function CalendarPage({ searchParams, }: CalendarPageProps) { - const cookieStore = await cookies(); - const supabase = createClient(cookieStore); - const timeEntries = await getTimeEntries(searchParams); const clients = await getClientsFromTimeEntries(); const filtersAmountActive = Object.values(await searchParams).length; diff --git a/utils/utils.ts b/utils/utils.ts index 243e1ec..f218482 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -100,8 +100,5 @@ export const buildQueryParams = (inputParams?: { } } - if (!Object.keys(inputParams ?? {}).length) - params.append("order", "startingDate.desc"); - return params.toString(); }; From eb27c003689d471ab6d6fdb859d7c9ab36ba33af Mon Sep 17 00:00:00 2001 From: Jasper Date: Thu, 9 Apr 2026 09:57:54 +0200 Subject: [PATCH 09/11] Edit member with postgREST --- services/actions.ts | 5 ++++- services/members.ts | 6 ++---- services/timeEntries.ts | 36 ++++++++++++++---------------------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/services/actions.ts b/services/actions.ts index 9c71af1..4bdadfe 100644 --- a/services/actions.ts +++ b/services/actions.ts @@ -69,7 +69,7 @@ const memberSchema = z.object({ info: z.string().max(150), lastName: z.string().trim().min(1), position: z.string().trim(), - startingDate: z.iso.datetime(), + startingDate: z.iso.datetime({ local: true }), }); const fullNameSchema = z @@ -156,7 +156,10 @@ export const editMemberEvent = async ( const data = Object.fromEntries(formData); const validatedData = memberSchema.safeParse(data); + console.table(data); + if (!validatedData.success) { + console.table(z.flattenError(validatedData.error)); return { message: "Error validating data", errors: z.flattenError(validatedData.error).fieldErrors, diff --git a/services/members.ts b/services/members.ts index 2cc0b11..f3554d0 100644 --- a/services/members.ts +++ b/services/members.ts @@ -108,11 +108,9 @@ export const editMember = async ( member: CreatedMember, ): Promise<{ message: string; errors: {} }> => { try { - const requestResult = await fetch(`${REST_URL}/${member.id}`, { + const requestResult = await fetch(`${REST_URL}?id=eq.${member.id}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - }, + headers: restHeaders, body: JSON.stringify(member), }); if (!requestResult.ok) { diff --git a/services/timeEntries.ts b/services/timeEntries.ts index 7baaeb0..b7b8abe 100644 --- a/services/timeEntries.ts +++ b/services/timeEntries.ts @@ -47,7 +47,7 @@ export const getTimeEntries = async ( headers: restHeaders, }); if (response.status === 404) { - throw new NotFoundError("Time entry not found!"); + throw new NotFoundError("Time entries not found!"); } return response.json(); } catch (error) { @@ -57,16 +57,12 @@ export const getTimeEntries = async ( }; export async function deleteTimeEntry(id: string) { + console.log("delete id: ", id); try { - const response = await fetch( - `https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries/${id}`, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }, - ); + const response = await fetch(`${REST_URL}?id=eq.${id}`, { + method: "DELETE", + headers: restHeaders, + }); if (response.status === 404) { throw new NotFoundError("Time entry not found!"); } @@ -82,18 +78,13 @@ export async function createTimeEntry( timeEntry: TimeEntryData, ): Promise<{ message: string; errors: {} }> { try { - const requestResult = await fetch( - "https://my-json-server.typicode.com/MrJasperge/taskflow-db/time-entries", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(timeEntry), - }, - ); - if (!requestResult.ok) { - const resultText = await requestResult.text(); + const response = await fetch(`${REST_URL}`, { + method: "POST", + headers: restHeaders, + body: JSON.stringify(timeEntry), + }); + if (!response.ok) { + const resultText = await response.text(); return { message: "Failed to create time entry", errors: { server: [resultText || "Unknown server error"] }, @@ -107,6 +98,7 @@ export async function createTimeEntry( errors: {}, }; } catch (error) { + console.error(error); return { message: "Network error while creating time entry", errors: { From bbc029083875c38308ecd4758fd3e5778505b99d Mon Sep 17 00:00:00 2001 From: Jasper Date: Thu, 9 Apr 2026 10:39:15 +0200 Subject: [PATCH 10/11] Update filter logic for postgREST syntax --- services/actions.ts | 3 -- services/members.ts | 4 +- services/queries.ts | 27 ++++++++++++ services/timeEntries.ts | 16 +++---- services/translations.ts | 15 ------- src/components/filters/CalendarFilters.tsx | 2 +- src/components/time-entries/TimeEntries.tsx | 1 + utils/utils.ts | 49 +++++++++++++++++++-- 8 files changed, 84 insertions(+), 33 deletions(-) diff --git a/services/actions.ts b/services/actions.ts index 4bdadfe..aba0d46 100644 --- a/services/actions.ts +++ b/services/actions.ts @@ -156,10 +156,7 @@ export const editMemberEvent = async ( const data = Object.fromEntries(formData); const validatedData = memberSchema.safeParse(data); - console.table(data); - if (!validatedData.success) { - console.table(z.flattenError(validatedData.error)); return { message: "Error validating data", errors: z.flattenError(validatedData.error).fieldErrors, diff --git a/services/members.ts b/services/members.ts index f3554d0..a816472 100644 --- a/services/members.ts +++ b/services/members.ts @@ -2,7 +2,7 @@ import { revalidatePath } from "next/cache"; -import { buildQueryParams } from "@/utils/utils"; +import { buildMemberQueryParams } from "@/utils/utils"; import { CreatedMember, MemberData } from "@/types/dataTypes"; class NotFoundError extends Error { @@ -54,7 +54,7 @@ export const getClientsFromMembers = async (): Promise => { export const getMembers = async ( searchParams?: Promise<{ [key: string]: string }>, ): Promise => { - const queryParams = buildQueryParams(await searchParams); + const queryParams = buildMemberQueryParams(await searchParams); try { const response = await fetch(`${REST_URL}?${queryParams}`, { diff --git a/services/queries.ts b/services/queries.ts index 6a1e3e0..3ef5913 100644 --- a/services/queries.ts +++ b/services/queries.ts @@ -25,6 +25,33 @@ export const membersSortByOptions = [ }, ]; +export const calendarSortByOptions = [ + { + value: "startDateDESC", + placeholder: "Starting date new-old", + query: "-startTimestamp", + postgRESTQuery: "startTimestamp.desc", + }, + { + value: "startDateASC", + placeholder: "Starting date old-new", + query: "startTimestamp", + postgRESTQuery: "startTimestamp.asc", + }, + { + value: "nameASC", + placeholder: "Name A-Z", + query: "client", + postgRESTQuery: "client.asc", + }, + { + value: "nameDESC", + placeholder: "Name Z-A", + query: "-client", + postgRESTQuery: "client.desc", + }, +]; + export const filterOptions = [ { value: "client", diff --git a/services/timeEntries.ts b/services/timeEntries.ts index b7b8abe..41a1b0b 100644 --- a/services/timeEntries.ts +++ b/services/timeEntries.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache"; import { CreatedTimeEntry, TimeEntryData } from "@/types/dataTypes"; -import { buildQueryParams } from "@/utils/utils"; +import { buildTimeEntriesQueryParams } from "@/utils/utils"; const REST_URL = `${process.env.SUPABASE_URL}/rest/v1/time-entries`; const API_KEY = process.env.SUPABASE_PUBLISHABLE_DEFAULT_KEY!; @@ -39,7 +39,7 @@ export const getClientsFromTimeEntries = async (): Promise => { export const getTimeEntries = async ( searchParams?: Promise<{ [key: string]: string }>, ): Promise => { - const queryParams = buildQueryParams(await searchParams); + const queryParams = buildTimeEntriesQueryParams(await searchParams); try { const response = await fetch(`${REST_URL}?${queryParams}`, { @@ -57,20 +57,20 @@ export const getTimeEntries = async ( }; export async function deleteTimeEntry(id: string) { - console.log("delete id: ", id); try { const response = await fetch(`${REST_URL}?id=eq.${id}`, { method: "DELETE", - headers: restHeaders, + headers: { ...restHeaders, Prefer: "return=representation" }, }); - if (response.status === 404) { - throw new NotFoundError("Time entry not found!"); + if (!response.ok) { + throw new Error(await response.text()); } + const deletedRows = (await response.json()) as CreatedTimeEntry[]; revalidatePath("/"); - return await response.json(); + return deletedRows[0] ?? null; } catch (error) { console.error(error); - return []; + return null; } } diff --git a/services/translations.ts b/services/translations.ts index 3f644ac..815954c 100644 --- a/services/translations.ts +++ b/services/translations.ts @@ -15,18 +15,3 @@ export const translations = { title: "Default Title", }, }; - -export const calendarSortByOptions = [ - { - value: "startDateDESC", - placeholder: "Starting date new-old", - query: "-startTimestamp", - }, - { - value: "startDateASC", - placeholder: "Starting date old-new", - query: "startTimestamp", - }, - { value: "nameASC", placeholder: "Name A-Z", query: "client" }, - { value: "nameDESC", placeholder: "Name Z-A", query: "-client" }, -]; diff --git a/src/components/filters/CalendarFilters.tsx b/src/components/filters/CalendarFilters.tsx index ae0203a..9b1fe67 100644 --- a/src/components/filters/CalendarFilters.tsx +++ b/src/components/filters/CalendarFilters.tsx @@ -1,6 +1,6 @@ "use client"; -import { calendarSortByOptions } from "@/services/translations"; +import { calendarSortByOptions } from "@/services/queries"; import { CheckboxField, InputField, diff --git a/src/components/time-entries/TimeEntries.tsx b/src/components/time-entries/TimeEntries.tsx index d6b308f..4865c7f 100644 --- a/src/components/time-entries/TimeEntries.tsx +++ b/src/components/time-entries/TimeEntries.tsx @@ -35,6 +35,7 @@ export const TimeEntries = ({ timeEntries }: TimeEntriesProps) => { return; const response = await deleteTimeEntry(data.id); + if (!response) return; toast(`Event deleted: ${response.client}`, { duration: 5000, diff --git a/utils/utils.ts b/utils/utils.ts index f218482..07ad25c 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,4 +1,7 @@ -import { membersSortByOptions } from "@/services/queries"; +import { + membersSortByOptions, + calendarSortByOptions, +} from "@/services/queries"; export const getElapsedTime = (startDate: Date, stopDate: Date): number => { const totalMinutes = Math.max( @@ -61,7 +64,44 @@ export const formatFullName = (firstName: string, lastName: string): string => { else return `${firstName} ${lastName}`; }; -export const buildQueryParams = (inputParams?: { +export const buildTimeEntriesQueryParams = (inputParams?: { + [key: string]: string; +}): string => { + const params = new URLSearchParams(); + + for (const [param, value] of Object.entries(inputParams ?? {})) { + if (!value) continue; + if (param === "sortBy") { + const sort = calendarSortByOptions.find( + (option) => option.value === value, + )?.postgRESTQuery; + if (sort) { + params.set("order", sort); + } + continue; + } + if (param === "client") { + const values = value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + if (values.length) params.append("client", `in.(${values.join(",")})`); + continue; + } + if (param === "searchClient") { + params.append("client", `ilike.*${value}*`); + continue; + } + if (param === "startingDate") { + params.append("startTimestamp", `gte.${value}`); + } + } + if (!params.has("order")) params.set("order", "startTimestamp.desc"); + + return params.toString(); +}; + +export const buildMemberQueryParams = (inputParams?: { [key: string]: string; }): string => { const params = new URLSearchParams(); @@ -70,7 +110,7 @@ export const buildQueryParams = (inputParams?: { if (!value) continue; if (param === "sortBy") { const sort = membersSortByOptions.find( - (o) => o.value === value, + (option) => option.value === value, )?.postgRESTQuery; if (sort) params.set("order", sort); continue; @@ -96,9 +136,10 @@ export const buildQueryParams = (inputParams?: { continue; } if (param === "startingDate") { - params.append("startingDate", `gte.${encodeURIComponent(value)}`); + params.append("startingDate", `gte.${value}`); } } + if (!params.has("order")) params.set("order", "startingDate.desc"); return params.toString(); }; From 5c1ea402667ec85d294cd85262f344a4112adec7 Mon Sep 17 00:00:00 2001 From: Jasper Date: Thu, 9 Apr 2026 11:04:59 +0200 Subject: [PATCH 11/11] Add check for empty response --- services/timeEntries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/timeEntries.ts b/services/timeEntries.ts index 41a1b0b..d26288b 100644 --- a/services/timeEntries.ts +++ b/services/timeEntries.ts @@ -49,7 +49,8 @@ export const getTimeEntries = async ( if (response.status === 404) { throw new NotFoundError("Time entries not found!"); } - return response.json(); + const acquiredRows = (await response.json()) as CreatedTimeEntry[]; + return acquiredRows ?? []; } catch (error) { console.error(error); return [];