init
This commit is contained in:
commit
ba1f43fd8e
35 changed files with 5926 additions and 0 deletions
195
frontend/src/components/ChecklistItem.tsx
Normal file
195
frontend/src/components/ChecklistItem.tsx
Normal file
|
@ -0,0 +1,195 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import type { ChecklistItem as ChecklistItemType } from '../types'
|
||||
|
||||
interface ChecklistItemProps {
|
||||
item: ChecklistItemType
|
||||
onUpdate: (id: number, updates: Partial<ChecklistItemType>) => Promise<void>
|
||||
onDelete: (id: number) => Promise<void>
|
||||
onLock: (id: number, user: string) => Promise<void>
|
||||
depth?: number
|
||||
children?: ChecklistItemType[]
|
||||
}
|
||||
|
||||
export default function ChecklistItem({
|
||||
item,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onLock,
|
||||
depth = 0,
|
||||
children = []
|
||||
}: ChecklistItemProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [userName, setUserName] = useState('')
|
||||
const contentRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
const isLocked = item.locked_by && item.lock_until && new Date(item.lock_until) > new Date()
|
||||
const isLockedByMe = isLocked && item.locked_by === userName
|
||||
|
||||
useEffect(() => {
|
||||
// Generate a random user name if not set
|
||||
if (!userName) {
|
||||
const randomName = `user_${Math.random().toString(36).substr(2, 9)}`
|
||||
setUserName(randomName)
|
||||
}
|
||||
}, [userName])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && contentRef.current) {
|
||||
contentRef.current.focus()
|
||||
// Select all text when entering edit mode
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(contentRef.current)
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (isLocked && !isLockedByMe) {
|
||||
alert('This item is being edited by someone else')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLockedByMe) {
|
||||
try {
|
||||
await onLock(item.id, userName)
|
||||
} catch (error) {
|
||||
console.error('Failed to lock item:', error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const newContent = contentRef.current?.textContent?.trim() || ''
|
||||
if (newContent === '') return
|
||||
|
||||
try {
|
||||
await onUpdate(item.id, { content: newContent })
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to update item:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.textContent = item.content
|
||||
}
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
try {
|
||||
setIsDeleting(true)
|
||||
await onDelete(item.id)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete item:', error)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCheck = async () => {
|
||||
try {
|
||||
await onUpdate(item.id, { checked: !item.checked })
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle item:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// Small delay to allow for button clicks
|
||||
setTimeout(() => {
|
||||
if (isEditing) {
|
||||
handleSave()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${depth > 0 ? 'ml-4 border-l-2 border-gray-200 dark:border-gray-700 pl-3' : ''}`}>
|
||||
<div className="flex items-center justify-between py-2 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg px-2 transition-colors duration-200">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
onChange={handleToggleCheck}
|
||||
disabled={Boolean(isLocked && !isLockedByMe)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
|
||||
<span
|
||||
ref={contentRef}
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
className={`flex-1 text-sm text-gray-700 dark:text-gray-300 break-words outline-none ${
|
||||
isEditing
|
||||
? 'px-2 py-1 border-2 border-blue-500 rounded focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-800 bg-white dark:bg-gray-800'
|
||||
: 'cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200'
|
||||
} ${
|
||||
item.checked ? 'line-through text-gray-500 dark:text-gray-500' : ''
|
||||
}`}
|
||||
onClick={!isEditing ? handleEdit : undefined}
|
||||
onKeyDown={isEditing ? handleKeyDown : undefined}
|
||||
onBlur={isEditing ? handleBlur : undefined}
|
||||
>
|
||||
{item.content}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{isLocked && !isLockedByMe && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-2 py-1 rounded-full whitespace-nowrap">
|
||||
🔒 {item.locked_by}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors duration-200 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Delete item"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children.length > 0 && (
|
||||
<div className="mt-1">
|
||||
{children.map(child => (
|
||||
<ChecklistItem
|
||||
key={child.id}
|
||||
item={child}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
onLock={onLock}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue