diff --git a/.github/workflows/build-app.prod.yml b/.github/workflows/build-app.prod.yml
new file mode 100644
index 0000000..2df1b21
--- /dev/null
+++ b/.github/workflows/build-app.prod.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on: push
+
+jobs:
+ lint:
+ name: 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: 🧹 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
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/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/actions.ts b/services/actions.ts
index 9c71af1..aba0d46 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
diff --git a/services/members.ts b/services/members.ts
index 26d157b..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 {
@@ -12,17 +12,21 @@ class NotFoundError extends Error {
}
}
+const REST_URL = `${process.env.SUPABASE_URL}/rest/v1/members`;
+const API_KEY = process.env.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}`, {
+ 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);
+ const queryParams = buildMemberQueryParams(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,11 @@ 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}?id=eq.${member.id}`, {
+ method: "PUT",
+ headers: restHeaders,
+ 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..3ef5913 100644
--- a/services/queries.ts
+++ b/services/queries.ts
@@ -3,21 +3,52 @@ 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",
+ },
+];
+
+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",
},
];
diff --git a/services/timeEntries.ts b/services/timeEntries.ts
index 3761fc8..d26288b 100644
--- a/services/timeEntries.ts
+++ b/services/timeEntries.ts
@@ -3,7 +3,16 @@
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!;
+
+const restHeaders = {
+ apikey: API_KEY,
+ Authorization: `Bearer ${API_KEY}`,
+ "Content-Type": "application/json",
+};
class NotFoundError extends Error {
constructor(message: string) {
@@ -14,15 +23,10 @@ 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 response = await fetch(`${REST_URL}?order=client`, {
+ method: "GET",
+ headers: restHeaders,
+ });
const result = (await response.json()) as CreatedTimeEntry[];
return [...new Set(result.map((entry) => entry.client))];
@@ -35,20 +39,18 @@ export const getClientsFromTimeEntries = async (): Promise => {
export const getTimeEntries = async (
searchParams?: Promise<{ [key: string]: string }>,
): Promise => {
- const baseURL = `http://localhost:3004/time-entries`;
- const queryParams = buildQueryParams(await searchParams);
+ const queryParams = buildTimeEntriesQueryParams(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("Time entry not found!");
+ 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 [];
@@ -57,20 +59,19 @@ export const getTimeEntries = async (
export async function deleteTimeEntry(id: string) {
try {
- const response = await fetch(`http://localhost:3004/time-entries/${id}`, {
+ const response = await fetch(`${REST_URL}?id=eq.${id}`, {
method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
+ 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;
}
}
@@ -78,15 +79,13 @@ export async function createTimeEntry(
timeEntry: TimeEntryData,
): Promise<{ message: string; errors: {} }> {
try {
- const requestResult = await fetch("http://localhost:3004/time-entries", {
+ const response = await fetch(`${REST_URL}`, {
method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
+ headers: restHeaders,
body: JSON.stringify(timeEntry),
});
- if (!requestResult.ok) {
- const resultText = await requestResult.text();
+ if (!response.ok) {
+ const resultText = await response.text();
return {
message: "Failed to create time entry",
errors: { server: [resultText || "Unknown server error"] },
@@ -100,6 +99,7 @@ export async function createTimeEntry(
errors: {},
};
} catch (error) {
+ console.error(error);
return {
message: "Network error while creating time entry",
errors: {
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/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 {
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/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..07ad25c 100644
--- a/utils/utils.ts
+++ b/utils/utils.ts
@@ -1,4 +1,7 @@
-import { filterOptions, membersSortByOptions } from "@/services/queries";
+import {
+ membersSortByOptions,
+ calendarSortByOptions,
+} from "@/services/queries";
export const getElapsedTime = (startDate: Date, stopDate: Date): number => {
const totalMinutes = Math.max(
@@ -61,24 +64,82 @@ 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 query = membersSortByOptions.find(
+ const sort = calendarSortByOptions.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);
+ )?.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();
- params.append("_sort", "startingDate");
+ for (const [param, value] of Object.entries(inputParams ?? {})) {
+ if (!value) continue;
+ if (param === "sortBy") {
+ const sort = membersSortByOptions.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 === "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.${value}`);
+ }
+ }
+ if (!params.has("order")) params.set("order", "startingDate.desc");
return params.toString();
};