feat: removed timeline and dark theme
Build and Deploy / build-and-push (push) Successful in 55s Details

This commit is contained in:
Ludo 2026-05-04 15:35:12 +02:00
parent edb0a52e41
commit fc9bc3fe62
8 changed files with 363 additions and 509 deletions

View File

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

View File

@ -122,13 +122,13 @@ function App() {
<p>Loading tasks...</p>
) : (
<>
<Timeline tasks={tasks} />
<TodaysTasks
tasks={todaysTasks}
onComplete={handleCompleteTask}
onRenew={handleRenewTask}
/>
<CreateTaskForm onCreateTask={handleCreateTask} />
<Timeline tasks={tasks} />
</>
)}
</main>

View File

@ -1,6 +1,6 @@
.create-task-form {
width: 100%;
max-width: 600px;
max-width: 100%;
margin: 0 auto;
}

View File

@ -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;
}
.upcoming-badge {
display: none;
}
.upcoming-due {
font-size: 12px;
min-width: auto;
text-align: right;
}
}
.timeline-task.essential {
background: #2c3e50;
}

View File

@ -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);
// Calculate positions for tasks on timeline
const totalDays = 45; // 15 past + 30 future
const todayPosition = (15 / totalDays) * 100; // 33.33% from left
const taskPositions = tasks.map(task => {
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 }[];
// Only show tasks within timeline range
if (dueDate < startDate || dueDate > endDate) return null;
const hasMore = upcomingItems.length > MAX_VISIBLE;
const daysDiff = Math.floor((dueDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
const position = (daysDiff / totalDays) * 100;
return {
task,
position,
dueDate,
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' });
};
}).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<string, typeof taskPositions>);
// 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;
});
});
return (
<div className="timeline-container">
<div className="timeline">
<div className="timeline-line"></div>
{/* TODAY marker */}
<div className="today-marker" style={{ left: `${todayPosition}%` }}>
<div className="today-arrow"></div>
<div className="today-label">TODAY</div>
</div>
{/* Tasks */}
{Object.entries(tasksByDay).map(([dateKey, dayTasks]) => {
const avgPosition = dayTasks.reduce((sum, item) => sum + item.position, 0) / dayTasks.length;
return (
<div
key={dateKey}
className="timeline-day-group"
style={{ left: `${avgPosition}%` }}
>
<div className="timeline-date">{formatDate(dayTasks[0].dueDate)}</div>
<div className="timeline-tasks">
{dayTasks.map(({ task }, index) => (
<div
key={task.id}
className={`timeline-task ${task.priority.toLowerCase()}`}
style={{
marginLeft: `${index * 8}px`,
zIndex: dayTasks.length - index
}}
title={task.name}
>
{task.name}
</div>
<section className="upcoming-section">
<h2 className="section-title">What the future holds</h2>
{upcomingItems.length === 0 ? (
<p className="no-upcoming">No upcoming tasks scheduled</p>
) : (
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
{upcomingItems.map(({ task, dueDate }) => (
<UpcomingRow key={task.id} task={task} dueLabel={formatDueDate(dueDate)} />
))}
</div>
</div>
)}
</section>
);
})}
</div>
}
interface UpcomingRowProps {
task: Task;
dueLabel: string;
}
function UpcomingRow({ task, dueLabel }: UpcomingRowProps) {
const [tooltipVisible, setTooltipVisible] = useState(false);
return (
<div className="upcoming-row">
<span
className="upcoming-name"
onClick={() => setTooltipVisible(v => !v)}
onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
>
<span className="upcoming-name-text">{task.name}</span>
{tooltipVisible && <span className="name-tooltip">{task.name}</span>}
</span>
<span className="upcoming-badge">
{task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'}
</span>
<span className="upcoming-due">{dueLabel}</span>
</div>
);
}

View File

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

View File

@ -15,17 +15,15 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
return (
<div className="todays-tasks">
<h2>Today's Tasks</h2>
{/* Essential Tasks */}
<div className="task-section">
<h3 className="priority-header essential">Essential</h3>
<section className="task-section">
<h2 className="section-title">Need to do</h2>
{essentialTasks.length === 0 ? (
<p className="no-tasks">No essential tasks for today</p>
<p className="no-tasks">Nothing essential for today</p>
) : (
<div className="task-list">
<div className="task-grid">
{essentialTasks.map(task => (
<TaskItem
<TaskCard
key={task.id}
task={task}
onComplete={onComplete}
@ -34,58 +32,68 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
))}
</div>
)}
</div>
</section>
{/* When I Have Time Tasks */}
<div className="task-section">
<h3
className="priority-header when-i-have-time clickable"
<section className="task-section">
<h2
className="section-title section-title--clickable"
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
>
When I have time ({whenIHaveTimeTasks.length})
<span className="toggle-icon">{showWhenIHaveTime ? '▼' : '▶'}</span>
</h3>
When I have time
<span className="count-badge">{whenIHaveTimeTasks.length}</span>
<span className="toggle-icon">{showWhenIHaveTime ? '▾' : '▸'}</span>
</h2>
{showWhenIHaveTime && (
<div className="task-list">
{whenIHaveTimeTasks.length === 0 ? (
<p className="no-tasks">No tasks in this category</p>
whenIHaveTimeTasks.length === 0 ? (
<p className="no-tasks">Nothing here</p>
) : (
whenIHaveTimeTasks.map(task => (
<TaskItem
<div className="task-grid">
{whenIHaveTimeTasks.map(task => (
<TaskCard
key={task.id}
task={task}
onComplete={onComplete}
onRenew={onRenew}
/>
))
)}
))}
</div>
)
)}
</div>
</section>
</div>
);
}
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 (
<div className="task-item">
<span className="task-name">{task.name}</span>
<div className="task-actions">
<div className="task-card">
<span
className="task-card-name"
onClick={() => setTooltipVisible(v => !v)}
onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
>
{task.name}
{tooltipVisible && <span className="name-tooltip">{task.name}</span>}
</span>
<div className="task-card-actions">
<button
className="action-btn complete-btn"
className="action-btn"
onClick={() => onComplete(task.id)}
title="Mark as completed"
>
</button>
<button
className="action-btn renew-btn"
className="action-btn"
onClick={() => onRenew(task.id)}
title="Renew task"
>

View File

@ -1,47 +1,15 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font: 16px/150% var(--sans);
color: #111;
background: #fff;
color-scheme: light;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
}
* {
@ -50,42 +18,18 @@
body {
margin: 0;
background: #fff;
}
#root {
min-height: 100svh;
background: #fff;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 48px;
letter-spacing: -1.5px;
h1, h2, h3 {
margin: 0;
@media (max-width: 768px) {
font-size: 32px;
}
}
h2 {
font-size: 24px;
margin: 0 0 8px;
}
p {
margin: 0;
}
code {
font-family: var(--mono);
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
background: var(--code-bg);
color: var(--text-h);
}