allow to name checklist

This commit is contained in:
lubiana 2025-07-25 20:06:40 +02:00
parent c7eae58857
commit eb023a40d0
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
6 changed files with 202 additions and 2 deletions

View file

@ -0,0 +1,118 @@
import React, { useState, useRef, useEffect } from 'react'
interface EditableTitleProps {
title: string
onSave: (newTitle: string) => Promise<void>
className?: string
}
export const EditableTitle: React.FC<EditableTitleProps> = ({
title,
onSave,
className = ''
}) => {
const [isEditing, setIsEditing] = useState(false)
const [editValue, setEditValue] = useState(title)
const [isSaving, setIsSaving] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
setEditValue(title)
}, [title])
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [isEditing])
const handleStartEdit = () => {
setIsEditing(true)
setEditValue(title)
}
const handleSave = async () => {
const trimmedValue = editValue.trim()
if (trimmedValue === title || trimmedValue === '') {
setIsEditing(false)
return
}
setIsSaving(true)
try {
await onSave(trimmedValue)
setIsEditing(false)
} catch (error) {
console.error('Failed to save title:', error)
// Reset to original value on error
setEditValue(title)
} finally {
setIsSaving(false)
}
}
const handleCancel = () => {
setIsEditing(false)
setEditValue(title)
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSave()
} else if (e.key === 'Escape') {
e.preventDefault()
handleCancel()
}
}
if (isEditing) {
return (
<div className={`flex items-center gap-2 ${className}`}>
<input
ref={inputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleSave}
disabled={isSaving}
className="flex-1 px-2 py-1 text-base font-bold text-gray-900 dark:text-white bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
maxLength={100}
/>
{isSaving && (
<div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
<span>Saving...</span>
</div>
)}
</div>
)
}
return (
<button
onClick={handleStartEdit}
className={`text-base font-bold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 group ${className}`}
title="Click to edit title"
>
<span className="flex items-center gap-1">
{title}
<svg
className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</span>
</button>
)
}

View file

@ -10,6 +10,7 @@ export function saveChecklist(checklist: SavedChecklist) {
if (existingIndex > -1) {
// Update lastOpened timestamp if checklist already exists
existingChecklists[existingIndex].lastOpened = new Date().toISOString();
existingChecklists[existingIndex].name = checklist.name;
} else {
// Add new checklist with lastOpened timestamp
existingChecklists.push({ ...checklist, lastOpened: new Date().toISOString() });

View file

@ -28,6 +28,7 @@ export function useSSE(uuid: string) {
switch (data.type) {
case 'checklist_name':
case 'checklist_name_updated':
if (data.name) {
saveChecklist({
uuid,

View file

@ -2,6 +2,7 @@ import { useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import { useSSE } from '../hooks/useSSE'
import ChecklistItem from '../components/ChecklistItem'
import { EditableTitle } from '../components/EditableTitle'
import type { ChecklistItem as ChecklistItemType } from '../types'
interface ItemGroup {
@ -204,6 +205,22 @@ export default function Checklist() {
}
}
const updateChecklistName = async (newName: string) => {
if (!uuid) throw new Error('No checklist UUID')
const response = await fetch(`/api/checklists/${uuid}/name`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newName }),
})
if (!response.ok) {
throw new Error('Failed to update checklist name')
}
}
const handleAddItem = async () => {
await addItem(newItemContent)
setNewItemContent('')
@ -353,7 +370,11 @@ export default function Checklist() {
<span className="text-base">Back</span>
</button>
</Link>
<h1 className="text-base font-bold text-gray-900 dark:text-white">{checkListName}</h1>
<EditableTitle
title={checkListName}
onSave={updateChecklistName}
className="text-base"
/>
</div>
<div className="flex items-center gap-2">
{isConnected ? (

View file

@ -20,7 +20,7 @@ export interface SavedChecklist {
}
export interface SSEEvent {
type: 'full_state' | 'item_added' | 'item_updated' | 'item_deleted' | 'item_locked' | 'item_unlocked' | 'checklist_name'
type: 'full_state' | 'item_added' | 'item_updated' | 'item_deleted' | 'item_locked' | 'item_unlocked' | 'checklist_name' | 'checklist_name_updated'
name?: string
items?: ChecklistItem[]
item?: ChecklistItem