Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
4 changes: 2 additions & 2 deletions apps/web/src/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export default function AuthenticatedLayout({
<OrgGuard>
<CommandPaletteProvider>
<SidebarProvider>
<div className="flex h-screen overflow-hidden bg-background">
<div className="flex h-screen overflow-hidden bg-sidebar">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<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 />
Expand Down
28 changes: 11 additions & 17 deletions apps/web/src/app/(authenticated)/task/[taskId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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"
>
<ArrowLeft size={16} className="text-foreground" />
<ArrowLeftIcon size={16} className="text-foreground" />
</Link>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Project Overview</span>
Expand All @@ -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)}
>
<PencilSimple size={16} />
<PencilSimpleIcon size={16} />
Edit
</button>
<DropdownMenu>
<DropdownMenuTrigger className="flex size-9 items-center justify-center rounded-lg border border-border bg-card transition-colors hover:border-foreground/20 hover:bg-accent">
<DotsThree
size={20}
weight="bold"
className="text-foreground"
/>
<DotsThreeIcon size={20} weight="bold" className="text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-40 rounded-lg border border-border bg-popover p-1"
Expand All @@ -167,7 +163,7 @@ export default function TaskDetailsPage({ params }: PageProps) {
className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-destructive hover:bg-destructive/10 focus:bg-destructive/10 focus:text-destructive"
onClick={handleDelete}
>
<Trash size={16} />
<TrashIcon size={16} />
<span className="text-sm">Delete task</span>
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down Expand Up @@ -285,7 +281,7 @@ export default function TaskDetailsPage({ params }: PageProps) {
Due Date
</span>
<div className="flex items-center gap-2">
<CalendarBlank size={16} className="text-muted-foreground" />
<CalendarIcon size={16} className="text-muted-foreground" />
<span className="font-medium text-foreground text-sm">
{formatDate(task.dueDate)}
</span>
Expand All @@ -310,9 +306,7 @@ export default function TaskDetailsPage({ params }: PageProps) {
</div>

<div className="flex items-center justify-between">
<span className="text-[13px] text-muted-foreground">
Status
</span>
<span className="text-[13px] text-muted-foreground">Status</span>
<div className="flex items-center gap-2">
<div
className="size-2 rounded-full"
Expand Down
149 changes: 122 additions & 27 deletions apps/web/src/components/board/add-column.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
"use client";

import { Check, Plus, X } from "@phosphor-icons/react";
import { CheckIcon, PlusIcon, XIcon } from "@phosphor-icons/react";
import type { ComponentType, SVGProps } from "react";
import { useRef, useState } from "react";
import {
BacklogIcon,
DoneIcon,
InProgressIcon,
ReviewIcon,
TodoIcon,
} from "~/components/icons";
import { cn } from "~/lib/utils";

interface AddColumnProps {
onAdd: (name: string, color?: string) => void;
onAdd: (name: string, color?: string, description?: string) => void;
isLoading?: boolean;
}

type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;

const COLUMN_COLORS = [
{ id: "blue", color: "#3b82f6" },
{ id: "yellow", color: "#eab308" },
Expand All @@ -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<SelectionMode>("icon");
const [selectedIcon, setSelectedIcon] = useState(COLUMN_ICONS[0].id);
const [selectedColor, setSelectedColor] = useState(COLUMN_COLORS[0].color);
const inputRef = useRef<HTMLInputElement>(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);
};
Expand Down Expand Up @@ -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"
/>

<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs">Color</span>
<div className="flex flex-wrap gap-1.5">
{COLUMN_COLORS.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setSelectedColor(c.color)}
className={cn(
"flex size-6 items-center justify-center rounded-md transition-all",
selectedColor === c.color
? "ring-2 ring-foreground ring-offset-1 ring-offset-background"
: "hover:scale-110",
)}
style={{ backgroundColor: c.color }}
>
{selectedColor === c.color && (
<Check size={12} weight="bold" className="text-white" />
)}
</button>
))}
</div>
<input
type="text"
value={description}
onChange={(e) => 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 */}
<div className="flex gap-1 rounded-lg bg-accent/50 p-0.5">
<button
type="button"
onClick={() => setMode("icon")}
className={cn(
"flex-1 rounded-md px-2 py-1 text-xs transition-colors",
mode === "icon"
? "bg-background font-medium text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
Icon
</button>
<button
type="button"
onClick={() => setMode("color")}
className={cn(
"flex-1 rounded-md px-2 py-1 text-xs transition-colors",
mode === "color"
? "bg-background font-medium text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
Color
</button>
</div>

{mode === "icon" ? (
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs">Icon</span>
<div className="flex flex-wrap gap-1.5">
{COLUMN_ICONS.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
type="button"
onClick={() => setSelectedIcon(item.id)}
title={item.label}
className={cn(
"flex size-7 items-center justify-center rounded-md transition-all",
selectedIcon === item.id
? "bg-accent ring-2 ring-foreground ring-offset-1 ring-offset-background"
: "text-muted-foreground hover:bg-accent hover:text-foreground",
)}
>
<Icon width={18} height={18} />
</button>
);
})}
</div>
</div>
) : (
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs">Color</span>
<div className="flex flex-wrap gap-1.5">
{COLUMN_COLORS.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setSelectedColor(c.color)}
className={cn(
"flex size-6 items-center justify-center rounded-md transition-all",
selectedColor === c.color
? "ring-2 ring-foreground ring-offset-1 ring-offset-background"
: "hover:scale-110",
)}
style={{ backgroundColor: c.color }}
>
{selectedColor === c.color && (
<CheckIcon size={12} weight="bold" className="text-white" />
)}
</button>
))}
</div>
</div>
)}

<div className="flex gap-2">
<button
type="button"
Expand All @@ -104,7 +199,7 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) {
onClick={handleCancel}
className="flex size-8 items-center justify-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<X size={14} />
<XIcon size={14} />
</button>
</div>
</div>
Expand All @@ -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"
>
<Plus size={14} />
<PlusIcon size={14} />
<span className="text-sm">Add column</span>
</button>
);
Expand Down
Loading