generated from ludops/ludops-skeleton
feat: can now delete task
Build and Deploy / build-and-push (push) Successful in 1m22s
Details
Build and Deploy / build-and-push (push) Successful in 1m22s
Details
This commit is contained in:
parent
fc9bc3fe62
commit
844eb00270
|
|
@ -81,6 +81,19 @@ fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/complete', async (requ
|
|||
reply.send(updatedTask);
|
||||
});
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>('/api/tasks/:id', async (request, reply) => {
|
||||
const id = parseInt(request.params.id);
|
||||
|
||||
const deleted = await db.delete(tasks).where(eq(tasks.id, id)).returning();
|
||||
|
||||
if (!deleted.length) {
|
||||
reply.code(404).send({ error: 'Task not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.code(204).send();
|
||||
});
|
||||
|
||||
fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/renew', async (request, reply) => {
|
||||
const id = parseInt(request.params.id);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,21 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleDeleteTask = async (taskId: number) => {
|
||||
setError(null)
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' })
|
||||
if (!response.ok) {
|
||||
setError('Failed to delete task')
|
||||
return
|
||||
}
|
||||
await fetchTasks()
|
||||
} catch (error) {
|
||||
console.error('Error deleting task:', error)
|
||||
setError('Failed to delete task')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCompleteTask = async (taskId: number) => {
|
||||
setError(null)
|
||||
try {
|
||||
|
|
@ -81,24 +96,6 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleRenewTask = async (taskId: number) => {
|
||||
setError(null)
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/renew`, {
|
||||
method: 'PATCH',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
setError('Failed to renew task')
|
||||
return
|
||||
}
|
||||
await fetchTasks()
|
||||
} catch (error) {
|
||||
console.error('Error renewing task:', error)
|
||||
setError('Failed to renew task')
|
||||
}
|
||||
}
|
||||
|
||||
const todaysTasks = tasks.filter(isTaskDueToday)
|
||||
|
||||
return (
|
||||
|
|
@ -125,10 +122,9 @@ function App() {
|
|||
<TodaysTasks
|
||||
tasks={todaysTasks}
|
||||
onComplete={handleCompleteTask}
|
||||
onRenew={handleRenewTask}
|
||||
/>
|
||||
<CreateTaskForm onCreateTask={handleCreateTask} />
|
||||
<Timeline tasks={tasks} />
|
||||
<Timeline tasks={tasks} onDeleteTask={handleDeleteTask} />
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
|||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
alert('Please enter a task name');
|
||||
return;
|
||||
}
|
||||
if (!name.trim()) return;
|
||||
|
||||
const taskInput: NewTaskInput = {
|
||||
name: name.trim(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
margin-top: 48px;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}.upcoming-list {
|
||||
}
|
||||
|
||||
.upcoming-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
|
@ -17,7 +19,7 @@
|
|||
|
||||
.upcoming-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
|
|
@ -101,9 +103,26 @@
|
|||
border-top-color: #111;
|
||||
}
|
||||
|
||||
.upcoming-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.upcoming-delete:hover {
|
||||
color: #111;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.upcoming-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 4px 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { useState } from 'react';
|
||||
import type { Task } from '../types';
|
||||
import { calculateNextDueDate } from '../utils/taskUtils';
|
||||
import './Timeline.css';
|
||||
|
||||
interface TimelineProps {
|
||||
tasks: Task[];
|
||||
onDeleteTask: (id: number) => void;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE = 5;
|
||||
|
||||
export default function Timeline({ tasks }: TimelineProps) {
|
||||
export default function Timeline({ tasks, onDeleteTask }: TimelineProps) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export default function Timeline({ tasks }: TimelineProps) {
|
|||
) : (
|
||||
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
|
||||
{upcomingItems.map(({ task, dueDate }) => (
|
||||
<UpcomingRow key={task.id} task={task} dueLabel={formatDueDate(dueDate)} />
|
||||
<UpcomingRow key={task.id} task={task} dueLabel={formatDueDate(dueDate)} onDelete={() => onDeleteTask(task.id)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -53,26 +53,20 @@ export default function Timeline({ tasks }: TimelineProps) {
|
|||
interface UpcomingRowProps {
|
||||
task: Task;
|
||||
dueLabel: string;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function UpcomingRow({ task, dueLabel }: UpcomingRowProps) {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
|
||||
function UpcomingRow({ task, dueLabel, onDelete }: UpcomingRowProps) {
|
||||
return (
|
||||
<div className="upcoming-row">
|
||||
<span
|
||||
className="upcoming-name"
|
||||
onClick={() => setTooltipVisible(v => !v)}
|
||||
onMouseEnter={() => setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
>
|
||||
<span className="upcoming-name">
|
||||
<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>
|
||||
<button className="upcoming-delete" onClick={onDelete} title="Remove task">✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import './TodaysTasks.css';
|
|||
interface TodaysTasksProps {
|
||||
tasks: Task[];
|
||||
onComplete: (taskId: number) => void;
|
||||
onRenew: (taskId: number) => void;
|
||||
}
|
||||
|
||||
export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksProps) {
|
||||
export default function TodaysTasks({ tasks, onComplete }: TodaysTasksProps) {
|
||||
const essentialTasks = tasks.filter(t => t.priority === 'ESSENTIAL');
|
||||
const whenIHaveTimeTasks = tasks.filter(t => t.priority === 'WHEN_I_HAVE_TIME');
|
||||
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
|
||||
|
|
@ -27,7 +26,6 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
|||
key={task.id}
|
||||
task={task}
|
||||
onComplete={onComplete}
|
||||
onRenew={onRenew}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -53,7 +51,6 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
|||
key={task.id}
|
||||
task={task}
|
||||
onComplete={onComplete}
|
||||
onRenew={onRenew}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -67,10 +64,9 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
|||
interface TaskCardProps {
|
||||
task: Task;
|
||||
onComplete: (taskId: number) => void;
|
||||
onRenew: (taskId: number) => void;
|
||||
}
|
||||
|
||||
function TaskCard({ task, onComplete, onRenew }: TaskCardProps) {
|
||||
function TaskCard({ task, onComplete }: TaskCardProps) {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -92,13 +88,6 @@ function TaskCard({ task, onComplete, onRenew }: TaskCardProps) {
|
|||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
className="action-btn"
|
||||
onClick={() => onRenew(task.id)}
|
||||
title="Renew task"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue