diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts
index 78dad0d..8571062 100644
--- a/apps/api/drizzle.config.ts
+++ b/apps/api/drizzle.config.ts
@@ -5,6 +5,6 @@ export default defineConfig({
schema: "./src/database/schema.ts",
dialect: "sqlite",
dbCredentials: {
- url: "file:local.db",
+ url: "file:kaneo.db",
},
}) satisfies Config;
diff --git a/apps/api/drizzle/0000_abandoned_power_man.sql b/apps/api/drizzle/0000_powerful_magneto.sql
similarity index 83%
rename from apps/api/drizzle/0000_abandoned_power_man.sql
rename to apps/api/drizzle/0000_powerful_magneto.sql
index d57888d..f8b8508 100644
--- a/apps/api/drizzle/0000_abandoned_power_man.sql
+++ b/apps/api/drizzle/0000_powerful_magneto.sql
@@ -3,7 +3,7 @@ CREATE TABLE `project` (
`workspace_id` text NOT NULL,
`name` text NOT NULL,
`description` text,
- `created_at` integer DEFAULT '"2025-01-18T14:59:51.091Z"' NOT NULL,
+ `created_at` integer DEFAULT '"2025-01-25T23:50:45.754Z"' NOT NULL,
FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
@@ -21,8 +21,9 @@ CREATE TABLE `task` (
`title` text NOT NULL,
`description` text,
`status` text DEFAULT 'to-do' NOT NULL,
+ `priority` text DEFAULT 'low',
`due_date` integer,
- `created_at` integer DEFAULT '"2025-01-18T14:59:51.091Z"' NOT NULL,
+ `created_at` integer DEFAULT '"2025-01-25T23:50:45.754Z"' NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`assignee_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
);
@@ -32,7 +33,7 @@ CREATE TABLE `user` (
`name` text NOT NULL,
`password` text NOT NULL,
`email` text NOT NULL,
- `created_at` integer DEFAULT '"2025-01-18T14:59:51.090Z"' NOT NULL
+ `created_at` integer DEFAULT '"2025-01-25T23:50:45.753Z"' NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
@@ -41,7 +42,7 @@ CREATE TABLE `workspace` (
`name` text NOT NULL,
`description` text,
`owner_id` text NOT NULL,
- `created_at` integer DEFAULT '"2025-01-18T14:59:51.091Z"' NOT NULL,
+ `created_at` integer DEFAULT '"2025-01-25T23:50:45.754Z"' NOT NULL,
FOREIGN KEY (`owner_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
@@ -50,7 +51,7 @@ CREATE TABLE `workspace_member` (
`workspace_id` text NOT NULL,
`user_id` text NOT NULL,
`role` text,
- `joined_at` integer DEFAULT '"2025-01-18T14:59:51.091Z"' NOT NULL,
+ `joined_at` integer DEFAULT '"2025-01-25T23:50:45.754Z"' NOT NULL,
FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
);
diff --git a/apps/api/drizzle/0001_mean_ultimo.sql b/apps/api/drizzle/0001_mean_ultimo.sql
deleted file mode 100644
index 9e03e43..0000000
--- a/apps/api/drizzle/0001_mean_ultimo.sql
+++ /dev/null
@@ -1,67 +0,0 @@
-PRAGMA foreign_keys=OFF;--> statement-breakpoint
-CREATE TABLE `__new_project` (
- `id` text PRIMARY KEY NOT NULL,
- `workspace_id` text NOT NULL,
- `name` text NOT NULL,
- `description` text,
- `created_at` integer DEFAULT '"2025-01-25T16:10:08.340Z"' NOT NULL,
- FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE cascade ON DELETE cascade
-);
---> statement-breakpoint
-INSERT INTO `__new_project`("id", "workspace_id", "name", "description", "created_at") SELECT "id", "workspace_id", "name", "description", "created_at" FROM `project`;--> statement-breakpoint
-DROP TABLE `project`;--> statement-breakpoint
-ALTER TABLE `__new_project` RENAME TO `project`;--> statement-breakpoint
-PRAGMA foreign_keys=ON;--> statement-breakpoint
-CREATE TABLE `__new_task` (
- `id` text PRIMARY KEY NOT NULL,
- `project_id` text NOT NULL,
- `assignee_id` text NOT NULL,
- `title` text NOT NULL,
- `description` text,
- `status` text DEFAULT 'to-do' NOT NULL,
- `due_date` integer,
- `created_at` integer DEFAULT '"2025-01-25T16:10:08.340Z"' NOT NULL,
- FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE cascade ON DELETE cascade,
- FOREIGN KEY (`assignee_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
-);
---> statement-breakpoint
-INSERT INTO `__new_task`("id", "project_id", "assignee_id", "title", "description", "status", "due_date", "created_at") SELECT "id", "project_id", "assignee_id", "title", "description", "status", "due_date", "created_at" FROM `task`;--> statement-breakpoint
-DROP TABLE `task`;--> statement-breakpoint
-ALTER TABLE `__new_task` RENAME TO `task`;--> statement-breakpoint
-CREATE TABLE `__new_user` (
- `id` text PRIMARY KEY NOT NULL,
- `name` text NOT NULL,
- `password` text NOT NULL,
- `email` text NOT NULL,
- `created_at` integer DEFAULT '"2025-01-25T16:10:08.340Z"' NOT NULL
-);
---> statement-breakpoint
-INSERT INTO `__new_user`("id", "name", "password", "email", "created_at") SELECT "id", "name", "password", "email", "created_at" FROM `user`;--> statement-breakpoint
-DROP TABLE `user`;--> statement-breakpoint
-ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
-CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
-CREATE TABLE `__new_workspace` (
- `id` text PRIMARY KEY NOT NULL,
- `name` text NOT NULL,
- `description` text,
- `owner_id` text NOT NULL,
- `created_at` integer DEFAULT '"2025-01-25T16:10:08.340Z"' NOT NULL,
- FOREIGN KEY (`owner_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
-);
---> statement-breakpoint
-INSERT INTO `__new_workspace`("id", "name", "description", "owner_id", "created_at") SELECT "id", "name", "description", "owner_id", "created_at" FROM `workspace`;--> statement-breakpoint
-DROP TABLE `workspace`;--> statement-breakpoint
-ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
-CREATE TABLE `__new_workspace_member` (
- `id` text PRIMARY KEY NOT NULL,
- `workspace_id` text NOT NULL,
- `user_id` text NOT NULL,
- `role` text,
- `joined_at` integer DEFAULT '"2025-01-25T16:10:08.340Z"' NOT NULL,
- FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE cascade ON DELETE cascade,
- FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE cascade ON DELETE cascade
-);
---> statement-breakpoint
-INSERT INTO `__new_workspace_member`("id", "workspace_id", "user_id", "role", "joined_at") SELECT "id", "workspace_id", "user_id", "role", "joined_at" FROM `workspace_member`;--> statement-breakpoint
-DROP TABLE `workspace_member`;--> statement-breakpoint
-ALTER TABLE `__new_workspace_member` RENAME TO `workspace_member`;
\ No newline at end of file
diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json
index d06e6e5..6d2a10e 100644
--- a/apps/api/drizzle/meta/0000_snapshot.json
+++ b/apps/api/drizzle/meta/0000_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
- "id": "c0af92e1-37ce-46b9-bd70-337eb6049763",
+ "id": "0bc2ad49-3384-4972-89cf-56af7f84a77f",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"project": {
@@ -41,7 +41,7 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false,
- "default": "'\"2025-01-18T14:59:51.091Z\"'"
+ "default": "'\"2025-01-25T23:50:45.754Z\"'"
}
},
"indexes": {},
@@ -147,6 +147,14 @@
"autoincrement": false,
"default": "'to-do'"
},
+ "priority": {
+ "name": "priority",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'low'"
+ },
"due_date": {
"name": "due_date",
"type": "integer",
@@ -160,7 +168,7 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false,
- "default": "'\"2025-01-18T14:59:51.091Z\"'"
+ "default": "'\"2025-01-25T23:50:45.754Z\"'"
}
},
"indexes": {},
@@ -225,7 +233,7 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false,
- "default": "'\"2025-01-18T14:59:51.090Z\"'"
+ "default": "'\"2025-01-25T23:50:45.753Z\"'"
}
},
"indexes": {
@@ -277,7 +285,7 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false,
- "default": "'\"2025-01-18T14:59:51.091Z\"'"
+ "default": "'\"2025-01-25T23:50:45.754Z\"'"
}
},
"indexes": {},
@@ -333,7 +341,7 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false,
- "default": "'\"2025-01-18T14:59:51.091Z\"'"
+ "default": "'\"2025-01-25T23:50:45.754Z\"'"
}
},
"indexes": {},
diff --git a/apps/api/drizzle/meta/0001_snapshot.json b/apps/api/drizzle/meta/0001_snapshot.json
deleted file mode 100644
index 62d9ad2..0000000
--- a/apps/api/drizzle/meta/0001_snapshot.json
+++ /dev/null
@@ -1,375 +0,0 @@
-{
- "version": "6",
- "dialect": "sqlite",
- "id": "d77a137d-164b-48be-8ff7-bf1802961d15",
- "prevId": "c0af92e1-37ce-46b9-bd70-337eb6049763",
- "tables": {
- "project": {
- "name": "project",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "workspace_id": {
- "name": "workspace_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'\"2025-01-25T16:10:08.340Z\"'"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "project_workspace_id_workspace_id_fk": {
- "name": "project_workspace_id_workspace_id_fk",
- "tableFrom": "project",
- "tableTo": "workspace",
- "columnsFrom": ["workspace_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "session": {
- "name": "session",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "expires_at": {
- "name": "expires_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {
- "session_user_id_user_id_fk": {
- "name": "session_user_id_user_id_fk",
- "tableFrom": "session",
- "tableTo": "user",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "task": {
- "name": "task",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "project_id": {
- "name": "project_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "assignee_id": {
- "name": "assignee_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'to-do'"
- },
- "due_date": {
- "name": "due_date",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'\"2025-01-25T16:10:08.340Z\"'"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "task_project_id_project_id_fk": {
- "name": "task_project_id_project_id_fk",
- "tableFrom": "task",
- "tableTo": "project",
- "columnsFrom": ["project_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- },
- "task_assignee_id_user_id_fk": {
- "name": "task_assignee_id_user_id_fk",
- "tableFrom": "task",
- "tableTo": "user",
- "columnsFrom": ["assignee_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "user": {
- "name": "user",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'\"2025-01-25T16:10:08.340Z\"'"
- }
- },
- "indexes": {
- "user_email_unique": {
- "name": "user_email_unique",
- "columns": ["email"],
- "isUnique": true
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "workspace": {
- "name": "workspace",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "owner_id": {
- "name": "owner_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'\"2025-01-25T16:10:08.340Z\"'"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "workspace_owner_id_user_id_fk": {
- "name": "workspace_owner_id_user_id_fk",
- "tableFrom": "workspace",
- "tableTo": "user",
- "columnsFrom": ["owner_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "workspace_member": {
- "name": "workspace_member",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "workspace_id": {
- "name": "workspace_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "role": {
- "name": "role",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "joined_at": {
- "name": "joined_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": "'\"2025-01-25T16:10:08.340Z\"'"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "workspace_member_workspace_id_workspace_id_fk": {
- "name": "workspace_member_workspace_id_workspace_id_fk",
- "tableFrom": "workspace_member",
- "tableTo": "workspace",
- "columnsFrom": ["workspace_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- },
- "workspace_member_user_id_user_id_fk": {
- "name": "workspace_member_user_id_user_id_fk",
- "tableFrom": "workspace_member",
- "tableTo": "user",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "cascade"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- }
- },
- "views": {},
- "enums": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "indexes": {}
- }
-}
diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json
index b70cf2f..8114680 100644
--- a/apps/api/drizzle/meta/_journal.json
+++ b/apps/api/drizzle/meta/_journal.json
@@ -5,15 +5,8 @@
{
"idx": 0,
"version": "6",
- "when": 1737212391097,
- "tag": "0000_abandoned_power_man",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "6",
- "when": 1737821408351,
- "tag": "0001_mean_ultimo",
+ "when": 1737849045760,
+ "tag": "0000_powerful_magneto",
"breakpoints": true
}
]
diff --git a/apps/api/package.json b/apps/api/package.json
index 4c9f808..e80e00c 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -7,6 +7,7 @@
"dev": "bun run --watch src/index.ts"
},
"dependencies": {
+ "@bogeychan/elysia-logger": "^0.1.7",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/jwt": "^1.2.0",
"@elysiajs/websocket": "^0.2.8",
diff --git a/apps/api/src/database/schema.ts b/apps/api/src/database/schema.ts
index 4fe9ddd..f9af7b3 100644
--- a/apps/api/src/database/schema.ts
+++ b/apps/api/src/database/schema.ts
@@ -101,6 +101,7 @@ export const taskTable = sqliteTable("task", {
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("to-do"),
+ priority: text("priority").default("low"),
dueDate: integer("due_date", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" })
.default(new Date())
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index 94e627c..4553ed8 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -1,3 +1,4 @@
+import { logger } from "@bogeychan/elysia-logger";
import { cors } from "@elysiajs/cors";
import { Elysia } from "elysia";
import project from "./project";
@@ -5,10 +6,12 @@ import task from "./task";
import user from "./user";
import { validateSessionToken } from "./user/controllers/validate-session-token";
import workspace from "./workspace";
+import workspaceUser from "./workspace-user";
const app = new Elysia()
.state("userId", "")
.use(cors())
+ .use(logger())
.use(user)
.guard({
async beforeHandle({ store, cookie: { session } }) {
@@ -39,6 +42,7 @@ const app = new Elysia()
.use(workspace)
.use(project)
.use(task)
+ .use(workspaceUser)
.onError(({ code, error }) => {
switch (code) {
case "VALIDATION":
diff --git a/apps/api/src/project/controllers/delete-project.ts b/apps/api/src/project/controllers/delete-project.ts
index 6711435..4d56af0 100644
--- a/apps/api/src/project/controllers/delete-project.ts
+++ b/apps/api/src/project/controllers/delete-project.ts
@@ -17,7 +17,7 @@ async function deleteProject({
const isProjectExisting = Boolean(existingProject);
if (!isProjectExisting) {
- throw new Error("TODO");
+ throw new Error("Project doesn't exist");
}
const [deletedProject] = await db
diff --git a/apps/api/src/project/controllers/update-project.ts b/apps/api/src/project/controllers/update-project.ts
index 8ed7851..0a11558 100644
--- a/apps/api/src/project/controllers/update-project.ts
+++ b/apps/api/src/project/controllers/update-project.ts
@@ -19,7 +19,7 @@ async function updateProject({
const isProjectExisting = Boolean(existingProject);
if (!isProjectExisting) {
- throw new Error("TODO");
+ throw new Error("Project doesn't exist");
}
const [updatedWorkspace] = await db
diff --git a/apps/api/src/project/index.ts b/apps/api/src/project/index.ts
index 389a36a..d1c203f 100644
--- a/apps/api/src/project/index.ts
+++ b/apps/api/src/project/index.ts
@@ -28,7 +28,7 @@ const project = new Elysia({ prefix: "/project" })
return projects;
})
.get("/:id", async ({ params: { id }, query: { workspaceId } }) => {
- if (!workspaceId) throw new Error("TODO");
+ if (!workspaceId) throw new Error("Workspace ID is required");
const project = await getProject({ id, workspaceId });
@@ -51,7 +51,7 @@ const project = new Elysia({ prefix: "/project" })
},
)
.delete("/:id", async ({ params: { id }, query: { workspaceId } }) => {
- if (!workspaceId) throw new Error("TODO");
+ if (!workspaceId) throw new Error("Workspace ID is required");
const deletedProject = await deleteProject({ id, workspaceId });
diff --git a/apps/api/src/task/controllers/create-task.ts b/apps/api/src/task/controllers/create-task.ts
new file mode 100644
index 0000000..e94fca1
--- /dev/null
+++ b/apps/api/src/task/controllers/create-task.ts
@@ -0,0 +1,31 @@
+import { eq } from "drizzle-orm";
+import db from "../../database";
+import { taskTable, userTable } from "../../database/schema";
+
+async function createTask(body: {
+ projectId: string;
+ assigneeId: string;
+ title: string;
+ status: string;
+ dueDate: Date | null;
+ description: string;
+ priority: string;
+}) {
+ const [assignee] = await db
+ .select({ name: userTable.name })
+ .from(userTable)
+ .where(eq(userTable.id, body.assigneeId));
+
+ if (!assignee) {
+ throw new Error("Assignee not found");
+ }
+
+ const [createdTask] = await db.insert(taskTable).values(body).returning();
+
+ return {
+ ...createdTask,
+ assigneeName: assignee.name,
+ };
+}
+
+export default createTask;
diff --git a/apps/api/src/task/controllers/get-tasks.ts b/apps/api/src/task/controllers/get-tasks.ts
new file mode 100644
index 0000000..da17aed
--- /dev/null
+++ b/apps/api/src/task/controllers/get-tasks.ts
@@ -0,0 +1,56 @@
+import { eq } from "drizzle-orm";
+import db from "../../database";
+import { projectTable, taskTable, userTable } from "../../database/schema";
+
+const DEFAULT_COLUMNS = [
+ { id: "to-do", name: "To Do" },
+ { id: "in-progress", name: "In Progress" },
+ { id: "in-review", name: "In Review" },
+ { id: "done", name: "Done" },
+] as const;
+
+async function getTasks(projectId: string) {
+ const project = await db.query.projectTable.findFirst({
+ where: eq(projectTable.id, projectId),
+ });
+
+ if (!project) {
+ throw new Error("Project not found");
+ }
+
+ const tasks = await db
+ .select({
+ id: taskTable.id,
+ title: taskTable.title,
+ description: taskTable.description,
+ status: taskTable.status,
+ priority: taskTable.priority,
+ dueDate: taskTable.dueDate,
+ createdAt: taskTable.createdAt,
+ assigneeId: taskTable.assigneeId,
+ assigneeName: userTable.name,
+ assigneeEmail: userTable.email,
+ })
+ .from(taskTable)
+ .leftJoin(userTable, eq(taskTable.assigneeId, userTable.id))
+ .where(eq(taskTable.projectId, projectId));
+
+ const columns = DEFAULT_COLUMNS.map((column) => ({
+ id: column.id,
+ name: column.name,
+ tasks: tasks
+ .filter((task) => task.status === column.id)
+ .map((task) => ({
+ ...task,
+ })),
+ }));
+
+ return {
+ id: project.id,
+ name: project.name,
+ workspaceId: project.workspaceId,
+ columns,
+ };
+}
+
+export default getTasks;
diff --git a/apps/api/src/task/controllers/update-task-status.ts b/apps/api/src/task/controllers/update-task-status.ts
new file mode 100644
index 0000000..365eded
--- /dev/null
+++ b/apps/api/src/task/controllers/update-task-status.ts
@@ -0,0 +1,15 @@
+import { eq } from "drizzle-orm";
+import db from "../../database";
+import { taskTable } from "../../database/schema";
+
+async function updateTaskStatus({
+ id,
+ status,
+}: { id: string; status: string }) {
+ await db
+ .update(taskTable)
+ .set({ status: status })
+ .where(eq(taskTable.id, id));
+}
+
+export default updateTaskStatus;
diff --git a/apps/api/src/task/controllers/update-task.ts b/apps/api/src/task/controllers/update-task.ts
new file mode 100644
index 0000000..46c880b
--- /dev/null
+++ b/apps/api/src/task/controllers/update-task.ts
@@ -0,0 +1,46 @@
+import { and, eq } from "drizzle-orm";
+import db from "../../database";
+import { taskTable } from "../../database/schema";
+
+async function updateTask({
+ id,
+ projectId,
+ title,
+ status,
+ dueDate,
+ description,
+}: {
+ id: string;
+ projectId: string;
+ assigneeId: string;
+ title: string;
+ status: string;
+ dueDate: Date;
+ description: string;
+}) {
+ const [existingTask] = await db
+ .select()
+ .from(taskTable)
+ .where(and(eq(taskTable.id, id), eq(taskTable.projectId, projectId)));
+
+ const isTaskExisting = Boolean(existingTask);
+
+ if (!isTaskExisting) {
+ throw new Error("Task doesn't exist");
+ }
+
+ const [updatedWorkspace] = await db
+ .update(taskTable)
+ .set({
+ title,
+ description,
+ status,
+ dueDate,
+ })
+ .where(and(eq(taskTable.id, id), eq(taskTable.projectId, projectId)))
+ .returning();
+
+ return updatedWorkspace;
+}
+
+export default updateTask;
diff --git a/apps/api/src/task/index.ts b/apps/api/src/task/index.ts
index ac5e1f0..2653c6e 100644
--- a/apps/api/src/task/index.ts
+++ b/apps/api/src/task/index.ts
@@ -1,9 +1,86 @@
-import Elysia from "elysia";
+import Elysia, { t } from "elysia";
-const task = new Elysia({ prefix: "/task" }).ws("/", {
- message(ws, message) {
- ws.send(message);
- },
-});
+import createTask from "./controllers/create-task";
+import getTasks from "./controllers/get-tasks";
+import updateTaskStatus from "./controllers/update-task-status";
+
+const connections = new Map();
+
+const task = new Elysia({ prefix: "/task" })
+ .post(
+ "/create",
+ async ({ body }) => {
+ const createdTask = await createTask(body);
+
+ return createdTask;
+ },
+ {
+ body: t.Object({
+ projectId: t.String(),
+ assigneeId: t.String(),
+ title: t.String(),
+ status: t.String(),
+ dueDate: t.Date(),
+ description: t.String(),
+ priority: t.String(),
+ }),
+ },
+ )
+ .ws("/ws/:projectId", {
+ async open(ws) {
+ const projectId = ws.data.params.projectId;
+
+ if (!connections.has(projectId)) {
+ connections.set(projectId, new Set());
+ }
+
+ connections.get(projectId).add(ws);
+
+ const boardState = await getTasks(projectId);
+ ws.send(boardState);
+ },
+ async message(
+ ws,
+ message: {
+ type: string;
+ id: string;
+ status: string;
+ },
+ ) {
+ const projectId = ws.data.params.projectId;
+
+ const { type, id, status } = message;
+
+ if (type === "UPDATE_TASK") {
+ await updateTaskStatus({
+ id,
+ status,
+ });
+
+ const clients = connections.get(projectId);
+ const boardState = await getTasks(projectId);
+
+ if (clients) {
+ for (const client of clients) {
+ client.send(boardState);
+ }
+ }
+ }
+ },
+ close(ws) {
+ const projectId = ws.data.params.projectId;
+
+ const clients = connections.get(projectId);
+ if (clients) {
+ clients.delete(ws);
+
+ if (clients.size === 0) {
+ connections.delete(projectId);
+ }
+ }
+
+ console.log(`Client disconnected from project ${projectId}`);
+ },
+ });
export default task;
diff --git a/apps/api/src/workspace-user/controllers/get-workspace-users.ts b/apps/api/src/workspace-user/controllers/get-workspace-users.ts
new file mode 100644
index 0000000..8f51f20
--- /dev/null
+++ b/apps/api/src/workspace-user/controllers/get-workspace-users.ts
@@ -0,0 +1,34 @@
+import { eq } from "drizzle-orm";
+import db from "../../database";
+import {
+ userTable,
+ workspaceTable,
+ workspaceUserTable,
+} from "../../database/schema";
+
+function getWorkspaceUsers({ workspaceId }: { workspaceId: string }) {
+ return db
+ .select({
+ userId: userTable.id,
+ userName: userTable.name,
+ })
+ .from(workspaceTable)
+ .where(eq(workspaceTable.id, workspaceId))
+ .leftJoin(
+ workspaceUserTable,
+ eq(workspaceTable.id, workspaceUserTable.workspaceId),
+ )
+ .leftJoin(userTable, eq(workspaceUserTable.userId, userTable.id))
+ .unionAll(
+ db
+ .select({
+ userId: userTable.id,
+ userName: userTable.name,
+ })
+ .from(workspaceTable)
+ .leftJoin(userTable, eq(workspaceTable.ownerId, userTable.id))
+ .where(eq(workspaceTable.id, workspaceId)),
+ );
+}
+
+export default getWorkspaceUsers;
diff --git a/apps/api/src/workspace-user/index.ts b/apps/api/src/workspace-user/index.ts
new file mode 100644
index 0000000..e84a1b6
--- /dev/null
+++ b/apps/api/src/workspace-user/index.ts
@@ -0,0 +1,13 @@
+import Elysia from "elysia";
+import getWorkspaceUsers from "./controllers/get-workspace-users";
+
+const workspaceUser = new Elysia({ prefix: "/workspace-user" }).get(
+ "/list/:workspaceId",
+ async ({ params: { workspaceId } }) => {
+ const workspaceUsersInWorkspace = await getWorkspaceUsers({ workspaceId });
+
+ return workspaceUsersInWorkspace;
+ },
+);
+
+export default workspaceUser;
diff --git a/apps/api/src/workspace/controllers/get-workspaces.ts b/apps/api/src/workspace/controllers/get-workspaces.ts
index c876598..e4730a5 100644
--- a/apps/api/src/workspace/controllers/get-workspaces.ts
+++ b/apps/api/src/workspace/controllers/get-workspaces.ts
@@ -7,26 +7,37 @@ import {
} from "../../database/schema";
async function getWorkspaces({ userId }: { userId: string }) {
- const workspacesWithProjects = await db
+ const workspaces = await db
.select({
id: workspaceTable.id,
name: workspaceTable.name,
ownerId: workspaceTable.ownerId,
- projectId: projectTable.id,
- projectName: projectTable.name,
})
.from(workspaceTable)
.leftJoin(
workspaceUserTable,
eq(workspaceTable.id, workspaceUserTable.workspaceId),
)
- .leftJoin(projectTable, eq(workspaceTable.id, projectTable.workspaceId))
.where(
or(
eq(workspaceTable.ownerId, userId),
eq(workspaceUserTable.userId, userId),
),
- );
+ )
+ .groupBy(workspaceTable.id, workspaceTable.name, workspaceTable.ownerId);
+
+ const workspacesWithProjects = await Promise.all(
+ workspaces.map(async (workspace) => ({
+ ...workspace,
+ projects: await db
+ .select({
+ id: projectTable.id,
+ name: projectTable.name,
+ })
+ .from(projectTable)
+ .where(eq(projectTable.workspaceId, workspace.id)),
+ })),
+ );
return workspacesWithProjects;
}
diff --git a/apps/api/src/workspace/controllers/update-workspace.ts b/apps/api/src/workspace/controllers/update-workspace.ts
index 82e3ff9..623fa95 100644
--- a/apps/api/src/workspace/controllers/update-workspace.ts
+++ b/apps/api/src/workspace/controllers/update-workspace.ts
@@ -25,7 +25,7 @@ async function updateWorkspace({
const isWorkspaceExisting = Boolean(existingWorkspace);
if (!isWorkspaceExisting) {
- throw new Error("TODO");
+ throw new Error("Workspace doesn't exist");
}
const updatedWorkspace = await db
diff --git a/apps/web/package.json b/apps/web/package.json
index 4e7b1ec..a6c8204 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -29,10 +29,12 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.0.1",
+ "immer": "^10.1.1",
"lucide-react": "^0.474.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
+ "react-use-websocket": "^4.11.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"zod": "3.24.1",
diff --git a/apps/web/src/components/common/sidebar/sections/projects/add-project-modal.tsx b/apps/web/src/components/common/sidebar/sections/projects/create-project-modal.tsx
similarity index 96%
rename from apps/web/src/components/common/sidebar/sections/projects/add-project-modal.tsx
rename to apps/web/src/components/common/sidebar/sections/projects/create-project-modal.tsx
index 525d249..4220d84 100644
--- a/apps/web/src/components/common/sidebar/sections/projects/add-project-modal.tsx
+++ b/apps/web/src/components/common/sidebar/sections/projects/create-project-modal.tsx
@@ -7,12 +7,12 @@ import { useQueryClient } from "@tanstack/react-query";
import { X } from "lucide-react";
import { useState } from "react";
-type AddProjectModalProps = {
+type CreateProjectModalProps = {
open: boolean;
onClose: () => void;
};
-function AddProjectModal({ open, onClose }: AddProjectModalProps) {
+function CreateProjectModal({ open, onClose }: CreateProjectModalProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const queryClient = useQueryClient();
@@ -108,4 +108,4 @@ function AddProjectModal({ open, onClose }: AddProjectModalProps) {
);
}
-export default AddProjectModal;
+export default CreateProjectModal;
diff --git a/apps/web/src/components/common/sidebar/sections/projects/empty-project-state.tsx b/apps/web/src/components/common/sidebar/sections/projects/empty-project-state.tsx
new file mode 100644
index 0000000..323e316
--- /dev/null
+++ b/apps/web/src/components/common/sidebar/sections/projects/empty-project-state.tsx
@@ -0,0 +1,105 @@
+import { Button } from "@/components/ui/button";
+import useGetWorkspaces from "@/hooks/queries/workspace/use-get-workspace";
+import { motion } from "framer-motion";
+import { Layout, Plus } from "lucide-react";
+import { useState } from "react";
+import { CreateWorkspaceModal } from "../workspaces/components/create-workspace-modal";
+import CreateProjectModal from "./create-project-modal";
+
+export function BoardEmptyState() {
+ const { data: workspaces } = useGetWorkspaces();
+ const [isCreateWorkspaceOpen, setIsCreateWorkspaceOpen] = useState(false);
+ const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false);
+
+ return (
+
+
+
+
+
+ {workspaces?.data && workspaces?.data.length === 0
+ ? "No Workspace Selected"
+ : "No Project Selected"}
+
+
+
+ {workspaces?.data && workspaces?.data.length === 0
+ ? "Get started by creating your first workspace and organizing your projects."
+ : "Select a project from the sidebar or create a new one to get started."}
+
+
+
+ {workspaces?.data && workspaces?.data.length === 0 ? (
+
setIsCreateWorkspaceOpen(true)}
+ className="bg-indigo-600 text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 w-full"
+ >
+
+ Create First Workspace
+
+ ) : (
+ <>
+
setIsCreateProjectOpen(true)}
+ className="bg-indigo-600 text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 w-full"
+ >
+
+ Create New Project
+
+ >
+ )}
+
+
+
+
+ Quick Tips
+
+
+
+
+
+ Create workspaces
+ {" "}
+ to organize different areas of work
+
+
+
+
+
+ Add projects
+ {" "}
+ to track specific initiatives or goals
+
+
+
+
+
+ Invite team members
+ {" "}
+ to collaborate on tasks together
+
+
+
+
+
+
setIsCreateWorkspaceOpen(false)}
+ />
+ setIsCreateProjectOpen(false)}
+ />
+
+ );
+}
diff --git a/apps/web/src/components/common/sidebar/sections/projects/project-item.tsx b/apps/web/src/components/common/sidebar/sections/projects/project-item.tsx
index 6e3884b..7d8e037 100644
--- a/apps/web/src/components/common/sidebar/sections/projects/project-item.tsx
+++ b/apps/web/src/components/common/sidebar/sections/projects/project-item.tsx
@@ -1,5 +1,5 @@
import useGetProject from "@/hooks/queries/project/use-get-project";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
import useProjectStore from "@/store/project";
import { Layout } from "lucide-react";
import { useEffect } from "react";
@@ -21,10 +21,16 @@ function ProjectItem({ workspaceId, projectId }: ProjectItemProps) {
}
}, [data, setProject, isSelected]);
+ const handleSelectProject = () => {
+ if (!isSelected) {
+ setProject(data);
+ }
+ };
+
return (
setProject(data)}
+ onClick={handleSelectProject}
className={cn(
"w-full text-left px-2 py-1.5 rounded-md flex items-center text-sm transition-all group",
isSelected
diff --git a/apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace.tsx b/apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace.tsx
index bffcdcc..1673159 100644
--- a/apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace.tsx
+++ b/apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace.tsx
@@ -1,6 +1,6 @@
import { Plus } from "lucide-react";
import { useState } from "react";
-import { CreateWorkspaceModal } from "./add-workspace-modal";
+import { CreateWorkspaceModal } from "./create-workspace-modal";
function AddWorkspace() {
const [isAddWorkspaceModalOpen, setIsAddWorkspaceModalOpen] =
diff --git a/apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace-modal.tsx b/apps/web/src/components/common/sidebar/sections/workspaces/components/create-workspace-modal.tsx
similarity index 100%
rename from apps/web/src/components/common/sidebar/sections/workspaces/components/add-workspace-modal.tsx
rename to apps/web/src/components/common/sidebar/sections/workspaces/components/create-workspace-modal.tsx
diff --git a/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-item.tsx b/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-item.tsx
index a87afb8..5c5c2e6 100644
--- a/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-item.tsx
+++ b/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-item.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
import { ChevronDown, ChevronRight, Folder, FolderOpen } from "lucide-react";
import { Fragment } from "react/jsx-runtime";
import Projects from "../../projects";
diff --git a/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-menu.tsx b/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-menu.tsx
index 006d91b..4b607c8 100644
--- a/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-menu.tsx
+++ b/apps/web/src/components/common/sidebar/sections/workspaces/components/workspace-menu.tsx
@@ -1,13 +1,13 @@
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import useDeleteWorkspace from "@/hooks/mutations/workspace/use-delete-workspace";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
import useProjectStore from "@/store/project";
import useWorkspaceStore from "@/store/workspace";
import { useQueryClient } from "@tanstack/react-query";
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react";
import { useState } from "react";
-import AddProjectModal from "../../projects/add-project-modal";
+import CreateProjectModal from "../../projects/create-project-modal";
type WorkspaceMenuProps = {
id: string;
@@ -94,7 +94,7 @@ function WorkspaceMenu({ id }: WorkspaceMenuProps) {
- setCreateProjectModalOpen(false)}
/>
diff --git a/apps/web/src/components/common/sidebar/sections/workspaces/index.tsx b/apps/web/src/components/common/sidebar/sections/workspaces/index.tsx
index a29b77d..1e88e43 100644
--- a/apps/web/src/components/common/sidebar/sections/workspaces/index.tsx
+++ b/apps/web/src/components/common/sidebar/sections/workspaces/index.tsx
@@ -47,10 +47,10 @@ function Workspaces() {
};
useEffect(() => {
- if (data?.data) {
+ if (data?.data && !selectedWorkspace) {
setWorkspace(data?.data[0]);
}
- }, [data?.data, setWorkspace]);
+ }, [data?.data, setWorkspace, selectedWorkspace]);
if (!workspaces || !workspaces.length) {
return (
diff --git a/apps/web/src/components/kanban-board/column/column-dropzone.tsx b/apps/web/src/components/kanban-board/column/column-dropzone.tsx
index add8f19..63681f4 100644
--- a/apps/web/src/components/kanban-board/column/column-dropzone.tsx
+++ b/apps/web/src/components/kanban-board/column/column-dropzone.tsx
@@ -1,4 +1,4 @@
-import type { Column } from "@/types/workspace";
+import type { Column } from "@/types/project";
import { useDroppable } from "@dnd-kit/core";
import {
SortableContext,
diff --git a/apps/web/src/components/kanban-board/column/column-header.tsx b/apps/web/src/components/kanban-board/column/column-header.tsx
index 35e2d07..ea49285 100644
--- a/apps/web/src/components/kanban-board/column/column-header.tsx
+++ b/apps/web/src/components/kanban-board/column/column-header.tsx
@@ -1,27 +1,41 @@
-import type { Column } from "@/types/workspace";
-import { MoreHorizontal } from "lucide-react";
+import { CreateTaskModal } from "@/components/task/create-task-modal";
+import toKebabCase from "@/lib/to-kebab-case";
+import type { Column } from "@/types/project";
+import { Plus } from "lucide-react";
+import { useState } from "react";
interface ColumnHeaderProps {
column: Column;
}
export function ColumnHeader({ column }: ColumnHeaderProps) {
+ const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
+
return (
-
-
-
- {column.name}
-
-
- {column.tasks.length}
-
+ <>
+
setIsTaskModalOpen(false)}
+ status={toKebabCase(column.name)}
+ />
+
+
+
+ {column.name}
+
+
+ {column.tasks.length}
+
+
+
+
setIsTaskModalOpen(true)}
+ className=" text-left px-2 py-1.5 text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800/50 rounded-md flex items-center transition-all group"
+ >
+
+
-
-
-
-
+ >
);
}
diff --git a/apps/web/src/components/kanban-board/column/index.tsx b/apps/web/src/components/kanban-board/column/index.tsx
index 36792a7..4b8b071 100644
--- a/apps/web/src/components/kanban-board/column/index.tsx
+++ b/apps/web/src/components/kanban-board/column/index.tsx
@@ -1,4 +1,4 @@
-import type { Column as ColumnType } from "@/types/workspace";
+import type { Column as ColumnType } from "@/types/project";
import { ColumnDropzone } from "./column-dropzone";
import { ColumnHeader } from "./column-header";
@@ -8,7 +8,7 @@ interface ColumnProps {
function Column({ column }: ColumnProps) {
return (
-
+
diff --git a/apps/web/src/components/kanban-board/index.tsx b/apps/web/src/components/kanban-board/index.tsx
index c0eb1c4..25f6413 100644
--- a/apps/web/src/components/kanban-board/index.tsx
+++ b/apps/web/src/components/kanban-board/index.tsx
@@ -1,4 +1,4 @@
-import generateProject from "@/lib/workspace/generate-project";
+import useBoardWebsocket from "@/hooks/use-board-websocket";
import useProjectStore from "@/store/project";
import {
DndContext,
@@ -8,108 +8,71 @@ import {
type UniqueIdentifier,
closestCorners,
} from "@dnd-kit/core";
+import { useSearch } from "@tanstack/react-router";
+import { produce } from "immer";
import { useState } from "react";
+import { BoardEmptyState } from "../common/sidebar/sections/projects/empty-project-state";
import Column from "./column";
import TaskCard from "./task-card";
function KanbanBoard() {
- const [project, setProject] = useState(
- generateProject({
- projectId: "sample-1",
- workspaceId: "workspace-1",
- tasksPerColumn: 4,
- }),
- );
- const { project: selectedProject } = useProjectStore();
+ const { project, setProject } = useProjectStore();
const [activeId, setActiveId] = useState
(null);
+ const { ws } = useBoardWebsocket();
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id);
};
- // TODO: Simplify this function
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
-
- if (!over) return;
+ if (!over || !project?.columns) return;
const activeId = active.id.toString();
const overId = over.id.toString();
- const sourceColumn = project.columns.find((col) =>
- col.tasks.some((task) => task.id === activeId),
- );
-
- const destinationColumn = project.columns.find((col) => {
- if (col.id === overId) return true;
- return col.tasks.some((task) => task.id === overId);
- });
-
- if (!sourceColumn || !destinationColumn) return;
-
- setProject((currentProject) => {
- const updatedColumns = [...currentProject.columns];
-
- const sourceColumnIndex = updatedColumns.findIndex(
- (col) => col.id === sourceColumn.id,
+ const updatedProject = produce(project, (draft) => {
+ const sourceColumn = draft?.columns?.find((col) =>
+ col.tasks.some((task) => task.id === activeId),
);
- const destinationColumnIndex = updatedColumns.findIndex(
- (col) => col.id === destinationColumn.id,
+ const destinationColumn = draft?.columns?.find(
+ (col) =>
+ col.id === overId || col.tasks.some((task) => task.id === overId),
);
+ if (!sourceColumn || !destinationColumn) return;
+
const sourceTaskIndex = sourceColumn.tasks.findIndex(
(task) => task.id === activeId,
);
const task = sourceColumn.tasks[sourceTaskIndex];
- updatedColumns[sourceColumnIndex] = {
- ...sourceColumn,
- tasks: sourceColumn.tasks.filter((t) => t.id !== activeId),
- };
+ sourceColumn.tasks = sourceColumn.tasks.filter((t) => t.id !== activeId);
if (sourceColumn.id === destinationColumn.id) {
const destinationIndex = destinationColumn.tasks.findIndex(
- (task) => task.id === overId,
+ (t) => t.id === overId,
);
- const newTasks = [...destinationColumn.tasks];
- newTasks.splice(sourceTaskIndex, 1);
- newTasks.splice(destinationIndex, 0, task);
-
- updatedColumns[destinationColumnIndex] = {
- ...destinationColumn,
- tasks: newTasks,
- };
+ destinationColumn.tasks.splice(destinationIndex, 0, task);
} else {
const updatedTask = { ...task, status: destinationColumn.id };
+ ws?.send(JSON.stringify({ type: "UPDATE_TASK", ...updatedTask }));
- if (overId === destinationColumn.id) {
- updatedColumns[destinationColumnIndex] = {
- ...destinationColumn,
- tasks: [...destinationColumn.tasks, updatedTask],
- };
- } else {
- const destinationIndex = destinationColumn.tasks.findIndex(
- (task) => task.id === overId,
- );
- const newTasks = [...destinationColumn.tasks];
- newTasks.splice(destinationIndex, 0, updatedTask);
-
- updatedColumns[destinationColumnIndex] = {
- ...destinationColumn,
- tasks: newTasks,
- };
- }
- }
+ const destinationIndex =
+ overId === destinationColumn.id
+ ? destinationColumn.tasks.length
+ : destinationColumn.tasks.findIndex((t) => t.id === overId);
- return {
- ...currentProject,
- columns: updatedColumns,
- };
+ destinationColumn.tasks.splice(destinationIndex, 0, updatedTask);
+ }
});
+ setProject(updatedProject);
setActiveId(null);
};
+ if (!project || !project?.columns) return ;
+
const activeTask = activeId
? project.columns
.flatMap((col) => col.tasks)
@@ -126,20 +89,19 @@ function KanbanBoard() {
-
- {project.columns.map((column) => (
+
+ {project?.columns.map((column) => (
))}
-
{activeTask ? (
diff --git a/apps/web/src/components/kanban-board/task-card.tsx b/apps/web/src/components/kanban-board/task-card.tsx
index cfe52a1..5d505e2 100644
--- a/apps/web/src/components/kanban-board/task-card.tsx
+++ b/apps/web/src/components/kanban-board/task-card.tsx
@@ -1,8 +1,8 @@
-import type { Task } from "@/types/workspace";
+import type { Task } from "@/types/project";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { format } from "date-fns";
-import { Calendar, Flag } from "lucide-react";
+import { Calendar, Flag, UserIcon } from "lucide-react";
interface TaskCardProps {
task: Task;
@@ -38,18 +38,15 @@ function TaskCard({ task }: TaskCardProps) {
style={style}
{...attributes}
{...listeners}
- className="bg-white dark:bg-zinc-800/50 backdrop-blur-xs rounded-lg border border-zinc-200 dark:border-zinc-700/50 p-3 cursor-move hover:border-zinc-300 dark:hover:border-zinc-700 transition-colors shadow-xs"
+ className="group bg-white dark:bg-zinc-800/50 backdrop-blur-sm rounded-lg border border-zinc-200 dark:border-zinc-700/50 p-3 cursor-move hover:border-zinc-300 dark:hover:border-zinc-700 transition-colors shadow-sm"
>
-
- {task.title}
-
-
-
- {task.description}
-
-
-
-
+
+
+
+ {task.title}
+
+
+
@@ -57,11 +54,40 @@ function TaskCard({ task }: TaskCardProps) {
{task.priority}
+
+
+
+ {task.description}
+
+
+
+ {task.assigneeName ? (
+
+
+ {task.assigneeName}
+
+
+ ) : (
+
+
+
+ Unassigned
+
+
+ )}
{task.dueDate && (
-
-
- {format(new Date(task.dueDate), "MMM d")}
+
+
+
+ {format(new Date(task.dueDate), "MMM d")}
+
)}
diff --git a/apps/web/src/components/task/create-task-modal.tsx b/apps/web/src/components/task/create-task-modal.tsx
new file mode 100644
index 0000000..0922633
--- /dev/null
+++ b/apps/web/src/components/task/create-task-modal.tsx
@@ -0,0 +1,224 @@
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import useCreateTask from "@/hooks/mutations/task/use-create-task";
+import useGetWorkspaceUsers from "@/hooks/queries/workspace-users/use-get-workspace-users";
+import useProjectStore from "@/store/project";
+import useWorkspaceStore from "@/store/workspace";
+import { zodResolver } from "@hookform/resolvers/zod";
+import * as Dialog from "@radix-ui/react-dialog";
+import { produce } from "immer";
+import { X } from "lucide-react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+interface CreateTaskModalProps {
+ open: boolean;
+ onClose: () => void;
+ status?: string;
+}
+
+const taskSchema = z.object({
+ title: z.string().min(1, { message: "Title is required" }),
+ description: z.string().optional(),
+ priority: z.enum(["low", "medium", "high", "urgent"]),
+ assigneeId: z.string(),
+});
+
+type TaskFormValues = z.infer
;
+
+export function CreateTaskModal({
+ open,
+ onClose,
+ status,
+}: CreateTaskModalProps) {
+ const { project, setProject } = useProjectStore();
+ const { workspace } = useWorkspaceStore();
+ const { data: users } = useGetWorkspaceUsers({
+ workspaceId: workspace?.id ?? "",
+ });
+
+ const form = useForm({
+ resolver: zodResolver(taskSchema),
+ defaultValues: {
+ title: "",
+ description: "",
+ priority: "low",
+ assigneeId: "",
+ },
+ });
+ const { mutateAsync } = useCreateTask();
+
+ const onSubmit = async (data: TaskFormValues) => {
+ if (!project?.id || !workspace?.id) return;
+
+ const newTask = await mutateAsync({
+ title: data.title.trim(),
+ description: data.description?.trim() || "",
+ assigneeId: data.assigneeId,
+ priority: data.priority,
+ projectId: project?.id,
+ dueDate: new Date(),
+ status: status ?? "to-do",
+ });
+
+ setProject(
+ produce(project, (draft) => {
+ const targetColumn = draft.columns?.find(
+ (col) => col.id === newTask.status,
+ );
+ if (targetColumn) {
+ targetColumn.tasks.push(newTask);
+ }
+ }),
+ );
+
+ form.reset();
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+ New Task
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx
index ab2b95a..6c8e685 100644
--- a/apps/web/src/components/ui/alert.tsx
+++ b/apps/web/src/components/ui/alert.tsx
@@ -1,7 +1,7 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
index e87d9aa..1d9677c 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -1,7 +1,7 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const Avatar = React.forwardRef<
React.ElementRef,
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index 9bfde97..554155d 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -2,7 +2,7 @@ import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
index 06a7002..8857d72 100644
--- a/apps/web/src/components/ui/dialog.tsx
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -2,7 +2,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const Dialog = DialogPrimitive.Root;
diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx
index 211b9d4..8b8a097 100644
--- a/apps/web/src/components/ui/form.tsx
+++ b/apps/web/src/components/ui/form.tsx
@@ -11,7 +11,7 @@ import {
} from "react-hook-form";
import { Label } from "@/components/ui/label";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const Form = FormProvider;
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
index dca4fd6..2cfeec4 100644
--- a/apps/web/src/components/ui/input.tsx
+++ b/apps/web/src/components/ui/input.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const Input = React.forwardRef>(
({ className, type, ...props }, ref) => {
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
index a115d28..804d9be 100644
--- a/apps/web/src/components/ui/label.tsx
+++ b/apps/web/src/components/ui/label.tsx
@@ -2,7 +2,7 @@ import * as LabelPrimitive from "@radix-ui/react-label";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
diff --git a/apps/web/src/components/ui/spinner.tsx b/apps/web/src/components/ui/spinner.tsx
index e36a723..3026c18 100644
--- a/apps/web/src/components/ui/spinner.tsx
+++ b/apps/web/src/components/ui/spinner.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
import { type VariantProps, cva } from "class-variance-authority";
import { Loader2 } from "lucide-react";
diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx
index 7bb9b33..584dd9b 100644
--- a/apps/web/src/components/ui/tooltip.tsx
+++ b/apps/web/src/components/ui/tooltip.tsx
@@ -1,7 +1,7 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/cn";
const TooltipProvider = TooltipPrimitive.Provider;
diff --git a/apps/web/src/constants/descriptions.ts b/apps/web/src/constants/descriptions.ts
deleted file mode 100644
index dfe7943..0000000
--- a/apps/web/src/constants/descriptions.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-const descriptions = [
- "This needs to be completed ASAP for the next release",
- "Several users reported this issue in production",
- "Documentation is outdated and needs revision",
- "Please review and provide feedback",
- "Current implementation is not performant",
- "Coverage is currently below threshold",
- "Users are experiencing slow load times",
- "Security vulnerabilities in old packages",
- "System crashes when input is invalid",
- "Users need better visualization of data",
-];
-
-export default descriptions;
diff --git a/apps/web/src/constants/priorities-and-statuses.ts b/apps/web/src/constants/priorities-and-statuses.ts
deleted file mode 100644
index 37f3184..0000000
--- a/apps/web/src/constants/priorities-and-statuses.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const priorities = ["low", "medium", "high", "urgent"];
-export const statuses = ["todo", "in-progress", "in-review", "done"];
diff --git a/apps/web/src/constants/task-titles.ts b/apps/web/src/constants/task-titles.ts
deleted file mode 100644
index d446765..0000000
--- a/apps/web/src/constants/task-titles.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-const taskTitles = [
- "Implement new feature",
- "Fix bug in authentication",
- "Update documentation",
- "Review pull request",
- "Refactor legacy code",
- "Write unit tests",
- "Optimize database queries",
- "Update dependencies",
- "Add error handling",
- "Create user dashboard",
-];
-
-export default taskTitles;
diff --git a/apps/web/src/constants/urls.ts b/apps/web/src/constants/urls.ts
new file mode 100644
index 0000000..263748e
--- /dev/null
+++ b/apps/web/src/constants/urls.ts
@@ -0,0 +1,2 @@
+export const API_URL: string =
+ import.meta.env.VITE_API_URL ?? "http://0.0.0.0:1337";
diff --git a/apps/web/src/fetchers/task/create-task.ts b/apps/web/src/fetchers/task/create-task.ts
new file mode 100644
index 0000000..1940d9b
--- /dev/null
+++ b/apps/web/src/fetchers/task/create-task.ts
@@ -0,0 +1,29 @@
+import { api } from "@kaneo/libs";
+
+async function createProject(
+ title: string,
+ description: string,
+ projectId: string,
+ assigneeId: string,
+ status: string,
+ dueDate: Date,
+ priority: string,
+) {
+ const response = await api.task.create.post({
+ title,
+ description,
+ projectId,
+ assigneeId,
+ status,
+ dueDate,
+ priority,
+ });
+
+ if (response.error) {
+ throw new Error(response.error.value.message);
+ }
+
+ return response?.data;
+}
+
+export default createProject;
diff --git a/apps/web/src/fetchers/workspace-user/get-workspace-users.ts b/apps/web/src/fetchers/workspace-user/get-workspace-users.ts
new file mode 100644
index 0000000..689e479
--- /dev/null
+++ b/apps/web/src/fetchers/workspace-user/get-workspace-users.ts
@@ -0,0 +1,15 @@
+import { api } from "@kaneo/libs";
+
+async function getWorkspaceUsers({ workspaceId }: { workspaceId: string }) {
+ const response = await api["workspace-user"]
+ .list({ workspaceId: workspaceId })
+ .get();
+
+ if (response.error) {
+ throw new Error(response.error.value.message);
+ }
+
+ return response.data;
+}
+
+export default getWorkspaceUsers;
diff --git a/apps/web/src/hooks/mutations/task/use-create-task.ts b/apps/web/src/hooks/mutations/task/use-create-task.ts
new file mode 100644
index 0000000..6efe120
--- /dev/null
+++ b/apps/web/src/hooks/mutations/task/use-create-task.ts
@@ -0,0 +1,37 @@
+import createTask from "@/fetchers/task/create-task";
+import { useMutation } from "@tanstack/react-query";
+
+type CreateTaskInput = {
+ title: string;
+ description: string;
+ assigneeId: string;
+ projectId: string;
+ status: string;
+ dueDate: Date;
+ priority: "low" | "medium" | "high" | "urgent";
+};
+
+function useCreateTask() {
+ return useMutation({
+ mutationFn: ({
+ title,
+ description,
+ assigneeId,
+ projectId,
+ status,
+ dueDate,
+ priority,
+ }: CreateTaskInput) =>
+ createTask(
+ title,
+ description,
+ projectId,
+ assigneeId,
+ status,
+ dueDate,
+ priority,
+ ),
+ });
+}
+
+export default useCreateTask;
diff --git a/apps/web/src/hooks/queries/project/use-get-project.ts b/apps/web/src/hooks/queries/project/use-get-project.ts
index d2d9c59..c133243 100644
--- a/apps/web/src/hooks/queries/project/use-get-project.ts
+++ b/apps/web/src/hooks/queries/project/use-get-project.ts
@@ -7,7 +7,7 @@ function useGetProject({
}: { id: string; workspaceId: string }) {
return useQuery({
queryFn: () => getProject({ id, workspaceId }),
- queryKey: ["projects", id],
+ queryKey: ["projects", workspaceId, id],
enabled: !!id,
});
}
diff --git a/apps/web/src/hooks/queries/project/use-get-projects.ts b/apps/web/src/hooks/queries/project/use-get-projects.ts
index 7a1ca97..6fd07b6 100644
--- a/apps/web/src/hooks/queries/project/use-get-projects.ts
+++ b/apps/web/src/hooks/queries/project/use-get-projects.ts
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
function useGetProjects({ workspaceId }: { workspaceId: string }) {
return useQuery({
queryFn: () => getProjects({ workspaceId }),
- queryKey: ["projects"],
+ queryKey: ["projects", workspaceId],
});
}
diff --git a/apps/web/src/hooks/queries/workspace-users/use-get-workspace-users.ts b/apps/web/src/hooks/queries/workspace-users/use-get-workspace-users.ts
new file mode 100644
index 0000000..685a221
--- /dev/null
+++ b/apps/web/src/hooks/queries/workspace-users/use-get-workspace-users.ts
@@ -0,0 +1,11 @@
+import getWorkspaceUsers from "@/fetchers/workspace-user/get-workspace-users";
+import { useQuery } from "@tanstack/react-query";
+
+function useGetWorkspaceUsers({ workspaceId }: { workspaceId: string }) {
+ return useQuery({
+ queryKey: ["workspace-users", workspaceId],
+ queryFn: () => getWorkspaceUsers({ workspaceId }),
+ });
+}
+
+export default useGetWorkspaceUsers;
diff --git a/apps/web/src/hooks/use-board-websocket.ts b/apps/web/src/hooks/use-board-websocket.ts
new file mode 100644
index 0000000..241e01c
--- /dev/null
+++ b/apps/web/src/hooks/use-board-websocket.ts
@@ -0,0 +1,40 @@
+import { API_URL } from "@/constants/urls";
+import useProjectStore from "@/store/project";
+import { useEffect, useRef } from "react";
+
+type WebSocketHook = {
+ ws: WebSocket | null;
+};
+
+function useBoardWebSocket(): WebSocketHook {
+ const wsRef = useRef(null);
+ const { project, setProject } = useProjectStore();
+
+ useEffect(() => {
+ if (!project?.id) return;
+
+ const socket = new WebSocket(`${API_URL}/task/ws/${project.id}`);
+
+ socket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ setProject(data);
+ } catch (error) {
+ console.error("WebSocket message parsing error", error);
+ }
+ };
+
+ wsRef.current = socket;
+
+ return () => {
+ socket.close();
+ wsRef.current = null;
+ };
+ }, [project?.id, setProject]);
+
+ return {
+ ws: wsRef.current,
+ };
+}
+
+export default useBoardWebSocket;
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 807ea36..38b7308 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -58,29 +58,9 @@
}
}
-/* Custom scrollbar styles */
-@utility scrollbar-thin {
- scrollbar-width: thin;
-
- &::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
-}
-@utility scrollbar-thumb-zinc-700 {
- &::-webkit-scrollbar-thumb {
- background-color: rgb(63 63 70);
- border-radius: 9999px;
- }
-}
-@utility scrollbar-track-zinc-900 {
- &::-webkit-scrollbar-track {
- background-color: rgb(24 24 27);
- }
-}
-
@layer base {
:root {
+ color-scheme: light;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
@@ -104,6 +84,7 @@
}
.dark {
+ color-scheme: dark;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
@@ -124,6 +105,28 @@
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
+
+ * {
+ scrollbar-width: thin;
+ }
+
+ *::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ *::-webkit-scrollbar-thumb {
+ background-color: theme('colors.zinc.300');
+ border-radius: 9999px;
+ }
+
+ .dark *::-webkit-scrollbar-thumb {
+ background-color: theme('colors.zinc.700');
+ }
+
+ *::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
}
@layer base {
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/cn.ts
similarity index 100%
rename from apps/web/src/lib/utils.ts
rename to apps/web/src/lib/cn.ts
diff --git a/apps/web/src/lib/to-kebab-case.ts b/apps/web/src/lib/to-kebab-case.ts
new file mode 100644
index 0000000..7ef4dd4
--- /dev/null
+++ b/apps/web/src/lib/to-kebab-case.ts
@@ -0,0 +1,9 @@
+function toKebabCase(text: string): string {
+ return text
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
+ .replace(/[^a-zA-Z0-9]+/g, "-")
+ .toLowerCase()
+ .replace(/^-|-$/g, "");
+}
+
+export default toKebabCase;
diff --git a/apps/web/src/lib/workspace/generate-project.ts b/apps/web/src/lib/workspace/generate-project.ts
deleted file mode 100644
index a2c2e55..0000000
--- a/apps/web/src/lib/workspace/generate-project.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import generateTasksForColumn from "./generate-tasks-for-column";
-
-function generateProject({
- projectId = "1",
- workspaceId = "1",
- tasksPerColumn = 3,
-}) {
- return {
- id: projectId,
- name: `Project ${projectId}`,
- workspaceId,
- columns: [
- {
- id: "todo",
- name: "To Do",
- tasks: generateTasksForColumn(
- tasksPerColumn,
- "todo",
- projectId,
- workspaceId,
- ),
- },
- {
- id: "in-progress",
- name: "In Progress",
- tasks: generateTasksForColumn(
- Math.ceil(tasksPerColumn / 2),
- "in-progress",
- projectId,
- workspaceId,
- ),
- },
- {
- id: "in-review",
- name: "In Review",
- tasks: generateTasksForColumn(
- Math.ceil(tasksPerColumn / 3),
- "in-review",
- projectId,
- workspaceId,
- ),
- },
- {
- id: "done",
- name: "Done",
- tasks: generateTasksForColumn(
- tasksPerColumn,
- "done",
- projectId,
- workspaceId,
- ),
- },
- ],
- };
-}
-
-export default generateProject;
diff --git a/apps/web/src/lib/workspace/generate-random-due-date.ts b/apps/web/src/lib/workspace/generate-random-due-date.ts
deleted file mode 100644
index 45fb70e..0000000
--- a/apps/web/src/lib/workspace/generate-random-due-date.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-function generateRandomDueDate() {
- const today = new Date();
- const futureDate = new Date();
- futureDate.setDate(today.getDate() + Math.floor(Math.random() * 30));
- return futureDate.toLocaleDateString();
-}
-
-export default generateRandomDueDate;
diff --git a/apps/web/src/lib/workspace/generate-task.ts b/apps/web/src/lib/workspace/generate-task.ts
deleted file mode 100644
index 4d4830d..0000000
--- a/apps/web/src/lib/workspace/generate-task.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import descriptions from "@/constants/descriptions";
-import { priorities } from "@/constants/priorities-and-statuses";
-import taskTitles from "@/constants/task-titles";
-import generateRandomDueDate from "./generate-random-due-date";
-
-function generateTask({
- projectId,
- workspaceId,
- status,
-}: {
- projectId: string;
- workspaceId: string;
- status: string;
-}) {
- const id = Math.random().toString(36).substr(2, 9);
-
- return {
- id,
- title: taskTitles[Math.floor(Math.random() * taskTitles.length)],
- description: descriptions[Math.floor(Math.random() * descriptions.length)],
- assigneeId: Math.floor(Math.random() * 5) + 1, // Random assignee 1-5
- priority: priorities[Math.floor(Math.random() * priorities.length)],
- dueDate: Math.random() > 0.3 ? generateRandomDueDate() : null, // 70% chance of having a due date
- status,
- projectId,
- workspaceId,
- };
-}
-
-export default generateTask;
diff --git a/apps/web/src/lib/workspace/generate-tasks-for-column.ts b/apps/web/src/lib/workspace/generate-tasks-for-column.ts
deleted file mode 100644
index ab08318..0000000
--- a/apps/web/src/lib/workspace/generate-tasks-for-column.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import generateTask from "./generate-task";
-
-function generateTasksForColumn(
- count: number,
- columnId: string,
- projectId: string,
- workspaceId: string,
-) {
- return Array(count)
- .fill(null)
- .map(() =>
- generateTask({
- projectId,
- workspaceId,
- status: columnId,
- }),
- );
-}
-
-export default generateTasksForColumn;
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 1e7fd60..8ab9152 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -14,7 +14,7 @@ export const Route = createRootRouteWithContext<{
function RootComponent() {
return (
<>
-
+
diff --git a/apps/web/src/routes/dashboard/index.tsx b/apps/web/src/routes/dashboard/index.tsx
index 0c212e3..9a42a88 100644
--- a/apps/web/src/routes/dashboard/index.tsx
+++ b/apps/web/src/routes/dashboard/index.tsx
@@ -18,7 +18,7 @@ function DashboardIndexRouteComponent() {
return (
<>
-
+
>
diff --git a/apps/web/src/store/hash.ts b/apps/web/src/store/hash.ts
new file mode 100644
index 0000000..40a9008
--- /dev/null
+++ b/apps/web/src/store/hash.ts
@@ -0,0 +1,19 @@
+import type { StateStorage } from "zustand/middleware";
+
+export const hashStorage: StateStorage = {
+ getItem: (key): string => {
+ const searchParams = new URLSearchParams(location.hash.slice(1));
+ const value = searchParams.get(key);
+ return String(value);
+ },
+ setItem: (key, newValue): void => {
+ const searchParams = new URLSearchParams(location.hash.slice(1));
+ searchParams.set(key, newValue);
+ location.hash = searchParams.toString();
+ },
+ removeItem: (key): void => {
+ const searchParams = new URLSearchParams(location.hash.slice(1));
+ searchParams.delete(key);
+ location.hash = searchParams.toString();
+ },
+};
diff --git a/apps/web/src/types/project/index.ts b/apps/web/src/types/project/index.ts
index bec45af..97932b7 100644
--- a/apps/web/src/types/project/index.ts
+++ b/apps/web/src/types/project/index.ts
@@ -3,4 +3,24 @@ export type Project = {
name: string;
description: string | null;
workspaceId: string;
+ columns?: Column[];
+};
+
+export type Column = {
+ id: string;
+ name: string;
+ tasks: Task[];
+};
+
+export type Task = {
+ id: string;
+ createdAt: Date;
+ description: string | null;
+ projectId: string;
+ assigneeId: string;
+ title: string;
+ status: string;
+ dueDate: Date | null;
+ priority: string | null;
+ assigneeName: string;
};
diff --git a/apps/web/src/types/workspace/index.ts b/apps/web/src/types/workspace/index.ts
index 0f35fbb..6655b78 100644
--- a/apps/web/src/types/workspace/index.ts
+++ b/apps/web/src/types/workspace/index.ts
@@ -3,21 +3,3 @@ export type Workspace = {
name: string;
ownerId: string;
};
-
-export type Column = {
- id: string;
- name: string;
- tasks: Task[];
-};
-
-export type Task = {
- id: string;
- title: string;
- description: string;
- assigneeId: number;
- priority: string;
- dueDate: string | null;
- status: string;
- projectId: string;
- workspaceId: string;
-};
diff --git a/bun.lockb b/bun.lockb
index bec071e..56c60ca 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/packages/libs/src/eden.ts b/packages/libs/src/eden.ts
index c2b9026..9b8a478 100644
--- a/packages/libs/src/eden.ts
+++ b/packages/libs/src/eden.ts
@@ -1,8 +1,11 @@
import { treaty } from "@elysiajs/eden";
import type { App } from "@kaneo/api";
-export const api = treaty(process.env.API_URL ?? "http://0.0.0.0:1337", {
- fetch: {
- credentials: "include",
+export const api = treaty(
+ import.meta.env.VITE_API_URL ?? "http://0.0.0.0:1337",
+ {
+ fetch: {
+ credentials: "include",
+ },
},
-});
+);