diff --git a/apps/web/src/App.css b/apps/web/src/App.css index ad29ecd..64ff49e 100644 --- a/apps/web/src/App.css +++ b/apps/web/src/App.css @@ -1,51 +1,60 @@ main { - max-width: 1400px; + max-width: 960px; margin: 0 auto; - padding: 40px 20px; + padding: 48px 24px; min-height: 100vh; } @media (max-width: 768px) { main { - padding: 20px 12px; + padding: 24px 16px; } } h1 { - font-size: 42px; + font-size: 28px; font-weight: 700; - margin-bottom: 16px; - color: #2c3e50; + color: #111; + margin: 0 0 8px 0; + letter-spacing: -0.5px; } @media (max-width: 768px) { h1 { - font-size: 32px; + font-size: 24px; } } -.subtitle { - color: var(--text); - margin: 0; +/* Shared section title — used by TodaysTasks and Timeline */ +.section-title { + font-size: 18px; + font-weight: 600; + color: #111; + margin: 0 0 16px 0; + padding-bottom: 10px; + border-bottom: 1px solid #e8e8e8; +} + +.section-title--clickable { + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + user-select: none; +} + +.section-title--clickable:hover { + color: #333; } .status { display: inline-block; margin-bottom: 40px; - padding: 8px 16px; - border-radius: 6px; - font-size: 13px; - font-weight: 500; -} - -.status--loading { + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + color: #888; background: #f5f5f5; - color: #666; -} - -.status--ok { - background: #f5f5f5; - color: #666; } .status--error { @@ -57,47 +66,43 @@ h1 { position: fixed; top: 20px; right: 20px; - background: #ffebee; - color: #c62828; + background: white; + color: #111; + border: 1px solid #e8e8e8; padding: 12px 16px; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); z-index: 1000; display: flex; align-items: center; gap: 12px; - max-width: 400px; - animation: slideIn 0.3s ease; + max-width: 380px; + font-size: 14px; + animation: slideIn 0.2s ease; } @media (max-width: 768px) { .error-message { - left: 12px; - right: 12px; + left: 16px; + right: 16px; max-width: none; } } @keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } + from { transform: translateY(-8px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } } .error-dismiss { background: none; border: none; - color: #c62828; + color: #999; cursor: pointer; - font-size: 18px; + font-size: 16px; padding: 0; - width: 24px; - height: 24px; + width: 20px; + height: 20px; display: flex; align-items: center; justify-content: center; @@ -105,7 +110,7 @@ h1 { } .error-dismiss:hover { - opacity: 0.7; + color: #111; } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7b00a63..1b0d401 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -122,13 +122,13 @@ function App() {

Loading tasks...

) : ( <> - + )} diff --git a/apps/web/src/components/CreateTaskForm.css b/apps/web/src/components/CreateTaskForm.css index 46988ad..41ae9e5 100644 --- a/apps/web/src/components/CreateTaskForm.css +++ b/apps/web/src/components/CreateTaskForm.css @@ -1,6 +1,6 @@ .create-task-form { width: 100%; - max-width: 600px; + max-width: 100%; margin: 0 auto; } diff --git a/apps/web/src/components/Timeline.css b/apps/web/src/components/Timeline.css index db60258..b804acd 100644 --- a/apps/web/src/components/Timeline.css +++ b/apps/web/src/components/Timeline.css @@ -1,141 +1,120 @@ -.timeline-container { - width: 100%; - padding: 40px 20px; - background: #fafafa; - border-radius: 12px; - margin-bottom: 30px; - overflow-x: auto; -} - -@media (max-width: 768px) { - .timeline-container { - padding: 24px 12px; - margin-bottom: 20px; - } -} - -.timeline { - position: relative; - height: 150px; - margin: 0 auto; - max-width: 1200px; - min-width: 600px; -} - -@media (max-width: 768px) { - .timeline { - height: 120px; - min-width: 500px; - } -} - -.timeline-line { - position: absolute; - top: 50%; - left: 0; - right: 0; - height: 2px; - background: #e0e0e0; - border-radius: 2px; -} - -.today-marker { - position: absolute; - top: 50%; - transform: translateX(-50%); - z-index: 100; -} - -.today-arrow { - font-size: 28px; - color: #e74c3c; - text-align: center; - line-height: 0; - margin-bottom: -6px; -} - -@media (max-width: 768px) { - .today-arrow { - font-size: 24px; - } -} - -.today-label { - background: #e74c3c; - color: white; - padding: 4px 10px; - border-radius: 4px; - font-weight: 600; - font-size: 11px; - text-align: center; - white-space: nowrap; -} - -@media (max-width: 768px) { - .today-label { - font-size: 10px; - padding: 3px 8px; - } -} - -.timeline-day-group { - position: absolute; - top: 0; - transform: translateX(-50%); +.upcoming-section { + margin-top: 48px; + padding-top: 32px; + border-top: 1px solid #e8e8e8; +}.upcoming-list { display: flex; flex-direction: column; + border: 1px solid #e8e8e8; + border-radius: 6px; + overflow: hidden; +} + +.upcoming-list--scrollable { + max-height: calc(5 * 49px); + overflow-y: auto; +} + +.upcoming-row { + display: grid; + grid-template-columns: 1fr auto auto; align-items: center; - gap: 6px; + gap: 16px; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + background: white; + transition: background 0.15s; } -.timeline-date { - font-size: 10px; - color: #999; - font-weight: 500; +.upcoming-row:last-child { + border-bottom: none; } -@media (max-width: 768px) { - .timeline-date { - font-size: 9px; - } +.upcoming-row:hover { + background: #fafafa; } -.timeline-tasks { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - margin-top: 40px; +.upcoming-name { + position: relative; + min-width: 0; + cursor: default; } -@media (max-width: 768px) { - .timeline-tasks { - margin-top: 32px; - } -} - -.timeline-task { - background: #555; - color: white; - padding: 4px 8px; - border-radius: 8px; - font-size: 10px; +.upcoming-name-text { + display: block; white-space: nowrap; - max-width: 100px; overflow: hidden; text-overflow: ellipsis; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - position: relative; + font-size: 14px; + color: #111; +} + +.upcoming-badge { + font-size: 11px; + color: #888; + white-space: nowrap; + border: 1px solid #e8e8e8; + padding: 2px 8px; + border-radius: 10px; +} + +.upcoming-due { + font-size: 13px; + color: #111; + font-weight: 500; + white-space: nowrap; + min-width: 80px; + text-align: right; +} + +.no-upcoming { + color: #aaa; + font-size: 14px; + font-style: italic; + padding: 16px 0; +} + +/* shared tooltip style */ +.name-tooltip { + position: absolute; + bottom: calc(100% + 6px); + left: 0; + background: #111; + color: white; + font-size: 12px; + padding: 6px 10px; + border-radius: 4px; + white-space: nowrap; + max-width: 280px; + overflow: hidden; + text-overflow: ellipsis; + z-index: 100; + pointer-events: none; +} + +.name-tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 12px; + border: 5px solid transparent; + border-top-color: #111; } @media (max-width: 768px) { - .timeline-task { - font-size: 9px; - padding: 3px 6px; - max-width: 80px; + .upcoming-row { + grid-template-columns: 1fr auto; + gap: 4px 12px; + padding: 10px 12px; } -} -.timeline-task.essential { - background: #2c3e50; -} + .upcoming-badge { + display: none; + } + + .upcoming-due { + font-size: 12px; + min-width: auto; + text-align: right; + } +} \ No newline at end of file diff --git a/apps/web/src/components/Timeline.tsx b/apps/web/src/components/Timeline.tsx index 1e8057a..b0ed066 100644 --- a/apps/web/src/components/Timeline.tsx +++ b/apps/web/src/components/Timeline.tsx @@ -1,101 +1,78 @@ +import { useState } from 'react'; import type { Task } from '../types'; -import { calculateNextDueDate, formatDate } from '../utils/taskUtils'; +import { calculateNextDueDate } from '../utils/taskUtils'; import './Timeline.css'; interface TimelineProps { tasks: Task[]; } +const MAX_VISIBLE = 5; + export default function Timeline({ tasks }: TimelineProps) { const today = new Date(); today.setHours(0, 0, 0, 0); - // Timeline range: 15 days past, 30 days future - const startDate = new Date(today); - startDate.setDate(startDate.getDate() - 15); - - const endDate = new Date(today); - endDate.setDate(endDate.getDate() + 30); + const upcomingItems = tasks + .map(task => { + const dueDate = calculateNextDueDate(task); + if (!dueDate) return null; + const due = new Date(dueDate); + due.setHours(0, 0, 0, 0); + if (due <= today) return null; + return { task, dueDate: due }; + }) + .filter(Boolean) + .sort((a, b) => a!.dueDate.getTime() - b!.dueDate.getTime()) as { task: Task; dueDate: Date }[]; - // Calculate positions for tasks on timeline - const totalDays = 45; // 15 past + 30 future - const todayPosition = (15 / totalDays) * 100; // 33.33% from left + const hasMore = upcomingItems.length > MAX_VISIBLE; - const taskPositions = tasks.map(task => { - const dueDate = calculateNextDueDate(task); - if (!dueDate) return null; - - // Only show tasks within timeline range - if (dueDate < startDate || dueDate > endDate) return null; - - const daysDiff = Math.floor((dueDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); - const position = (daysDiff / totalDays) * 100; - - return { - task, - position, - dueDate, - }; - }).filter(Boolean) as { task: Task; position: number; dueDate: Date }[]; - - // Group tasks by day - const tasksByDay = taskPositions.reduce((acc, item) => { - const dateKey = item.dueDate.toDateString(); - if (!acc[dateKey]) acc[dateKey] = []; - acc[dateKey].push(item); - return acc; - }, {} as Record); - - // Sort tasks within each day by priority - Object.values(tasksByDay).forEach(dayTasks => { - dayTasks.sort((a, b) => { - if (a.task.priority === 'ESSENTIAL' && b.task.priority !== 'ESSENTIAL') return -1; - if (a.task.priority !== 'ESSENTIAL' && b.task.priority === 'ESSENTIAL') return 1; - return 0; - }); - }); + const formatDueDate = (date: Date): string => { + const diff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + if (diff === 1) return 'Tomorrow'; + if (diff <= 7) return `In ${diff} days`; + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }; return ( -
-
-
- - {/* TODAY marker */} -
-
-
TODAY
+
+

What the future holds

+ {upcomingItems.length === 0 ? ( +

No upcoming tasks scheduled

+ ) : ( +
+ {upcomingItems.map(({ task, dueDate }) => ( + + ))}
+ )} +
+ ); +} - {/* Tasks */} - {Object.entries(tasksByDay).map(([dateKey, dayTasks]) => { - const avgPosition = dayTasks.reduce((sum, item) => sum + item.position, 0) / dayTasks.length; - - return ( -
-
{formatDate(dayTasks[0].dueDate)}
-
- {dayTasks.map(({ task }, index) => ( -
- {task.name} -
- ))} -
-
- ); - })} -
+interface UpcomingRowProps { + task: Task; + dueLabel: string; +} + +function UpcomingRow({ task, dueLabel }: UpcomingRowProps) { + const [tooltipVisible, setTooltipVisible] = useState(false); + + return ( +
+ setTooltipVisible(v => !v)} + onMouseEnter={() => setTooltipVisible(true)} + onMouseLeave={() => setTooltipVisible(false)} + > + {task.name} + {tooltipVisible && {task.name}} + + + {task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'} + + {dueLabel}
); } diff --git a/apps/web/src/components/TodaysTasks.css b/apps/web/src/components/TodaysTasks.css index be40ab2..c3ebc13 100644 --- a/apps/web/src/components/TodaysTasks.css +++ b/apps/web/src/components/TodaysTasks.css @@ -1,13 +1,112 @@ .todays-tasks { width: 100%; - max-width: 800px; - margin: 0 auto 40px; } -.todays-tasks h2 { - font-size: 24px; - margin-bottom: 20px; - color: #2c3e50; +.task-section { + margin-bottom: 36px; +} + +.count-badge { + font-size: 13px; + font-weight: 500; + color: #888; + background: #f0f0f0; + border-radius: 10px; + padding: 1px 8px; +} + +.toggle-icon { + font-size: 12px; + color: #999; + margin-left: auto; +} + +.task-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +@media (max-width: 900px) { + .task-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 540px) { + .task-grid { + grid-template-columns: 1fr; + } +} + +.task-card { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border: 1px solid #e8e8e8; + border-radius: 4px; + background: white; + min-width: 0; + transition: border-color 0.15s; +} + +.task-card:hover { + border-color: #bbb; +} + +.task-card-name { + flex: 1; + min-width: 0; + position: relative; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 14px; + color: #111; +} + +.task-card-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.action-btn { + width: 28px; + height: 28px; + border: 1px solid #e8e8e8; + border-radius: 3px; + font-size: 14px; + cursor: pointer; + background: white; + color: #555; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + padding: 0; +} + +.action-btn:hover { + background: #111; + border-color: #111; + color: white; +} + +@media (max-width: 768px) { + .action-btn { + width: 34px; + height: 34px; + font-size: 16px; + } +} + +.no-tasks { + color: #aaa; + font-size: 14px; + font-style: italic; } @media (max-width: 768px) { @@ -17,162 +116,4 @@ } } -.task-section { - margin-bottom: 24px; -} -@media (max-width: 768px) { - .task-section { - margin-bottom: 16px; - } -} - -.priority-header { - font-size: 16px; - font-weight: 600; - padding: 10px 14px; - border-radius: 6px; - margin-bottom: 10px; - display: flex; - justify-content: space-between; - align-items: center; - background: #f5f5f5; - color: #555; - border-left: 3px solid #ddd; -} - -@media (max-width: 768px) { - .priority-header { - font-size: 14px; - padding: 8px 12px; - } -} - -.priority-header.essential { - background: #fafafa; - color: #2c3e50; - border-left-color: #e74c3c; -} - -.priority-header.when-i-have-time { - background: #f5f5f5; - color: #666; - border-left-color: #bbb; -} - -.priority-header.clickable { - cursor: pointer; - user-select: none; - transition: background 0.2s; -} - -.priority-header.clickable:hover { - background: #efefef; -} - -.toggle-icon { - font-size: 12px; - margin-left: 8px; - color: #999; -} - -.task-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -@media (max-width: 768px) { - .task-list { - gap: 6px; - } -} - -.task-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: white; - border: 1px solid #e0e0e0; - border-radius: 6px; - transition: all 0.2s; -} - -@media (max-width: 768px) { - .task-item { - padding: 10px 12px; - } -} - -.task-item:hover { - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); - border-color: #ccc; -} - -.task-name { - font-size: 15px; - color: #2c3e50; - flex: 1; - margin-right: 12px; -} - -@media (max-width: 768px) { - .task-name { - font-size: 14px; - } -} - -.task-actions { - display: flex; - gap: 6px; - flex-shrink: 0; -} - -.action-btn { - width: 32px; - height: 32px; - border: 1px solid #e0e0e0; - border-radius: 4px; - font-size: 16px; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - background: white; - color: #666; -} - -@media (max-width: 768px) { - .action-btn { - width: 36px; - height: 36px; - } -} - -.complete-btn:hover { - background: #f5f5f5; - border-color: #2c3e50; - color: #2c3e50; -} - -.renew-btn:hover { - background: #f5f5f5; - border-color: #666; - color: #2c3e50; -} - -.no-tasks { - color: #999; - font-style: italic; - padding: 12px 16px; - font-size: 14px; -} - -@media (max-width: 768px) { - .no-tasks { - font-size: 13px; - padding: 10px 12px; - } -} diff --git a/apps/web/src/components/TodaysTasks.tsx b/apps/web/src/components/TodaysTasks.tsx index e6a06c1..bdbbf0b 100644 --- a/apps/web/src/components/TodaysTasks.tsx +++ b/apps/web/src/components/TodaysTasks.tsx @@ -15,17 +15,15 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP return (
-

Today's Tasks

- - {/* Essential Tasks */} -
-

Essential

+ +
+

Need to do

{essentialTasks.length === 0 ? ( -

No essential tasks for today

+

Nothing essential for today

) : ( -
+
{essentialTasks.map(task => ( - )} -
+
- {/* When I Have Time Tasks */} -
-

+

setShowWhenIHaveTime(!showWhenIHaveTime)} > - When I have time ({whenIHaveTimeTasks.length}) - {showWhenIHaveTime ? '▼' : '▶'} -

+ When I have time + {whenIHaveTimeTasks.length} + {showWhenIHaveTime ? '▾' : '▸'} + {showWhenIHaveTime && ( -
- {whenIHaveTimeTasks.length === 0 ? ( -

No tasks in this category

- ) : ( - whenIHaveTimeTasks.map(task => ( - Nothing here

+ ) : ( +
+ {whenIHaveTimeTasks.map(task => ( + - )) - )} -
+ ))} +
+ ) )} -
+
); } -interface TaskItemProps { +interface TaskCardProps { task: Task; onComplete: (taskId: number) => void; onRenew: (taskId: number) => void; } -function TaskItem({ task, onComplete, onRenew }: TaskItemProps) { +function TaskCard({ task, onComplete, onRenew }: TaskCardProps) { + const [tooltipVisible, setTooltipVisible] = useState(false); + return ( -
- {task.name} -
+
+ setTooltipVisible(v => !v)} + onMouseEnter={() => setTooltipVisible(true)} + onMouseLeave={() => setTooltipVisible(false)} + > + {task.name} + {tooltipVisible && {task.name}} + +