Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import React from 'react';
import React, { useState, useCallback } from 'react';
import { Outlet, Navigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import Sidebar from './Sidebar';

export default function AppLayout() {
const { token } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false);
const closeSidebar = useCallback(() => setSidebarOpen(false), []);
if (!token) return <Navigate to="/login" replace />;

return (
<div className="flex min-h-screen bg-gray-50">
<Sidebar />
<main className="flex-1 ml-56 p-8 overflow-x-hidden min-w-0">
<Outlet />
</main>
{sidebarOpen && (
<div
className="fixed inset-0 z-30 bg-black/40 md:hidden"
onClick={closeSidebar}
/>
)}
<Sidebar open={sidebarOpen} onClose={closeSidebar} />
<div className="flex-1 flex flex-col md:ml-56 min-w-0">
<header className="md:hidden flex items-center gap-3 px-4 py-3 bg-white border-b border-gray-100 sticky top-0 z-20">
<button
onClick={() => setSidebarOpen(true)}
className="p-1 rounded-lg hover:bg-gray-100 text-gray-600"
aria-label="Open menu"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<span className="font-semibold text-gray-900 text-sm">Chuva Bot</span>
</header>
<main className="flex-1 p-4 md:p-8 overflow-x-hidden min-w-0">
<Outlet />
</main>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import { NavLink, useParams, useNavigate } from 'react-router-dom';
import React, { useEffect } from 'react';
import { NavLink, useParams, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import { useProject } from '../../context/ProjectContext';

function NavItem({ to, children }) {
function NavItem({ to, children, onClick }) {
return (
<NavLink
to={to}
onClick={onClick}
className={({ isActive }) =>
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
isActive ? 'bg-gray-900 text-white font-medium' : 'text-gray-600 hover:bg-gray-100'
Expand All @@ -18,21 +19,40 @@ function NavItem({ to, children }) {
);
}

export default function Sidebar() {
export default function Sidebar({ open, onClose }) {
const { user, logout } = useAuth();
const { project, userRole } = useProject();
const { projectId } = useParams();
const navigate = useNavigate();
const location = useLocation();

useEffect(() => {
onClose();
}, [location.pathname, onClose]);

const handleLogout = () => {
logout();
navigate('/login');
};

return (
<aside className="fixed inset-y-0 left-0 w-56 bg-white border-r border-gray-100 flex flex-col z-40">
<div className="px-4 py-5 border-b border-gray-100">
<aside className={`
fixed inset-y-0 left-0 w-56 bg-white border-r border-gray-100 flex flex-col z-40
transition-transform duration-300
${open ? 'translate-x-0' : '-translate-x-full'}
md:translate-x-0
`}>
<div className="px-4 py-5 border-b border-gray-100 flex items-center justify-between">
<span className="font-bold text-gray-900">Chuva Bot</span>
<button
onClick={onClose}
className="md:hidden p-1 rounded-lg hover:bg-gray-100 text-gray-400"
aria-label="Close menu"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function WorkloadTable({ sprintId }) {
<tr className="text-gray-400 border-b border-gray-100">
<th className="text-left py-2 pr-3 font-medium">Member</th>
{STATUSES.map((s) => (
<th key={s} className="text-center py-2 px-2 font-medium whitespace-nowrap">
<th key={s} className="text-center py-2 px-2 font-medium whitespace-nowrap hidden sm:table-cell">
{s.replace('_', ' ')}
</th>
))}
Expand All @@ -65,7 +65,7 @@ export default function WorkloadTable({ sprintId }) {
<tr key={member.userId} className={i % 2 === 0 ? 'bg-gray-50' : ''}>
<td className="py-2 pr-3 font-medium text-gray-700 whitespace-nowrap">{member.fullName}</td>
{STATUSES.map((s) => (
<td key={s} className="py-2 px-2 text-center text-gray-600">
<td key={s} className="py-2 px-2 text-center text-gray-600 hidden sm:table-cell">
{getValue(member, s)}
</td>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export default function MemberList({ members, managerId, currentUserId, isManage
const isSelf = m.id === currentUserId;
return (
<div key={m.id} className="flex items-center justify-between bg-white border border-gray-100 rounded-lg px-4 py-3">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 min-w-0">
{isThisManager && <CrownIcon />}
<div>
<p className="text-sm font-medium text-gray-800">{m.fullName}</p>
<p className="text-xs text-gray-400">{m.email}</p>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">{m.fullName}</p>
<p className="text-xs text-gray-400 truncate">{m.email}</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 shrink-0">
{isThisManager && (
<span className="text-xs font-semibold text-amber-600 bg-amber-50 px-2 py-0.5 rounded">Manager</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ export default function SprintList({ sprints, isManager, onActivate, onClose, on
<div className="w-full overflow-hidden">
<div
ref={ref}
className="flex gap-4 overflow-x-auto pb-2 select-none"
className="flex flex-col sm:flex-row gap-4 sm:overflow-x-auto pb-2 select-none"
style={{ cursor: 'grab', scrollbarWidth: 'thin' }}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={stopDrag}
onMouseLeave={stopDrag}
>
{sprints.map((s) => (
<div key={s.id} className="w-72 shrink-0">
<div key={s.id} className="w-full sm:w-72 sm:shrink-0">
<SprintCard sprint={s} isManager={isManager} onActivate={onActivate} onClose={onClose} onReopen={onReopen} onSelect={onSelect} />
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import React, { useState, useRef } from 'react';
import KanbanColumn from './KanbanColumn';
import { useIsMobile } from '../../hooks/useIsMobile';

const STATUSES = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];

const LABELS = {
TODO: 'To Do',
IN_PROGRESS: 'In Progress',
BLOCKED: 'Blocked',
DONE: 'Done',
};

export default function KanbanBoard({ tasks, onTaskClick, onStatusChange }) {
const [draggingTaskId, setDraggingTaskId] = useState(null);
const [activeColumn, setActiveColumn] = useState('TODO');
const suppressNextClick = useRef(false);
const isMobile = useIsMobile(768);

const byStatus = STATUSES.reduce((acc, s) => {
acc[s] = tasks.filter((t) => t.status === s);
Expand Down Expand Up @@ -39,6 +49,40 @@ export default function KanbanBoard({ tasks, onTaskClick, onStatusChange }) {
}
};

if (isMobile) {
return (
<div>
<div className="flex border-b border-gray-200 mb-4 overflow-x-auto">
{STATUSES.map((s) => (
<button
key={s}
onClick={() => setActiveColumn(s)}
className={`px-4 py-2 text-sm font-medium whitespace-nowrap border-b-2 transition-colors flex-shrink-0 ${
activeColumn === s
? 'border-gray-900 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{LABELS[s]}
<span className="ml-1.5 text-xs text-gray-400">{byStatus[s].length}</span>
</button>
))}
</div>
<KanbanColumn
key={activeColumn}
status={activeColumn}
tasks={byStatus[activeColumn]}
onTaskClick={onTaskClick}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
draggingTaskId={draggingTaskId}
mobileMode
/>
</div>
);
}

return (
<div className="flex gap-4 overflow-x-auto pb-4" onClickCapture={handleClickCapture}>
{STATUSES.map((s) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const HOVER_BG = {
DONE: 'bg-green-50',
};

export default function KanbanColumn({ status, tasks, onTaskClick, onDragStart, onDragEnd, onDrop, draggingTaskId }) {
export default function KanbanColumn({ status, tasks, onTaskClick, onDragStart, onDragEnd, onDrop, draggingTaskId, mobileMode = false }) {
const [isOver, setIsOver] = useState(false);

const handleDragOver = (e) => {
Expand All @@ -36,10 +36,10 @@ export default function KanbanColumn({ status, tasks, onTaskClick, onDragStart,

return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex flex-col min-w-[220px] w-full rounded-xl border-t-4 ${COLORS[status]} p-3 gap-2 transition-colors ${isOver ? HOVER_BG[status] : 'bg-gray-50'}`}
onDragOver={!mobileMode ? handleDragOver : undefined}
onDragLeave={!mobileMode ? handleDragLeave : undefined}
onDrop={!mobileMode ? handleDrop : undefined}
className={`flex flex-col ${mobileMode ? 'w-full' : 'min-w-[220px] w-full'} rounded-xl border-t-4 ${COLORS[status]} p-3 gap-2 transition-colors ${isOver ? HOVER_BG[status] : 'bg-gray-50'}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">{status.replace('_', ' ')}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export default function TaskDetail({ task, onClose, onStatusChange, onEdit, onDe

return (
<div className="space-y-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="min-w-0">
<h3 className="text-base font-semibold text-gray-900">{task.taskName}</h3>
{task.description && <p className="mt-1 text-sm text-gray-500">{task.description}</p>}
</div>
Expand All @@ -67,8 +67,8 @@ export default function TaskDetail({ task, onClose, onStatusChange, onEdit, onDe
</div>

{canChangeStatus && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Change status:</span>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-gray-500 w-full sm:w-auto">Change status:</span>
{['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'].map((s) => (
<button
key={s}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function TaskForm({ initial = {}, sprints = [], members = [], onS
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-300 resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select value={form.status} onChange={set('status')} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-300">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export default function TaskTable({ tasks, onTaskClick }) {
<tr className="text-left text-xs text-gray-400 uppercase tracking-wider border-b border-gray-100">
<th className="pb-3 pr-4">Name</th>
<th className="pb-3 pr-4">Status</th>
<th className="pb-3 pr-4">Priority</th>
<th className="pb-3 pr-4">SP</th>
<th className="pb-3 pr-4">Assigned</th>
<th className="pb-3">Sprint</th>
<th className="pb-3 pr-4 hidden sm:table-cell">Priority</th>
<th className="pb-3 pr-4 hidden md:table-cell">SP</th>
<th className="pb-3 pr-4 hidden sm:table-cell">Assigned</th>
<th className="pb-3 hidden md:table-cell">Sprint</th>
</tr>
</thead>
<tbody>
Expand All @@ -28,10 +28,10 @@ export default function TaskTable({ tasks, onTaskClick }) {
>
<td className="py-3 pr-4 font-medium text-gray-800">{t.taskName}</td>
<td className="py-3 pr-4"><Badge value={t.status} /></td>
<td className="py-3 pr-4">{t.priority ? <Badge value={t.priority} /> : <span className="text-gray-300">—</span>}</td>
<td className="py-3 pr-4 text-gray-500">{t.storyPoints}</td>
<td className="py-3 pr-4 text-gray-500">{t.assignedTo?.fullName || <span className="text-gray-300">—</span>}</td>
<td className="py-3 text-gray-500">{t.sprint?.sprintName || <span className="text-gray-300">Backlog</span>}</td>
<td className="py-3 pr-4 hidden sm:table-cell">{t.priority ? <Badge value={t.priority} /> : <span className="text-gray-300">—</span>}</td>
<td className="py-3 pr-4 text-gray-500 hidden md:table-cell">{t.storyPoints}</td>
<td className="py-3 pr-4 text-gray-500 hidden sm:table-cell">{t.assignedTo?.fullName || <span className="text-gray-300">—</span>}</td>
<td className="py-3 text-gray-500 hidden md:table-cell">{t.sprint?.sprintName || <span className="text-gray-300">Backlog</span>}</td>
</tr>
))}
</tbody>
Expand Down
11 changes: 11 additions & 0 deletions MtdrSpring/backend/src/main/frontend/src/hooks/useIsMobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useState, useEffect } from 'react';

export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState(() => window.innerWidth < breakpoint);
useEffect(() => {
const handler = () => setIsMobile(window.innerWidth < breakpoint);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [breakpoint]);
return isMobile;
}
20 changes: 10 additions & 10 deletions MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export default function DashboardPage() {

return (
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
<h1 className="text-xl font-bold text-gray-900">Dashboard — {project.projectName}</h1>
<select
value={selectedSprintId ?? ''}
onChange={(e) => setSelectedSprintId(e.target.value ? Number(e.target.value) : null)}
className="border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none max-w-[220px]"
className="border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none w-full sm:max-w-[220px]"
>
<option value="">No sprint selected</option>
{sprints.map((s) => (
Expand All @@ -68,10 +68,10 @@ export default function DashboardPage() {
</select>
</div>

<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-6 gap-4">

{/* Row 1 — full width */}
<Widget title="Sprint Summary" className="md:col-span-6">
<Widget title="Sprint Summary" className="sm:col-span-2 md:col-span-6">
<SprintSummary sprintId={sid} />
</Widget>

Expand All @@ -87,28 +87,28 @@ export default function DashboardPage() {
</Widget>

{/* Row 3 — two halves */}
<Widget title="Velocity (SP per sprint)" className="md:col-span-3">
<Widget title="Velocity (SP per sprint)" className="sm:col-span-2 md:col-span-3">
<VelocityChart />
</Widget>
<Widget title="Efficiency (SP vs Hours)" className="md:col-span-3">
<Widget title="Efficiency (SP vs Hours)" className="sm:col-span-2 md:col-span-3">
<EfficiencyChart sprintId={sid} />
</Widget>

{/* Row 4 — two halves */}
<Widget title="Workload" className="md:col-span-3">
<Widget title="Workload" className="sm:col-span-2 md:col-span-3">
<WorkloadTable sprintId={sid} />
</Widget>
<Widget title="Hours per Member" className="md:col-span-3">
<Widget title="Hours per Member" className="sm:col-span-2 md:col-span-3">
<HoursPerMember sprintId={sid} />
</Widget>

{/* Row 5 — full width */}
<Widget title="Backlog Summary" className="md:col-span-6">
<Widget title="Backlog Summary" className="sm:col-span-2 md:col-span-6">
<BacklogSummary />
</Widget>

{isManager && (
<Widget title="Manager actions" className="md:col-span-6">
<Widget title="Manager actions" className="sm:col-span-2 md:col-span-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{[
{ label: 'Manage members', path: 'members' },
Expand Down
Loading
Loading