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);
|
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) => {
|
fastify.patch<{ Params: { id: string } }>('/api/tasks/:id/renew', async (request, reply) => {
|
||||||
const id = parseInt(request.params.id);
|
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) => {
|
const handleCompleteTask = async (taskId: number) => {
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
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)
|
const todaysTasks = tasks.filter(isTaskDueToday)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -125,10 +122,9 @@ function App() {
|
||||||
<TodaysTasks
|
<TodaysTasks
|
||||||
tasks={todaysTasks}
|
tasks={todaysTasks}
|
||||||
onComplete={handleCompleteTask}
|
onComplete={handleCompleteTask}
|
||||||
onRenew={handleRenewTask}
|
|
||||||
/>
|
/>
|
||||||
<CreateTaskForm onCreateTask={handleCreateTask} />
|
<CreateTaskForm onCreateTask={handleCreateTask} />
|
||||||
<Timeline tasks={tasks} />
|
<Timeline tasks={tasks} onDeleteTask={handleDeleteTask} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!name.trim()) {
|
if (!name.trim()) return;
|
||||||
alert('Please enter a task name');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskInput: NewTaskInput = {
|
const taskInput: NewTaskInput = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
margin-top: 48px;
|
margin-top: 48px;
|
||||||
padding-top: 32px;
|
padding-top: 32px;
|
||||||
border-top: 1px solid #e8e8e8;
|
border-top: 1px solid #e8e8e8;
|
||||||
}.upcoming-list {
|
}
|
||||||
|
|
||||||
|
.upcoming-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
|
|
@ -17,7 +19,7 @@
|
||||||
|
|
||||||
.upcoming-row {
|
.upcoming-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|
@ -101,9 +103,26 @@
|
||||||
border-top-color: #111;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.upcoming-row {
|
.upcoming-row {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
gap: 4px 12px;
|
gap: 4px 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import type { Task } from '../types';
|
import type { Task } from '../types';
|
||||||
import { calculateNextDueDate } from '../utils/taskUtils';
|
import { calculateNextDueDate } from '../utils/taskUtils';
|
||||||
import './Timeline.css';
|
import './Timeline.css';
|
||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
onDeleteTask: (id: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VISIBLE = 5;
|
const MAX_VISIBLE = 5;
|
||||||
|
|
||||||
export default function Timeline({ tasks }: TimelineProps) {
|
export default function Timeline({ tasks, onDeleteTask }: TimelineProps) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ export default function Timeline({ tasks }: TimelineProps) {
|
||||||
) : (
|
) : (
|
||||||
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
|
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
|
||||||
{upcomingItems.map(({ task, dueDate }) => (
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -53,26 +53,20 @@ export default function Timeline({ tasks }: TimelineProps) {
|
||||||
interface UpcomingRowProps {
|
interface UpcomingRowProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
dueLabel: string;
|
dueLabel: string;
|
||||||
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpcomingRow({ task, dueLabel }: UpcomingRowProps) {
|
function UpcomingRow({ task, dueLabel, onDelete }: UpcomingRowProps) {
|
||||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="upcoming-row">
|
<div className="upcoming-row">
|
||||||
<span
|
<span className="upcoming-name">
|
||||||
className="upcoming-name"
|
|
||||||
onClick={() => setTooltipVisible(v => !v)}
|
|
||||||
onMouseEnter={() => setTooltipVisible(true)}
|
|
||||||
onMouseLeave={() => setTooltipVisible(false)}
|
|
||||||
>
|
|
||||||
<span className="upcoming-name-text">{task.name}</span>
|
<span className="upcoming-name-text">{task.name}</span>
|
||||||
{tooltipVisible && <span className="name-tooltip">{task.name}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="upcoming-badge">
|
<span className="upcoming-badge">
|
||||||
{task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'}
|
{task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'}
|
||||||
</span>
|
</span>
|
||||||
<span className="upcoming-due">{dueLabel}</span>
|
<span className="upcoming-due">{dueLabel}</span>
|
||||||
|
<button className="upcoming-delete" onClick={onDelete} title="Remove task">✕</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ import './TodaysTasks.css';
|
||||||
interface TodaysTasksProps {
|
interface TodaysTasksProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
onComplete: (taskId: number) => void;
|
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 essentialTasks = tasks.filter(t => t.priority === 'ESSENTIAL');
|
||||||
const whenIHaveTimeTasks = tasks.filter(t => t.priority === 'WHEN_I_HAVE_TIME');
|
const whenIHaveTimeTasks = tasks.filter(t => t.priority === 'WHEN_I_HAVE_TIME');
|
||||||
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
|
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
|
||||||
|
|
@ -27,7 +26,6 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onRenew={onRenew}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,7 +51,6 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onRenew={onRenew}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -67,10 +64,9 @@ export default function TodaysTasks({ tasks, onComplete, onRenew }: TodaysTasksP
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
onComplete: (taskId: number) => void;
|
onComplete: (taskId: number) => void;
|
||||||
onRenew: (taskId: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskCard({ task, onComplete, onRenew }: TaskCardProps) {
|
function TaskCard({ task, onComplete }: TaskCardProps) {
|
||||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -92,13 +88,6 @@ function TaskCard({ task, onComplete, onRenew }: TaskCardProps) {
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="action-btn"
|
|
||||||
onClick={() => onRenew(task.id)}
|
|
||||||
title="Renew task"
|
|
||||||
>
|
|
||||||
↻
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue