Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f46b7a7
Feat/add new app configs and fix mobile view (#51)
jonattasmoraes Feb 2, 2026
6e489ba
Refact/side bar (#59)
luizhcastro Feb 4, 2026
b18caea
Feat/ws impl (#60)
DaNnielRody Feb 5, 2026
fc5ab34
fix: drag and drop usability, removing the flick view (#61)
luizhcastro Feb 6, 2026
1fd30d7
refact: redesign BoardHeader with filter tabs and display button
luizhcastro Feb 12, 2026
0cfecc7
refact: redesign kanban column header with status icons and descriptions
luizhcastro Feb 12, 2026
682e188
refact: redesign task cards with priority icon, labels, and footer
luizhcastro Feb 12, 2026
0f2b87d
feat: add inline task creation inside kanban columns
luizhcastro Feb 12, 2026
1fbefe4
refact: inline task creation as draft card triggered by + button
luizhcastro Feb 12, 2026
08f409e
feat: add custom Figma SVG icons for kanban column statuses
luizhcastro Feb 12, 2026
e54d30d
refact: align board UI with Figma design
luizhcastro Feb 12, 2026
b639fee
feat(server): add description field to column model and endpoints
luizhcastro Feb 12, 2026
4789764
feat(web): add description to column types, default columns and hooks
luizhcastro Feb 12, 2026
0e46fd2
feat(web): auto-create default columns on org creation and remove Emp…
luizhcastro Feb 12, 2026
e77d7a2
feat(web): add icon/color selector and description field to column forms
luizhcastro Feb 12, 2026
174d4e7
fix(server): add missing description field to delete and reorder colu…
luizhcastro Feb 13, 2026
262f355
refact: migrate Phosphor Icons to Icon suffix convention
luizhcastro Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ temp
.claude
.gemini
.idea
.agents
2 changes: 2 additions & 0 deletions apps/server/src/modules/columns/create-column/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { zDate } from "@/shared/schemas/zod-date";

export const createColumnBodySchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
color: z.string().optional(),
isCompleted: z.boolean().optional().default(false),
});
Expand All @@ -11,6 +12,7 @@ export type CreateColumnInput = z.infer<typeof createColumnBodySchema>;

export const createColumnResponseSchema = z.object({
name: z.string(),
description: z.string().nullable(),
id: z.string(),
createdAt: zDate,
updatedAt: zDate,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/columns/create-column/use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export async function createColumnUseCase(
return prisma.column.create({
data: {
name: input.name,
description: input.description,
color: input.color,
order: lastColumn ? lastColumn.order + 1 : 0,
isCompleted: input.isCompleted ?? false,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/columns/delete-column/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const deleteColumnParamsSchema = z.object({
export const deleteColumnResponseSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().nullable(),
createdAt: zDate,
updatedAt: zDate,
organizationId: z.string(),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/columns/get-columns/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const getColumnsSucessResponseSchema = z
.array(),
id: z.string(),
name: z.string(),
description: z.string().nullable(),
color: z.string().nullable(),
order: z.number(),
isCompleted: z.boolean(),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/columns/reorder-columns/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const reorderColumnsResponseSchema = z
.object({
id: z.string(),
name: z.string(),
description: z.string().nullable(),
createdAt: zDate,
updatedAt: zDate,
organizationId: z.string(),
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/columns/update-column/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const updateColumnParamsSchema = z.object({

export const updateColumnBodySchema = z.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
color: z.string().optional(),
order: z.number().int().optional(),
isCompleted: z.boolean().optional(),
Expand All @@ -17,6 +18,7 @@ export type UpdateColumnInput = z.infer<typeof updateColumnBodySchema>;
export const updateColumnResponseSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().nullable(),
createdAt: zDate,
updatedAt: zDate,
organizationId: z.string(),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/columns/update-column/use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export async function updateColumnUseCase(
where: { id },
data: {
name: input.name,
description: input.description,
color: input.color,
order: input.order,
isCompleted: input.isCompleted,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/shared/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { columnsRouter } from "../../modules/columns/router";
import { tasksRouter } from "../../modules/tasks/router";
import { openapiConfig } from "../config/openapi";
import { authPlugin } from "./plugins/auth.plugin";
import { wsPlugin } from "./plugins/ws.plugin";

const app = new Elysia()
.use(
Expand All @@ -19,6 +20,7 @@ const app = new Elysia()
)
.use(openapi(openapiConfig))
.use(authPlugin)
.use(wsPlugin)
.use([columnsRouter, tasksRouter])
.listen(env.PORT, ({ hostname, port }) =>
console.log(`Server is running on http://${hostname}:${port}`),
Expand Down
163 changes: 163 additions & 0 deletions apps/server/src/shared/http/plugins/ws.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { auth } from "@blaboard/auth";
import { Elysia, t } from "elysia";

type WsClient = {
readyState: number;
send: (data: string) => void;
};

const orgClients: Map<string, Map<string, WsClient>> = new Map();
const MAX_MESSAGE_SIZE = 1024 * 100;

function broadcastToOrg(orgId: string, message: unknown, excludeWsId?: string) {
const clients = orgClients.get(orgId);
if (!clients) return;

const messageStr = JSON.stringify(message);

if (messageStr.length > MAX_MESSAGE_SIZE) {
return;
}

for (const [wsId, ws] of clients.entries()) {
if (wsId !== excludeWsId && ws.readyState === 1) {
ws.send(messageStr);
}
}
}

function addClientToOrg(orgId: string, wsId: string, ws: WsClient) {
if (!orgClients.has(orgId)) {
orgClients.set(orgId, new Map());
}
orgClients.get(orgId)?.set(wsId, ws);
}

function removeClientFromOrg(orgId: string, wsId: string) {
const clients = orgClients.get(orgId);
if (!clients) return;

clients.delete(wsId);

if (clients.size === 0) {
orgClients.delete(orgId);
}
}

export const wsPlugin = new Elysia({ name: "ws" })
.ws("/ws", {
body: t.Union([
t.Object({
type: t.Literal("task:created"),
data: t.Object({
taskId: t.String(),
columnId: t.String(),
title: t.String(),
}),
}),
t.Object({
type: t.Literal("task:updated"),
data: t.Object({
taskId: t.String(),
}),
}),
t.Object({
type: t.Literal("task:deleted"),
data: t.Object({
taskId: t.String(),
}),
}),
t.Object({
type: t.Literal("task:moved"),
data: t.Object({
taskId: t.String(),
columnId: t.String(),
order: t.Number(),
}),
}),
t.Object({
type: t.Literal("column:created"),
data: t.Object({
columnId: t.String(),
name: t.String(),
}),
}),
t.Object({
type: t.Literal("column:updated"),
data: t.Object({
columnId: t.String(),
}),
}),
t.Object({
type: t.Literal("column:deleted"),
data: t.Object({
columnId: t.String(),
}),
}),
t.Object({
type: t.Literal("columns:reordered"),
data: t.Object({
columns: t.Array(
t.Object({
id: t.String(),
order: t.Number(),
}),
),
}),
}),
t.Object({
type: t.Literal("ping"),
}),
t.Object({
type: t.Literal("pong"),
}),
]),
query: t.Object({
orgId: t.String(),
}),
async open(ws) {
const requestedOrgId = ws.data.query.orgId;

const headers = new Headers();
for (const [key, value] of Object.entries(ws.data.headers)) {
if (value) headers.set(key, value);
}
const session = await auth.api.getSession({ headers });

if (!session) {
ws.close(1008, "Unauthorized");
return;
}

const activeOrganizationId = session.session.activeOrganizationId;
if (typeof activeOrganizationId !== "string") {
ws.close(1008, "Organization required");
return;
}

if (requestedOrgId !== activeOrganizationId) {
ws.close(1008, "Forbidden");
return;
}

addClientToOrg(activeOrganizationId, ws.id, ws.raw);
},
message(ws, message) {
const orgId = ws.data.query.orgId;

if (message.type === "ping") {
if (ws.raw.readyState === 1) {
ws.raw.send(JSON.stringify({ type: "pong" }));
}
return;
}

if (message.type === "pong") return;

broadcastToOrg(orgId, message, ws.id);
},
close(ws) {
const orgId = ws.data.query.orgId;
removeClientFromOrg(orgId, ws.id);
},
});
1 change: 1 addition & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_SERVER_URL="http://localhost:3000"
NEXT_PUBLIC_FRONTEND_URL="http://localhost:3001"
4 changes: 2 additions & 2 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import "@blaboard/env/web";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
typedRoutes: true,
reactCompiler: true,
typedRoutes: true,
reactCompiler: true,
};

export default nextConfig;
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
},
"dependencies": {
"@base-ui/react": "^1.0.0",
"@blaboard/server": "workspace:*",
"@blaboard/auth": "workspace:*",
"@blaboard/env": "workspace:*",
"@blaboard/server": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
Expand All @@ -24,8 +24,10 @@
"better-auth": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "catalog:",
"elysia": "1.4.22",
"framer-motion": "^12.31.0",
"lucide-react": "^0.563.0",
"next": "^16.1.1",
"next-themes": "^0.4.6",
Expand Down
Binary file added apps/web/public/android-chrome-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions apps/web/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
37 changes: 37 additions & 0 deletions apps/web/src/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { Sidebar, SidebarProvider, SidebarTrigger } from "~/components/layout";
import { OrgGuard } from "~/components/org";
import {
CommandPalette,
CommandPaletteProvider,
} from "~/components/command-palette";

export default function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<OrgGuard>
<CommandPaletteProvider>
<SidebarProvider>
<div className="flex h-screen overflow-hidden bg-sidebar">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-border bg-sidebar my-2 mr-2">
{/* Mobile Header */}
<header className="flex h-14 shrink-0 items-center gap-4 border-b border-border bg-background px-4 md:hidden">
<SidebarTrigger />
<span className="font-semibold">Blaboard</span>
</header>
<main className="flex flex-1 flex-col overflow-hidden">
{children}
</main>
</div>
</div>
<CommandPalette />
</SidebarProvider>
</CommandPaletteProvider>
</OrgGuard>
);
}
19 changes: 19 additions & 0 deletions apps/web/src/app/(authenticated)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { TaskBoard } from "~/components/board";
import { authClient } from "~/lib/auth-client";

export default function Home() {
const { data: session } = authClient.useSession();

if (!session?.user || !session?.session?.activeOrganizationId) {
return null;
}

return (
<TaskBoard
organizationId={session.session.activeOrganizationId}
userId={session.user.id}
/>
);
}
Loading