Skip to content

Commit 7dfdaf9

Browse files
committed
modular cards
1 parent be3cc4f commit 7dfdaf9

File tree

6 files changed

+415
-97
lines changed

6 files changed

+415
-97
lines changed
Lines changed: 177 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,184 @@
1-
"use client";
2-
3-
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
4-
import { ScrollArea } from "@/components/ui/scroll-area-fixed";
5-
import { DisplayCard, AddCard } from "./card";
6-
import { CardColumn } from "@/api-client/models/CardColumn";
7-
import { Data, Metadata } from "./interfaces";
8-
import { Dispatch } from "react";
9-
import { SetStateAction } from "react";
10-
11-
// Take in cards as children
12-
const Column = ({ title, children }: { title: string; children: React.ReactNode }) => {
13-
return (
14-
<div className="w-full h-fit max-h-full flex flex-col gap-3 bg-gradient-to-b from-card to-background rounded-lg border shadow-lg transition-all duration-200 hover:shadow-xl">
15-
<div className="p-3 border-b flex justify-center bg-gradient-to-r from-card-foreground/90 to-card-foreground rounded-t-lg">
16-
<h2 className="text-lg font-semibold text-card tracking-wide">{title}</h2>
17-
</div>
18-
<ScrollArea className="max-h-full flex px-3 pb-3">
19-
<div className="flex flex-col gap-3">{children}</div>
20-
</ScrollArea>
1+
"use client"
2+
3+
import type React from "react"
4+
5+
import { useState } from "react"
6+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
7+
import { ScrollArea } from "@/components/ui/scroll-area"
8+
import { Button } from "@/components/ui/button"
9+
import { MoreHorizontal, Plus, ChevronDown, ChevronRight } from "lucide-react"
10+
import { DisplayCard, AddCard } from "./card"
11+
import { CardColumn } from "@/api-client/models/CardColumn"
12+
import type { Data, Metadata } from "./interfaces"
13+
import type { Dispatch, SetStateAction } from "react"
14+
import { cn } from "@/lib/utils"
15+
16+
const Column = ({
17+
title,
18+
children,
19+
count,
20+
column,
21+
boardId,
22+
setMetadata,
23+
}: {
24+
title: string
25+
children: React.ReactNode
26+
count: number,
27+
column: CardColumn,
28+
boardId: number,
29+
setMetadata: Dispatch<SetStateAction<Metadata>>
30+
}) => {
31+
if (count === 0) return null
32+
33+
return (
34+
<div className="flex flex-col min-w-[320px] max-w-[400px] flex-1">
35+
<div className="flex items-center justify-between px-3 py-2 mb-3">
36+
<div className="flex items-center gap-2">
37+
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
38+
<h2 className="text-sm font-medium text-foreground">{title}</h2>
39+
<span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">{count}</span>
2140
</div>
22-
);
23-
};
41+
<div className="flex items-center gap-1">
42+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
43+
<MoreHorizontal className="h-3 w-3" />
44+
</Button>
45+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
46+
<AddCard
47+
boardId={boardId}
48+
column={column}
49+
setMetadata={setMetadata}
50+
/>
51+
</Button>
52+
</div>
53+
</div>
54+
<ScrollArea className="flex-1">
55+
<div className="flex flex-col gap-2 px-3 pb-4">{children}</div>
56+
</ScrollArea>
57+
</div>
58+
)
59+
}
2460

61+
const HiddenColumnsPanel = ({
62+
hiddenColumns,
63+
}: {
64+
hiddenColumns: Array<{ name: string; count: number; column: CardColumn }>
65+
}) => {
66+
const [isExpanded, setIsExpanded] = useState(false)
67+
68+
return (
69+
<div className="min-w-[240px] border-l border-border bg-card/50">
70+
<div className="p-3">
71+
<Button
72+
variant="ghost"
73+
size="sm"
74+
onClick={() => setIsExpanded(!isExpanded)}
75+
className="w-full justify-start gap-2 text-sm font-medium"
76+
>
77+
{isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
78+
Hidden columns
79+
</Button>
80+
81+
{isExpanded && (
82+
<div className="mt-2 space-y-1">
83+
{hiddenColumns.map((col) => (
84+
<Button
85+
key={col.column}
86+
variant="ghost"
87+
size="sm"
88+
className="w-full justify-between text-xs h-8 px-2"
89+
>
90+
<div className="flex items-center gap-2">
91+
<div
92+
className={cn(
93+
"w-2 h-2 rounded-full",
94+
col.column === CardColumn.InProgress && "bg-yellow-500",
95+
col.column === CardColumn.Done && "bg-green-500",
96+
col.column === CardColumn.Backlog && "bg-gray-500",
97+
col.column === CardColumn.Todo && "bg-blue-500",
98+
)}
99+
></div>
100+
<span className="text-muted-foreground">{col.name}</span>
101+
</div>
102+
<span className="text-xs text-muted-foreground">{col.count}</span>
103+
</Button>
104+
))}
105+
</div>
106+
)}
107+
</div>
108+
</div>
109+
)
110+
}
25111

26112
export const BoardColumns = ({
27-
data,
28-
setMetadata,
113+
data,
114+
setMetadata,
115+
setData,
29116
}: {
30-
data: Data;
31-
setMetadata: Dispatch<SetStateAction<Metadata>>;
117+
data: Data
118+
setMetadata: Dispatch<SetStateAction<Metadata>>
119+
setData: Dispatch<SetStateAction<Data | null>>
32120
}) => {
33-
return (
34-
<div className="h-full flex gap-6 p-6 bg-gradient-to-b from-background to-background/50">
35-
{[CardColumn.Backlog, CardColumn.Todo, CardColumn.InProgress, CardColumn.Done].map((columnValue) => (
36-
<Column key={columnValue} title={columnValue}>
37-
{
38-
columnValue === CardColumn.Backlog && (
39-
<AddCard
40-
boardId={data.board.id}
41-
setMetadata={setMetadata}
42-
className="border-2 border-card-foreground/20 hover:border-card-foreground/40 transition-colors shadow-sm hover:shadow-md h-full min-h-16"
43-
/>
44-
)
45-
46-
}
47-
{data.cards
48-
.filter((card) => card.column === columnValue)
49-
.map((card) => (
50-
<DisplayCard
51-
key={card.id}
52-
card={card}
53-
board={data.board}
54-
setMetadata={setMetadata}
55-
className="border-2 border-card-foreground/20 hover:border-card-foreground/40 transition-colors shadow-sm hover:shadow-md"
56-
N={120}
57-
/>
58-
))}
59-
</Column>
121+
122+
const allColumns = [
123+
{ column: CardColumn.Backlog, name: "Backlog" },
124+
{ column: CardColumn.Todo, name: "Todo" },
125+
{ column: CardColumn.InProgress, name: "In Progress" },
126+
{ column: CardColumn.Done, name: "Done" },
127+
]
128+
129+
const getColumnCount = (column: CardColumn) => {
130+
return data.cards.filter((card) => card.column === column).length
131+
}
132+
133+
// Columns that have 0 cards
134+
const hiddenColumns = allColumns.filter((col) => getColumnCount(col.column) === 0)
135+
.map((col) => ({
136+
name: col.name,
137+
count: getColumnCount(col.column),
138+
column: col.column,
139+
}))
140+
141+
142+
143+
return (
144+
<div className="h-full bg-background">
145+
<ResizablePanelGroup direction="horizontal" className="h-full">
146+
<ResizablePanel defaultSize={80} minSize={60}>
147+
<div className="h-full flex gap-6 p-6 overflow-x-auto">
148+
{allColumns.map(({ column, name }) => (
149+
<Column key={column} column={column} title={name} count={getColumnCount(column)} boardId={data.board.id} setMetadata={setMetadata}>
150+
{/* {column === CardColumn.Backlog && (
151+
<AddCard
152+
boardId={data.board.id}
153+
setMetadata={setMetadata}
154+
className="border border-dashed border-muted-foreground/30 hover:border-muted-foreground/50 transition-colors bg-card/50 hover:bg-card rounded-lg p-4 min-h-[60px] flex items-center justify-center text-sm text-muted-foreground hover:text-foreground"
155+
/>
156+
)} */}
157+
{data.cards
158+
.filter((card) => card.column === column)
159+
.map((card) => (
160+
<DisplayCard
161+
key={card.id}
162+
card={card}
163+
board={data.board}
164+
setMetadata={setMetadata}
165+
data={data}
166+
setData={setData}
167+
className="bg-card border border-border hover:border-border/80 transition-all duration-200 rounded-lg p-3 shadow-sm hover:shadow-md"
168+
N={120}
169+
/>
170+
))}
171+
</Column>
60172
))}
61-
</div>
62-
);
63-
};
173+
</div>
174+
</ResizablePanel>
175+
176+
<ResizableHandle />
177+
178+
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
179+
<HiddenColumnsPanel hiddenColumns={hiddenColumns}/>
180+
</ResizablePanel>
181+
</ResizablePanelGroup>
182+
</div>
183+
)
184+
}
Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
import { CardOut } from "@/api-client/models/CardOut";
22
import { DialogTrigger } from "@/components/ui/dialog";
33
import { GripVerticalIcon, PlusIcon } from "lucide-react";
4-
import { Metadata } from "./interfaces";
4+
import { Data, Metadata } from "./interfaces";
55
import { cn } from "@/lib/utils";
66
import { BoardOut } from "@/api-client/models/BoardOut";
77
import { Dispatch } from "react";
88
import { SetStateAction } from "react";
9+
import { MoreHorizontal, Plus, ChevronDown, ChevronRight } from "lucide-react"
10+
import { CardColumn } from "@/api-client";
11+
import { CardStatusDropdown } from "./card_status_dropdown";
912

1013
export function AddCard({
1114
boardId,
15+
column,
1216
setMetadata,
1317
className,
1418
}: {
1519
boardId: number;
20+
column?: CardColumn;
1621
setMetadata: Dispatch<SetStateAction<Metadata>>;
1722
className?: string;
1823
}) {
1924
const classNames = cn(
20-
"p-4 rounded-lg border border-dashed bg-background items-center justify-center flex hover:bg-accent/50 transition-all duration-200 cursor-pointer",
2125
className
2226
);
2327
return (
2428
<DialogTrigger>
2529
<div
2630
onClick={() => {
27-
setMetadata({ type: "card_add", boardId: boardId });
31+
setMetadata({ type: "card_add", boardId: boardId, column: column });
2832
}}
2933
className={classNames}
3034
>
31-
<PlusIcon className="w-8 h-8 text-foreground" />
35+
<Plus className="h-3 w-3" />
3236
</div>
3337
</DialogTrigger>
3438
);
@@ -37,44 +41,53 @@ export function AddCard({
3741
export function DisplayCard({
3842
card,
3943
board,
44+
data,
45+
setData,
4046
setMetadata,
4147
className,
4248
N = 30,
43-
}: {
44-
card: CardOut;
45-
board: BoardOut;
46-
setMetadata: Dispatch<SetStateAction<Metadata>>;
47-
className?: string;
48-
N?: number;
49-
}) {
49+
}: {
50+
card: CardOut
51+
board: BoardOut
52+
data: Data
53+
setData: Dispatch<SetStateAction<Data | null>>
54+
setMetadata: Dispatch<SetStateAction<Metadata>>
55+
className?: string
56+
N?: number
57+
}) {
5058
const classNames = cn(
51-
"flex flex-col items-start gap-2 p-2 rounded-lg border bg-background hover:bg-accent/50 transition-all duration-200 cursor-pointer shadow-sm hover:shadow-md",
52-
className
53-
);
59+
"flex flex-col items-start gap-2 p-2 rounded-lg border bg-background hover:bg-accent/50 transition-all duration-200 cursor-pointer shadow-sm hover:shadow-md group",
60+
className,
61+
)
62+
5463
return (
55-
<DialogTrigger>
56-
<div
57-
onClick={() => {
58-
setMetadata({ type: "card_view", card: card, board: board });
59-
}}
60-
className={classNames}
61-
>
62-
<div className="flex justify-between w-full">
63-
<div className="flex gap-1.5">
64-
{card.labels.sort().map((color) => (
65-
<div
66-
key={color}
67-
className={`w-3 h-3 rounded-full ring-1 ring-black/5`}
68-
style={{ backgroundColor: color }}
69-
/>
70-
))}
71-
</div>
72-
<span className="text-xs text-zinc-500 font-mono justify-end">{board.acronym}-{card.id}</span>
73-
</div>
74-
<h3 className="font-semibold justify-start text-left tracking-tight leading-snug text-foreground/80 line-clamp-2 antialiased">
75-
{card.title.length > N ? card.title.slice(0, N) + "..." : card.title}
76-
</h3>
64+
<DialogTrigger>
65+
<div
66+
onClick={() => {
67+
setMetadata({ type: "card_view", card: card, board: board })
68+
}}
69+
className={classNames}
70+
>
71+
<div className="flex justify-between w-full">
72+
<span className="text-xs text-zinc-500 font-mono justify-end">
73+
{board.acronym}-{card.id}
74+
</span>
75+
<div className="flex gap-1.5">
76+
{card.labels.sort().map((color) => (
77+
<div
78+
key={color}
79+
className={`w-3 h-3 rounded-full ring-1 ring-black/5`}
80+
style={{ backgroundColor: color }}
81+
/>
82+
))}
83+
<CardStatusDropdown card={card} data={data} setData={setData} />
7784
</div>
78-
</DialogTrigger>
79-
);
80-
}
85+
</div>
86+
<h3 className="font-semibold justify-start text-left tracking-tight leading-snug text-foreground/80 line-clamp-2 antialiased">
87+
{card.title.length > N ? card.title.slice(0, N) + "..." : card.title}
88+
</h3>
89+
</div>
90+
</DialogTrigger>
91+
)
92+
}
93+

0 commit comments

Comments
 (0)