Main branch Resync on w/ gitea. v0.1.6 #1

Merged
serversdown merged 20 commits from claude/nested-todo-app-mvp-014vUjLpD8wNjkMhLMXS6Kd5 into main 2026-01-04 04:30:52 -05:00
4 changed files with 121 additions and 9 deletions
Showing only changes of commit 3f309163b6 - Show all commits

View File

@@ -6,7 +6,7 @@ import {
updateTask, updateTask,
deleteTask deleteTask
} from '../utils/api' } from '../utils/api'
import { formatTime } from '../utils/format' import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu' import TaskMenu from './TaskMenu'
const STATUSES = [ const STATUSES = [
@@ -106,13 +106,13 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
)} )}
{/* Metadata row */} {/* Metadata row */}
{(task.estimated_minutes || (task.tags && task.tags.length > 0)) && ( {(formatTimeWithTotal(task, allTasks) || (task.tags && task.tags.length > 0)) && (
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
{/* Time estimate */} {/* Time estimate */}
{task.estimated_minutes && ( {formatTimeWithTotal(task, allTasks) && (
<div className="flex items-center gap-1 text-xs text-gray-500"> <div className="flex items-center gap-1 text-xs text-gray-500">
<Clock size={11} /> <Clock size={11} />
<span>{formatTime(task.estimated_minutes)}</span> <span>{formatTimeWithTotal(task, allTasks)}</span>
</div> </div>
)} )}

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check } from 'lucide-react' import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check, ListTodo } from 'lucide-react'
import { updateTask } from '../utils/api' import { updateTask } from '../utils/api'
const FLAG_COLORS = [ const FLAG_COLORS = [
@@ -12,11 +12,19 @@ const FLAG_COLORS = [
{ name: 'pink', color: 'bg-pink-500' } { name: 'pink', color: 'bg-pink-500' }
] ]
const STATUSES = [
{ key: 'backlog', label: 'Backlog', color: 'text-gray-400' },
{ key: 'in_progress', label: 'In Progress', color: 'text-blue-400' },
{ key: 'blocked', label: 'Blocked', color: 'text-red-400' },
{ key: 'done', label: 'Done', color: 'text-green-400' }
]
function TaskMenu({ task, onUpdate, onDelete, onEdit }) { function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [showTimeEdit, setShowTimeEdit] = useState(false) const [showTimeEdit, setShowTimeEdit] = useState(false)
const [showTagsEdit, setShowTagsEdit] = useState(false) const [showTagsEdit, setShowTagsEdit] = useState(false)
const [showFlagEdit, setShowFlagEdit] = useState(false) const [showFlagEdit, setShowFlagEdit] = useState(false)
const [showStatusEdit, setShowStatusEdit] = useState(false)
const [editTime, setEditTime] = useState(task.estimated_minutes || '') const [editTime, setEditTime] = useState(task.estimated_minutes || '')
const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '') const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '')
const menuRef = useRef(null) const menuRef = useRef(null)
@@ -28,6 +36,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
setShowTimeEdit(false) setShowTimeEdit(false)
setShowTagsEdit(false) setShowTagsEdit(false)
setShowFlagEdit(false) setShowFlagEdit(false)
setShowStatusEdit(false)
} }
} }
@@ -85,6 +94,17 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
} }
} }
const handleUpdateStatus = async (newStatus) => {
try {
await updateTask(task.id, { status: newStatus })
setShowStatusEdit(false)
setIsOpen(false)
onUpdate()
} catch (err) {
alert(`Error: ${err.message}`)
}
}
return ( return (
<div className="relative" ref={menuRef}> <div className="relative" ref={menuRef}>
<button <button
@@ -225,6 +245,42 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
</button> </button>
)} )}
{/* Status Change */}
{showStatusEdit ? (
<div className="p-3 border-b border-cyber-orange/20">
<div className="flex items-center gap-2 mb-2">
<ListTodo size={14} className="text-cyber-orange" />
<span className="text-sm text-gray-300">Change Status</span>
</div>
<div className="space-y-1">
{STATUSES.map(({ key, label, color }) => (
<button
key={key}
onClick={() => handleUpdateStatus(key)}
className={`w-full text-left px-2 py-1.5 rounded text-sm ${
task.status === key
? 'bg-cyber-orange/20 border border-cyber-orange/40'
: 'hover:bg-cyber-darker border border-transparent'
} ${color} transition-all`}
>
{label} {task.status === key && '✓'}
</button>
))}
</div>
</div>
) : (
<button
onClick={(e) => {
e.stopPropagation()
setShowStatusEdit(true)
}}
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-cyber-darker text-gray-300 text-sm"
>
<ListTodo size={14} />
<span>Change Status</span>
</button>
)}
{/* Edit Title */} {/* Edit Title */}
<button <button
onClick={(e) => { onClick={(e) => {

View File

@@ -14,7 +14,7 @@ import {
updateTask, updateTask,
deleteTask deleteTask
} from '../utils/api' } from '../utils/api'
import { formatTime } from '../utils/format' import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu' import TaskMenu from './TaskMenu'
const STATUS_COLORS = { const STATUS_COLORS = {
@@ -163,13 +163,13 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
</div> </div>
{/* Metadata row */} {/* Metadata row */}
{(task.estimated_minutes || (task.tags && task.tags.length > 0)) && ( {(formatTimeWithTotal(task) || (task.tags && task.tags.length > 0)) && (
<div className="flex items-center gap-3 mt-1"> <div className="flex items-center gap-3 mt-1">
{/* Time estimate */} {/* Time estimate */}
{task.estimated_minutes && ( {formatTimeWithTotal(task) && (
<div className="flex items-center gap-1 text-xs text-gray-500"> <div className="flex items-center gap-1 text-xs text-gray-500">
<Clock size={12} /> <Clock size={12} />
<span>{formatTime(task.estimated_minutes)}</span> <span>{formatTimeWithTotal(task)}</span>
</div> </div>
)} )}

View File

@@ -15,3 +15,59 @@ export function formatTags(tags) {
if (!tags || tags.length === 0) return null; if (!tags || tags.length === 0) return null;
return tags.join(', '); return tags.join(', ');
} }
// Recursively calculate total time estimate including all subtasks
export function calculateTotalTime(task) {
let total = task.estimated_minutes || 0;
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
total += calculateTotalTime(subtask);
}
}
return total;
}
// Calculate total time for a task from a flat list of all tasks
export function calculateTotalTimeFlat(task, allTasks) {
let total = task.estimated_minutes || 0;
// Find all direct children
const children = allTasks.filter(t => t.parent_task_id === task.id);
// Recursively add their times
for (const child of children) {
total += calculateTotalTimeFlat(child, allTasks);
}
return total;
}
// Format time display showing own estimate and total if different
export function formatTimeWithTotal(task, allTasks = null) {
const ownTime = task.estimated_minutes || 0;
// If we have a flat task list, use that to calculate total
const totalTime = allTasks
? calculateTotalTimeFlat(task, allTasks)
: calculateTotalTime(task);
const subtaskTime = totalTime - ownTime;
// No time estimates at all
if (totalTime === 0) return null;
// Only own estimate, no subtasks with time
if (subtaskTime === 0) {
return formatTime(ownTime);
}
// Only subtask estimates, no own estimate
if (ownTime === 0) {
return `(${formatTime(totalTime)} from subtasks)`;
}
// Both own and subtask estimates
return `${formatTime(ownTime)} (${formatTime(totalTime)} total)`;
}