generated from ludops/ludops-skeleton
feat: removed timeline and dark theme
Build and Deploy / build-and-push (push) Successful in 55s
Details
Build and Deploy / build-and-push (push) Successful in 55s
Details
This commit is contained in:
parent
edb0a52e41
commit
fc9bc3fe62
|
|
@ -1,51 +1,60 @@
|
||||||
main {
|
main {
|
||||||
max-width: 1400px;
|
max-width: 960px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 40px 20px;
|
padding: 48px 24px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
main {
|
main {
|
||||||
padding: 20px 12px;
|
padding: 24px 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 42px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 16px;
|
color: #111;
|
||||||
color: #2c3e50;
|
margin: 0 0 8px 0;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
/* Shared section title — used by TodaysTasks and Timeline */
|
||||||
color: var(--text);
|
.section-title {
|
||||||
margin: 0;
|
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 {
|
.status {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
padding: 8px 16px;
|
padding: 6px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
color: #888;
|
||||||
}
|
|
||||||
|
|
||||||
.status--loading {
|
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--ok {
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #666;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status--error {
|
.status--error {
|
||||||
|
|
@ -57,47 +66,43 @@ h1 {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background: #ffebee;
|
background: white;
|
||||||
color: #c62828;
|
color: #111;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 400px;
|
max-width: 380px;
|
||||||
animation: slideIn 0.3s ease;
|
font-size: 14px;
|
||||||
|
animation: slideIn 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.error-message {
|
.error-message {
|
||||||
left: 12px;
|
left: 16px;
|
||||||
right: 12px;
|
right: 16px;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from { transform: translateY(-8px); opacity: 0; }
|
||||||
transform: translateX(100%);
|
to { transform: translateY(0); opacity: 1; }
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-dismiss {
|
.error-dismiss {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #c62828;
|
color: #999;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -105,7 +110,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-dismiss:hover {
|
.error-dismiss:hover {
|
||||||
opacity: 0.7;
|
color: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,13 +122,13 @@ function App() {
|
||||||
<p>Loading tasks...</p>
|
<p>Loading tasks...</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Timeline tasks={tasks} />
|
|
||||||
<TodaysTasks
|
<TodaysTasks
|
||||||
tasks={todaysTasks}
|
tasks={todaysTasks}
|
||||||
onComplete={handleCompleteTask}
|
onComplete={handleCompleteTask}
|
||||||
onRenew={handleRenewTask}
|
onRenew={handleRenewTask}
|
||||||
/>
|
/>
|
||||||
<CreateTaskForm onCreateTask={handleCreateTask} />
|
<CreateTaskForm onCreateTask={handleCreateTask} />
|
||||||
|
<Timeline tasks={tasks} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.create-task-form {
|
.create-task-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,120 @@
|
||||||
.timeline-container {
|
.upcoming-section {
|
||||||
width: 100%;
|
margin-top: 48px;
|
||||||
padding: 40px 20px;
|
padding-top: 32px;
|
||||||
background: #fafafa;
|
border-top: 1px solid #e8e8e8;
|
||||||
border-radius: 12px;
|
}.upcoming-list {
|
||||||
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%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
background: white;
|
||||||
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-date {
|
.upcoming-row:last-child {
|
||||||
font-size: 10px;
|
border-bottom: none;
|
||||||
color: #999;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.upcoming-row:hover {
|
||||||
.timeline-date {
|
background: #fafafa;
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-tasks {
|
.upcoming-name {
|
||||||
display: flex;
|
position: relative;
|
||||||
flex-direction: column;
|
min-width: 0;
|
||||||
align-items: flex-start;
|
cursor: default;
|
||||||
gap: 4px;
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.upcoming-name-text {
|
||||||
.timeline-tasks {
|
display: block;
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-task {
|
|
||||||
background: #555;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 10px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 100px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
font-size: 14px;
|
||||||
position: relative;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.timeline-task {
|
.upcoming-row {
|
||||||
font-size: 9px;
|
grid-template-columns: 1fr auto;
|
||||||
padding: 3px 6px;
|
gap: 4px 12px;
|
||||||
max-width: 80px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-task.essential {
|
.upcoming-badge {
|
||||||
background: #2c3e50;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upcoming-due {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,101 +1,78 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import type { Task } from '../types';
|
import type { Task } from '../types';
|
||||||
import { calculateNextDueDate, formatDate } from '../utils/taskUtils';
|
import { calculateNextDueDate } from '../utils/taskUtils';
|
||||||
import './Timeline.css';
|
import './Timeline.css';
|
||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE = 5;
|
||||||
|
|
||||||
export default function Timeline({ tasks }: TimelineProps) {
|
export default function Timeline({ tasks }: TimelineProps) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// Timeline range: 15 days past, 30 days future
|
const upcomingItems = tasks
|
||||||
const startDate = new Date(today);
|
.map(task => {
|
||||||
startDate.setDate(startDate.getDate() - 15);
|
const dueDate = calculateNextDueDate(task);
|
||||||
|
if (!dueDate) return null;
|
||||||
const endDate = new Date(today);
|
const due = new Date(dueDate);
|
||||||
endDate.setDate(endDate.getDate() + 30);
|
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 hasMore = upcomingItems.length > MAX_VISIBLE;
|
||||||
const totalDays = 45; // 15 past + 30 future
|
|
||||||
const todayPosition = (15 / totalDays) * 100; // 33.33% from left
|
|
||||||
|
|
||||||
const taskPositions = tasks.map(task => {
|
const formatDueDate = (date: Date): string => {
|
||||||
const dueDate = calculateNextDueDate(task);
|
const diff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
if (!dueDate) return null;
|
if (diff === 1) return 'Tomorrow';
|
||||||
|
if (diff <= 7) return `In ${diff} days`;
|
||||||
// Only show tasks within timeline range
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
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<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 (
|
return (
|
||||||
<div className="timeline-container">
|
<section className="upcoming-section">
|
||||||
<div className="timeline">
|
<h2 className="section-title">What the future holds</h2>
|
||||||
<div className="timeline-line"></div>
|
{upcomingItems.length === 0 ? (
|
||||||
|
<p className="no-upcoming">No upcoming tasks scheduled</p>
|
||||||
{/* TODAY marker */}
|
) : (
|
||||||
<div className="today-marker" style={{ left: `${todayPosition}%` }}>
|
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
|
||||||
<div className="today-arrow">↑</div>
|
{upcomingItems.map(({ task, dueDate }) => (
|
||||||
<div className="today-label">TODAY</div>
|
<UpcomingRow key={task.id} task={task} dueLabel={formatDueDate(dueDate)} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{/* Tasks */}
|
interface UpcomingRowProps {
|
||||||
{Object.entries(tasksByDay).map(([dateKey, dayTasks]) => {
|
task: Task;
|
||||||
const avgPosition = dayTasks.reduce((sum, item) => sum + item.position, 0) / dayTasks.length;
|
dueLabel: string;
|
||||||
|
}
|
||||||
return (
|
|
||||||
<div
|
function UpcomingRow({ task, dueLabel }: UpcomingRowProps) {
|
||||||
key={dateKey}
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
className="timeline-day-group"
|
|
||||||
style={{ left: `${avgPosition}%` }}
|
return (
|
||||||
>
|
<div className="upcoming-row">
|
||||||
<div className="timeline-date">{formatDate(dayTasks[0].dueDate)}</div>
|
<span
|
||||||
<div className="timeline-tasks">
|
className="upcoming-name"
|
||||||
{dayTasks.map(({ task }, index) => (
|
onClick={() => setTooltipVisible(v => !v)}
|
||||||
<div
|
onMouseEnter={() => setTooltipVisible(true)}
|
||||||
key={task.id}
|
onMouseLeave={() => setTooltipVisible(false)}
|
||||||
className={`timeline-task ${task.priority.toLowerCase()}`}
|
>
|
||||||
style={{
|
<span className="upcoming-name-text">{task.name}</span>
|
||||||
marginLeft: `${index * 8}px`,
|
{tooltipVisible && <span className="name-tooltip">{task.name}</span>}
|
||||||
zIndex: dayTasks.length - index
|
</span>
|
||||||
}}
|
<span className="upcoming-badge">
|
||||||
title={task.name}
|
{task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'}
|
||||||
>
|
</span>
|
||||||
{task.name}
|
<span className="upcoming-due">{dueLabel}</span>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,112 @@
|
||||||
.todays-tasks {
|
.todays-tasks {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todays-tasks h2 {
|
.task-section {
|
||||||
font-size: 24px;
|
margin-bottom: 36px;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
color: #2c3e50;
|
|
||||||
|
.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) {
|
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,15 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="todays-tasks">
|
<div className="todays-tasks">
|
||||||
<h2>Today's Tasks</h2>
|
|
||||||
|
<section className="task-section">
|
||||||
{/* Essential Tasks */}
|
<h2 className="section-title">Need to do</h2>
|
||||||
<div className="task-section">
|
|
||||||
<h3 className="priority-header essential">Essential</h3>
|
|
||||||
{essentialTasks.length === 0 ? (
|
{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 => (
|
{essentialTasks.map(task => (
|
||||||
<TaskItem
|
<TaskCard
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
|
@ -34,58 +32,68 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{/* When I Have Time Tasks */}
|
<section className="task-section">
|
||||||
<div className="task-section">
|
<h2
|
||||||
<h3
|
className="section-title section-title--clickable"
|
||||||
className="priority-header when-i-have-time clickable"
|
|
||||||
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
|
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
|
||||||
>
|
>
|
||||||
When I have time ({whenIHaveTimeTasks.length})
|
When I have time
|
||||||
<span className="toggle-icon">{showWhenIHaveTime ? '▼' : '▶'}</span>
|
<span className="count-badge">{whenIHaveTimeTasks.length}</span>
|
||||||
</h3>
|
<span className="toggle-icon">{showWhenIHaveTime ? '▾' : '▸'}</span>
|
||||||
|
</h2>
|
||||||
{showWhenIHaveTime && (
|
{showWhenIHaveTime && (
|
||||||
<div className="task-list">
|
whenIHaveTimeTasks.length === 0 ? (
|
||||||
{whenIHaveTimeTasks.length === 0 ? (
|
<p className="no-tasks">Nothing here</p>
|
||||||
<p className="no-tasks">No tasks in this category</p>
|
) : (
|
||||||
) : (
|
<div className="task-grid">
|
||||||
whenIHaveTimeTasks.map(task => (
|
{whenIHaveTimeTasks.map(task => (
|
||||||
<TaskItem
|
<TaskCard
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onRenew={onRenew}
|
onRenew={onRenew}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskItemProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
onComplete: (taskId: number) => void;
|
onComplete: (taskId: number) => void;
|
||||||
onRenew: (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 (
|
return (
|
||||||
<div className="task-item">
|
<div className="task-card">
|
||||||
<span className="task-name">{task.name}</span>
|
<span
|
||||||
<div className="task-actions">
|
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
|
<button
|
||||||
className="action-btn complete-btn"
|
className="action-btn"
|
||||||
onClick={() => onComplete(task.id)}
|
onClick={() => onComplete(task.id)}
|
||||||
title="Mark as completed"
|
title="Mark as completed"
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="action-btn renew-btn"
|
className="action-btn"
|
||||||
onClick={() => onRenew(task.id)}
|
onClick={() => onRenew(task.id)}
|
||||||
title="Renew task"
|
title="Renew task"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,15 @@
|
||||||
:root {
|
: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;
|
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
font: 16px/150% var(--sans);
|
||||||
letter-spacing: 0.18px;
|
color: #111;
|
||||||
color-scheme: light dark;
|
background: #fff;
|
||||||
color: var(--text);
|
color-scheme: light;
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1, h2, h3 {
|
||||||
h2 {
|
|
||||||
font-family: var(--heading);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 48px;
|
|
||||||
letter-spacing: -1.5px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@media (max-width: 768px) {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: var(--mono);
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--code-bg);
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue