Add right sidebar to board detail page
This commit is contained in:
parent
3711b0888d
commit
9958443c14
11 changed files with 226 additions and 111 deletions
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
|
|
@ -23,6 +23,7 @@
|
|||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
|
|
@ -1614,6 +1615,15 @@
|
|||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
|
|
@ -7018,6 +7028,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Boards } from './pages/Boards';
|
|||
import { BoardCreate } from './pages/BoardCreate';
|
||||
import { BoardEdit } from './pages/BoardEdit';
|
||||
import { BoardDetail } from './pages/BoardDetail';
|
||||
import { BoardEpics } from './pages/BoardEpics';
|
||||
import { CardDetail } from './pages/CardDetail';
|
||||
|
||||
const App = () => {
|
||||
|
|
@ -35,7 +36,7 @@ const App = () => {
|
|||
<ModalProvider>
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
<Navbar />
|
||||
<main className="flex-1 p-8 mx-auto w-full max-w-7xl">
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
|
|
@ -84,6 +85,14 @@ const App = () => {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/boards/:id/epics"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<BoardEpics />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/boards/:id/cards/:cardId"
|
||||
element={
|
||||
|
|
|
|||
40
frontend/src/components/BoardSidebar.tsx
Normal file
40
frontend/src/components/BoardSidebar.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
interface BoardSidebarProps {
|
||||
boardId: string;
|
||||
}
|
||||
|
||||
export function BoardSidebar({ boardId }: BoardSidebarProps) {
|
||||
const location = useLocation();
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'epics', label: 'Epics', icon: '📋', path: `/boards/${boardId}/epics` },
|
||||
{ id: 'history', label: 'History', icon: '📜', path: `/boards/${boardId}/history` },
|
||||
{ id: 'documents', label: 'Documents', icon: '📄', path: `/boards/${boardId}/documents` },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-gray-800 rounded-lg p-4 border border-gray-700 h-fit sticky top-4">
|
||||
<h3 className="text-white font-bold text-lg mb-4">Board Menu</h3>
|
||||
<nav className="space-y-2">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
className={`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
frontend/src/components/NarrowPageLayout.tsx
Normal file
5
frontend/src/components/NarrowPageLayout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export const NarrowPageLayout = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="flex-1 p-8 mx-auto w-full max-w-7xl">{children}</div>;
|
||||
};
|
||||
5
frontend/src/components/WidePageLayout.tsx
Normal file
5
frontend/src/components/WidePageLayout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export const WidePageLayout = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="flex-1 p-8 mx-auto w-full max-w-7xl">{children}</div>;
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ import { useListMutations } from '../hooks/useListMutations';
|
|||
import { SortableKanbanColumn } from '../components/kanban/SortableKanbanColumn';
|
||||
import { CreateListModal } from '../components/kanban/CreateListModal';
|
||||
import { CardPreviewModal } from '../components/CardPreviewModal';
|
||||
import { BoardSidebar } from '../components/BoardSidebar';
|
||||
import { useModal } from '../context/modals/useModal';
|
||||
import {
|
||||
DndContext,
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { Card as CardType, ListWithCards } from '../types/kanban';
|
||||
import { useState } from 'react';
|
||||
import { WidePageLayout } from '@/components/WidePageLayout';
|
||||
|
||||
export function BoardDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
|
@ -65,12 +67,8 @@ export function BoardDetail() {
|
|||
const handleDragOver = (event: DragOverEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
// console.log('---handleDragOver', event);
|
||||
if (!over) return;
|
||||
|
||||
// const activeId = parseInt(active.id as string);
|
||||
// const overId = parseInt(over.id as string);
|
||||
|
||||
const overIdStr = (over.id as string).split('_')[1];
|
||||
const overId = parseInt(overIdStr, 10);
|
||||
const activeIdStr = (active.id as string).split('_')[1];
|
||||
|
|
@ -105,8 +103,6 @@ export function BoardDetail() {
|
|||
|
||||
// Handle column reordering
|
||||
if (activeType === 'COLUMN') {
|
||||
// todo find over column id,
|
||||
|
||||
let overListIndex = -1;
|
||||
const activeList = board.lists.find((l) => l.id === activeId);
|
||||
if (overType === 'CARD') {
|
||||
|
|
@ -118,12 +114,7 @@ export function BoardDetail() {
|
|||
overListIndex = board.lists.findIndex((l) => l.id === overId);
|
||||
}
|
||||
|
||||
// console.log('-------active.id', active.id)
|
||||
// console.log('-------overType.id', overType)
|
||||
// console.log('-------overListIndex', overListIndex)
|
||||
|
||||
const activeListIndex = board.lists.findIndex((l) => l.id === activeId);
|
||||
// overListIndex = board.lists.findIndex((l) => l.id === overId);
|
||||
|
||||
if (activeListIndex === -1 || overListIndex === -1 || !activeList) return;
|
||||
|
||||
|
|
@ -133,15 +124,6 @@ export function BoardDetail() {
|
|||
reorderedLists.splice(overListIndex, 0, movedList);
|
||||
|
||||
await updateList(activeList.id, { name: activeList.name, pos: overListIndex });
|
||||
|
||||
// // Update all list positions
|
||||
// for (let i = 0; i < reorderedLists.length; i++) {
|
||||
// const list = reorderedLists[i];
|
||||
// if (list.pos !== i) {
|
||||
// await updateList(list.id, { name: list.name, pos: i });
|
||||
// }
|
||||
// }
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -236,71 +218,81 @@ export function BoardDetail() {
|
|||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<Link to="/boards" className="text-gray-400 hover:text-white transition-colors text-sm">
|
||||
← Back to Boards
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-white mt-2">{board.name}</h1>
|
||||
{board.description && <p className="text-gray-400 mt-1">{board.description}</p>}
|
||||
<WidePageLayout>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<Link to="/boards" className="text-gray-400 hover:text-white transition-colors text-sm">
|
||||
← Back to Boards
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-white mt-2">{board.name}</h1>
|
||||
{board.description && <p className="text-gray-400 mt-1">{board.description}</p>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
to={`/boards/${id}/edit`}
|
||||
className="bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Edit Board
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleAddList}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
+ Add List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
to={`/boards/${id}/edit`}
|
||||
className="bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Edit Board
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleAddList}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
+ Add List
|
||||
</button>
|
||||
</WidePageLayout>
|
||||
|
||||
<div className="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-start gap-6">
|
||||
<div className="flex-1">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={board.lists.map((list) => `COLUMN_${list.id}`)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4">
|
||||
{board.lists.map((list) => (
|
||||
<SortableKanbanColumn
|
||||
key={list.id}
|
||||
list={list}
|
||||
cards={list.cards}
|
||||
onOpenCardModal={handleOpenCardModal}
|
||||
onCardCreate={handleAddCard(list.id)}
|
||||
onListEdit={(name) => handleEditList(list.id, name)}
|
||||
onListDelete={() => handleDeleteList(list.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeCard ? (
|
||||
<div className="bg-gray-700 rounded-lg p-4 cursor-grabbing border border-gray-600 shadow-lg opacity-80">
|
||||
<h3 className="text-white font-medium">{activeCard.name}</h3>
|
||||
</div>
|
||||
) : activeList ? (
|
||||
<div className="bg-gray-800 rounded-lg p-4 cursor-grabbing border border-gray-700 shadow-lg opacity-80 min-w-[300px] max-w-[300px]">
|
||||
<h2 className="text-white font-bold text-lg">{activeList.name}</h2>
|
||||
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full inline-block mt-2">
|
||||
{activeList.cards.length} cards
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
<BoardSidebar boardId={id || ''} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={board.lists.map((list) => `COLUMN_${list.id}`)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4">
|
||||
{board.lists.map((list) => (
|
||||
<SortableKanbanColumn
|
||||
key={list.id}
|
||||
list={list}
|
||||
cards={list.cards}
|
||||
onOpenCardModal={handleOpenCardModal}
|
||||
onCardCreate={handleAddCard(list.id)}
|
||||
onListEdit={(name) => handleEditList(list.id, name)}
|
||||
onListDelete={() => handleDeleteList(list.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay>
|
||||
{activeCard ? (
|
||||
<div className="bg-gray-700 rounded-lg p-4 cursor-grabbing border border-gray-600 shadow-lg opacity-80">
|
||||
<h3 className="text-white font-medium">{activeCard.name}</h3>
|
||||
</div>
|
||||
) : activeList ? (
|
||||
<div className="bg-gray-800 rounded-lg p-4 cursor-grabbing border border-gray-700 shadow-lg opacity-80 min-w-[300px] max-w-[300px]">
|
||||
<h2 className="text-white font-bold text-lg">{activeList.name}</h2>
|
||||
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full inline-block mt-2">
|
||||
{activeList.cards.length} cards
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
35
frontend/src/pages/BoardEpics.tsx
Normal file
35
frontend/src/pages/BoardEpics.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useParams, Link } from 'react-router-dom';
|
||||
import { WidePageLayout } from '../components/WidePageLayout';
|
||||
|
||||
export function BoardEpics() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<WidePageLayout>
|
||||
<div>
|
||||
<Link
|
||||
to={`/boards/${id}`}
|
||||
className="text-gray-400 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
← Back to Board
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-white mt-2">Epics</h1>
|
||||
<p className="text-gray-400 mt-1">Manage and view epics for this board</p>
|
||||
</div>
|
||||
</WidePageLayout>
|
||||
|
||||
<WidePageLayout>
|
||||
<div className="bg-gray-800 rounded-lg p-8 border border-gray-700">
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">📋</div>
|
||||
<h2 className="text-xl font-bold text-white mb-2">Epics Coming Soon</h2>
|
||||
<p className="text-gray-400">
|
||||
This page will allow you to create and manage epics for your board.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</WidePageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,42 +1,45 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useBoards } from '../hooks/useBoards';
|
||||
import { BoardCard } from '../components/kanban/BoardCard';
|
||||
import { NarrowPageLayout } from '@/components/NarrowPageLayout';
|
||||
|
||||
export function Boards() {
|
||||
const { boards, deleteBoard } = useBoards();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">My Boards</h1>
|
||||
<p className="text-gray-400">Manage your Kanban boards</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/boards/new"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
+ Create Board
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{boards.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-400 text-lg mb-4">No boards yet</p>
|
||||
<NarrowPageLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">My Boards</h1>
|
||||
<p className="text-gray-400">Manage your Kanban boards</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/boards/new"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors inline-block"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Create your first board
|
||||
+ Create Board
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{boards.map((board) => (
|
||||
<BoardCard key={board.id} board={board} onDelete={deleteBoard} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{boards.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-400 text-lg mb-4">No boards yet</p>
|
||||
<Link
|
||||
to="/boards/new"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors inline-block"
|
||||
>
|
||||
Create your first board
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{boards.map((board) => (
|
||||
<BoardCard key={board.id} board={board} onDelete={deleteBoard} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NarrowPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue