diff --git a/apps/server/src/modules/columns/create-column/schemas.ts b/apps/server/src/modules/columns/create-column/schemas.ts index 47d5ed4..55a7dbd 100644 --- a/apps/server/src/modules/columns/create-column/schemas.ts +++ b/apps/server/src/modules/columns/create-column/schemas.ts @@ -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), }); @@ -11,6 +12,7 @@ export type CreateColumnInput = z.infer; export const createColumnResponseSchema = z.object({ name: z.string(), + description: z.string().nullable(), id: z.string(), createdAt: zDate, updatedAt: zDate, diff --git a/apps/server/src/modules/columns/create-column/use-case.ts b/apps/server/src/modules/columns/create-column/use-case.ts index 8e4c6d1..df6bdb6 100644 --- a/apps/server/src/modules/columns/create-column/use-case.ts +++ b/apps/server/src/modules/columns/create-column/use-case.ts @@ -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, diff --git a/apps/server/src/modules/columns/delete-column/schemas.ts b/apps/server/src/modules/columns/delete-column/schemas.ts index c9f2b96..4c35067 100644 --- a/apps/server/src/modules/columns/delete-column/schemas.ts +++ b/apps/server/src/modules/columns/delete-column/schemas.ts @@ -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(), diff --git a/apps/server/src/modules/columns/get-columns/schemas.ts b/apps/server/src/modules/columns/get-columns/schemas.ts index 0c8b794..3f49d2b 100644 --- a/apps/server/src/modules/columns/get-columns/schemas.ts +++ b/apps/server/src/modules/columns/get-columns/schemas.ts @@ -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(), diff --git a/apps/server/src/modules/columns/reorder-columns/schemas.ts b/apps/server/src/modules/columns/reorder-columns/schemas.ts index 67c4cca..adc3b18 100644 --- a/apps/server/src/modules/columns/reorder-columns/schemas.ts +++ b/apps/server/src/modules/columns/reorder-columns/schemas.ts @@ -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(), diff --git a/apps/server/src/modules/columns/update-column/schemas.ts b/apps/server/src/modules/columns/update-column/schemas.ts index 2d37391..460e357 100644 --- a/apps/server/src/modules/columns/update-column/schemas.ts +++ b/apps/server/src/modules/columns/update-column/schemas.ts @@ -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(), @@ -17,6 +18,7 @@ export type UpdateColumnInput = z.infer; export const updateColumnResponseSchema = z.object({ id: z.string(), name: z.string(), + description: z.string().nullable(), createdAt: zDate, updatedAt: zDate, organizationId: z.string(), diff --git a/apps/server/src/modules/columns/update-column/use-case.ts b/apps/server/src/modules/columns/update-column/use-case.ts index 8735cf8..5f70e0e 100644 --- a/apps/server/src/modules/columns/update-column/use-case.ts +++ b/apps/server/src/modules/columns/update-column/use-case.ts @@ -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, diff --git a/apps/web/src/app/(authenticated)/layout.tsx b/apps/web/src/app/(authenticated)/layout.tsx index 4e0f2bc..6e4c3b0 100644 --- a/apps/web/src/app/(authenticated)/layout.tsx +++ b/apps/web/src/app/(authenticated)/layout.tsx @@ -16,9 +16,9 @@ export default function AuthenticatedLayout({ -
+
-
+
{/* Mobile Header */}
diff --git a/apps/web/src/app/(authenticated)/task/[taskId]/page.tsx b/apps/web/src/app/(authenticated)/task/[taskId]/page.tsx index 9e07686..1e24c32 100644 --- a/apps/web/src/app/(authenticated)/task/[taskId]/page.tsx +++ b/apps/web/src/app/(authenticated)/task/[taskId]/page.tsx @@ -1,11 +1,11 @@ "use client"; import { - ArrowLeft, - CalendarBlank, - DotsThree, - PencilSimple, - Trash, + ArrowLeftIcon, + CalendarIcon, + DotsThreeIcon, + PencilSimpleIcon, + TrashIcon, } from "@phosphor-icons/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -133,7 +133,7 @@ export default function TaskDetailsPage({ params }: PageProps) { href="/" className="flex size-9 items-center justify-center rounded-lg border border-border bg-card transition-colors hover:border-foreground/20" > - +
Project Overview @@ -148,16 +148,12 @@ export default function TaskDetailsPage({ params }: PageProps) { className="flex h-9 items-center gap-2 rounded-lg border border-border bg-transparent px-3 text-foreground text-sm transition-colors hover:border-foreground/20 hover:bg-accent" onClick={() => setIsEditModalOpen(true)} > - + Edit - + - + Delete task @@ -285,7 +281,7 @@ export default function TaskDetailsPage({ params }: PageProps) { Due Date
- + {formatDate(task.dueDate)} @@ -310,9 +306,7 @@ export default function TaskDetailsPage({ params }: PageProps) {
- - Status - + Status
void; + onAdd: (name: string, color?: string, description?: string) => void; isLoading?: boolean; } +type IconComponent = ComponentType>; + const COLUMN_COLORS = [ { id: "blue", color: "#3b82f6" }, { id: "yellow", color: "#eab308" }, @@ -20,22 +30,43 @@ const COLUMN_COLORS = [ { id: "cyan", color: "#06b6d4" }, ]; +const COLUMN_ICONS: { id: string; label: string; icon: IconComponent }[] = [ + { id: "backlog", label: "Backlog", icon: BacklogIcon }, + { id: "todo", label: "Todo", icon: TodoIcon }, + { id: "in-progress", label: "In Progress", icon: InProgressIcon }, + { id: "review", label: "Review", icon: ReviewIcon }, + { id: "done", label: "Done", icon: DoneIcon }, +]; + +type SelectionMode = "icon" | "color"; + export function AddColumn({ onAdd, isLoading }: AddColumnProps) { const [isEditing, setIsEditing] = useState(false); const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [mode, setMode] = useState("icon"); + const [selectedIcon, setSelectedIcon] = useState(COLUMN_ICONS[0].id); const [selectedColor, setSelectedColor] = useState(COLUMN_COLORS[0].color); const inputRef = useRef(null); const handleSubmit = () => { if (!name.trim()) return; - onAdd(name.trim(), selectedColor); + const colorValue = + mode === "icon" ? `icon:${selectedIcon}` : selectedColor; + onAdd(name.trim(), colorValue, description.trim() || undefined); setName(""); + setDescription(""); + setMode("icon"); + setSelectedIcon(COLUMN_ICONS[0].id); setSelectedColor(COLUMN_COLORS[0].color); setIsEditing(false); }; const handleCancel = () => { setName(""); + setDescription(""); + setMode("icon"); + setSelectedIcon(COLUMN_ICONS[0].id); setSelectedColor(COLUMN_COLORS[0].color); setIsEditing(false); }; @@ -66,30 +97,94 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) { className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" /> -
- Color -
- {COLUMN_COLORS.map((c) => ( - - ))} -
+ setDescription(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Description (optional)" + className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-xs placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" + /> + + {/* Mode toggle */} +
+ +
+ {mode === "icon" ? ( +
+ Icon +
+ {COLUMN_ICONS.map((item) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+ ) : ( +
+ Color +
+ {COLUMN_COLORS.map((c) => ( + + ))} +
+
+ )} +
@@ -117,7 +212,7 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) { onClick={handleStartEditing} className="flex h-9 w-64 min-w-64 items-center justify-center gap-1.5 rounded-lg border border-border border-dashed text-muted-foreground transition-colors hover:border-foreground/30 hover:bg-accent hover:text-foreground" > - + Add column ); diff --git a/apps/web/src/components/board/board-header.tsx b/apps/web/src/components/board/board-header.tsx index 2727605..64e16f4 100644 --- a/apps/web/src/components/board/board-header.tsx +++ b/apps/web/src/components/board/board-header.tsx @@ -1,52 +1,81 @@ "use client"; -import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; +import { useState } from "react"; +import { + CubeIcon, + UsersThreeIcon, + CalendarIcon, + SlidersHorizontalIcon, + BellIcon, +} from "@phosphor-icons/react"; + +const FILTER_TABS = [ + { id: "projects", label: "Projects", icon: CubeIcon }, + { id: "teams", label: "Teams", icon: UsersThreeIcon }, + { id: "date", label: "Date", icon: CalendarIcon }, +] as const; + +type FilterTab = (typeof FILTER_TABS)[number]["id"]; interface BoardHeaderProps { - title: string; - subtitle: string; - searchQuery: string; - onSearchChange: (query: string) => void; - onNewTask?: () => void; + title: string; + issueCount: number; } -export function BoardHeader({ - title, - subtitle, - searchQuery, - onSearchChange, - onNewTask, -}: BoardHeaderProps) { - return ( -
-
-

- {title} -

-

{subtitle}

-
- -
-
- - onSearchChange(e.target.value)} - placeholder="Search tasks..." - className="flex-1 bg-transparent text-foreground text-sm placeholder:text-muted-foreground focus:outline-none" - /> -
- - -
-
- ); +export function BoardHeader({ title, issueCount }: BoardHeaderProps) { + const [activeTab, setActiveTab] = useState("projects"); + + return ( +
+
+
+

+ {title} +

+ + {issueCount} Issues + +
+ + +
+ +
+
+ {FILTER_TABS.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+ + +
+
+ ); } diff --git a/apps/web/src/components/board/create-task-modal.tsx b/apps/web/src/components/board/create-task-modal.tsx index ff74cce..7b730ae 100644 --- a/apps/web/src/components/board/create-task-modal.tsx +++ b/apps/web/src/components/board/create-task-modal.tsx @@ -1,373 +1,367 @@ "use client"; -import { - CalendarBlank, - CaretDown, - Check, - Plus, - X, -} from "@phosphor-icons/react"; +import { CalendarIcon, CaretDownIcon, CheckIcon, PlusIcon, XIcon } from "@phosphor-icons/react"; import { useState } from "react"; import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "~/components/ui/dialog"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import type { Column, CreateTaskInput, TaskLabel } from "~/lib/types"; interface CreateTaskModalProps { - isOpen: boolean; - onClose: () => void; - columns: Column[]; - onSubmit: ( - data: Omit, - ) => Promise; + isOpen: boolean; + onClose: () => void; + columns: Column[]; + onSubmit: ( + data: Omit, + ) => Promise; } interface TagProps { - text: string; - color: string; - onRemove?: () => void; + text: string; + color: string; + onRemove?: () => void; } const priorities = [ - { id: "HIGH" as const, label: "High", color: "#ef4444" }, - { id: "MEDIUM" as const, label: "Medium", color: "#f59e0b" }, - { id: "LOW" as const, label: "Low", color: "#22c55e" }, - { id: "NONE" as const, label: "None", color: "transparent" }, + { id: "HIGH" as const, label: "High", color: "#ef4444" }, + { id: "MEDIUM" as const, label: "Medium", color: "#f59e0b" }, + { id: "LOW" as const, label: "Low", color: "#22c55e" }, + { id: "NONE" as const, label: "None", color: "transparent" }, ]; const dueDates = [ - { - id: "today", - label: "Today", - getValue: () => new Date().toISOString(), - }, - { - id: "tomorrow", - label: "Tomorrow", - getValue: () => new Date(Date.now() + 86400000).toISOString(), - }, - { - id: "next-week", - label: "Next Week", - getValue: () => new Date(Date.now() + 7 * 86400000).toISOString(), - }, - { - id: "next-month", - label: "Next Month", - getValue: () => new Date(Date.now() + 30 * 86400000).toISOString(), - }, - { - id: "no-date", - label: "No due date", - getValue: () => undefined, - }, + { + id: "today", + label: "Today", + getValue: () => new Date().toISOString(), + }, + { + id: "tomorrow", + label: "Tomorrow", + getValue: () => new Date(Date.now() + 86400000).toISOString(), + }, + { + id: "next-week", + label: "Next Week", + getValue: () => new Date(Date.now() + 7 * 86400000).toISOString(), + }, + { + id: "next-month", + label: "Next Month", + getValue: () => new Date(Date.now() + 30 * 86400000).toISOString(), + }, + { + id: "no-date", + label: "No due date", + getValue: () => undefined, + }, ]; function Tag({ text, color, onRemove }: TagProps) { - return ( -
- - {text} - - {onRemove && ( - - )} -
- ); + return ( +
+ + {text} + + {onRemove && ( + + )} +
+ ); } export function CreateTaskModal({ - isOpen, - onClose, - columns, - onSubmit, + isOpen, + onClose, + columns, + onSubmit, }: CreateTaskModalProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [selectedColumn, setSelectedColumn] = useState( - columns[0] ?? null, - ); - const [priority, setPriority] = useState(priorities[3]); - const [dueDate, setDueDate] = useState(dueDates[4]); - const [tags, setTags] = useState([]); - const [isSubmitting, setIsSubmitting] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [selectedColumn, setSelectedColumn] = useState( + columns[0] ?? null, + ); + const [priority, setPriority] = useState(priorities[3]); + const [dueDate, setDueDate] = useState(dueDates[4]); + const [tags, setTags] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); - const resetForm = () => { - setName(""); - setDescription(""); - setSelectedColumn(columns[0] ?? null); - setPriority(priorities[3]); - setDueDate(dueDates[4]); - setTags([]); - }; + const resetForm = () => { + setName(""); + setDescription(""); + setSelectedColumn(columns[0] ?? null); + setPriority(priorities[3]); + setDueDate(dueDates[4]); + setTags([]); + }; - const handleSubmit = async () => { - if (!name.trim() || !selectedColumn) return; + const handleSubmit = async () => { + if (!name.trim() || !selectedColumn) return; - setIsSubmitting(true); - try { - await onSubmit({ - title: name, - description: description || undefined, - priority: priority.id, - dueDate: dueDate.getValue(), - labels: tags.length > 0 ? tags : undefined, - columnId: selectedColumn.id, - }); - resetForm(); - onClose(); - } finally { - setIsSubmitting(false); - } - }; + setIsSubmitting(true); + try { + await onSubmit({ + title: name, + description: description || undefined, + priority: priority.id, + dueDate: dueDate.getValue(), + labels: tags.length > 0 ? tags : undefined, + columnId: selectedColumn.id, + }); + resetForm(); + onClose(); + } finally { + setIsSubmitting(false); + } + }; - const removeTag = (index: number) => { - setTags(tags.filter((_, i) => i !== index)); - }; + const removeTag = (index: number) => { + setTags(tags.filter((_, i) => i !== index)); + }; - const handleClose = () => { - resetForm(); - onClose(); - }; + const handleClose = () => { + resetForm(); + onClose(); + }; - return ( - !open && handleClose()}> - - - - Create new task - - - - - + return ( + !open && handleClose()}> + + + + Create new task + + + + + -
-
- - setName(e.target.value)} - placeholder="Enter task name..." - className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" - /> -
+
+
+ + setName(e.target.value)} + placeholder="Enter task name..." + className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" + /> +
-
- -