Add right sidebar to board detail page

This commit is contained in:
david 2026-03-20 18:08:39 +03:00
parent 3711b0888d
commit 9958443c14
11 changed files with 226 additions and 111 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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={

View 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>
);
}

View 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>;
};

View 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>;
};

View file

@ -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,6 +218,7 @@ export function BoardDetail() {
return (
<div className="space-y-6">
<WidePageLayout>
<div className="flex justify-between items-start">
<div>
<Link to="/boards" className="text-gray-400 hover:text-white transition-colors text-sm">
@ -259,7 +242,11 @@ export function BoardDetail() {
</button>
</div>
</div>
</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}
@ -302,5 +289,10 @@ export function BoardDetail() {
</DragOverlay>
</DndContext>
</div>
<BoardSidebar boardId={id || ''} />
</div>
</div>
</div>
);
}

View 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>
);
}

View file

@ -1,11 +1,13 @@
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 (
<NarrowPageLayout>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
@ -38,5 +40,6 @@ export function Boards() {
</div>
)}
</div>
</NarrowPageLayout>
);
}

View file

@ -4,7 +4,10 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}

View file

@ -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: {