feat(notes): require a title to create a note
This commit is contained in:
1 parent
e9eb207b03
commit
2a3b33467b
6 files changed
+323
-99
No files matched your search
+127
-61
@@ -2,80 +2,142 @@ import { useState } from 'react'
|
||||
import type { Note } from '../types'
|
||||
import DeleteModal from './DeleteModal'
|
||||
|
||||
interface NoteEditorProps {
|
||||
interface EditEditorProps {
|
||||
mode: 'edit'
|
||||
note: Note
|
||||
onUpdate: (id: number, patch: Partial<Pick<Note, 'title' | 'content'>>) => void
|
||||
onDelete: (id: number) => void
|
||||
autoEdit?: boolean
|
||||
}
|
||||
|
||||
function NoteEditor({ note, onUpdate, onDelete, autoEdit = false }: NoteEditorProps) {
|
||||
const [editing, setEditing] = useState(autoEdit)
|
||||
interface CreateEditorProps {
|
||||
mode: 'create'
|
||||
onCreate: (title: string, content: string) => void
|
||||
onCancelCreate: () => void
|
||||
}
|
||||
|
||||
type NoteEditorProps = EditEditorProps | CreateEditorProps
|
||||
|
||||
interface NoteFormProps {
|
||||
title: string
|
||||
content: string
|
||||
titleMissing: boolean
|
||||
cancelLabel: string
|
||||
onTitleChange: (value: string) => void
|
||||
onContentChange: (value: string) => void
|
||||
onCancel: () => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
const TITLE_REQUIRED_MESSAGE = 'Le titre est obligatoire.'
|
||||
|
||||
function NoteForm({
|
||||
title,
|
||||
content,
|
||||
titleMissing,
|
||||
cancelLabel,
|
||||
onTitleChange,
|
||||
onContentChange,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: NoteFormProps) {
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-4 rounded-xl border border-gray-200 bg-white p-4 shadow-sm"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
onSubmit()
|
||||
}}
|
||||
>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-gray-500">Titre</span>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => onTitleChange(event.target.value)}
|
||||
autoFocus
|
||||
aria-invalid={titleMissing}
|
||||
className={`w-full rounded-lg border px-3 py-3 text-base focus:outline-none focus:ring-2 ${
|
||||
titleMissing
|
||||
? 'border-red-500 focus:ring-red-500/30'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/30'
|
||||
}`}
|
||||
placeholder="Sans titre"
|
||||
/>
|
||||
</label>
|
||||
{titleMissing && (
|
||||
<span role="alert" className="text-sm text-red-600">
|
||||
{TITLE_REQUIRED_MESSAGE}
|
||||
</span>
|
||||
)}
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-gray-500">Contenu</span>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(event) => onContentChange(event.target.value)}
|
||||
className="w-full min-h-48 rounded-lg border border-gray-300 px-3 py-3 text-base focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
placeholder="Écrire ici…"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg border border-gray-300 px-5 py-3 text-gray-600 hover:bg-gray-50 active:bg-gray-100"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-lg bg-blue-600 px-5 py-3 text-white hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteEditor(props: NoteEditorProps) {
|
||||
const [editing, setEditing] = useState(props.mode === 'create')
|
||||
const [title, setTitle] = useState(props.mode === 'edit' ? props.note.title : '')
|
||||
const [content, setContent] = useState(props.mode === 'edit' ? props.note.content : '')
|
||||
const [titleMissing, setTitleMissing] = useState(false)
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
const [title, setTitle] = useState(note.title)
|
||||
const [content, setContent] = useState(note.content)
|
||||
|
||||
const startEditing = () => {
|
||||
setTitle(note.title)
|
||||
setContent(note.content)
|
||||
setEditing(true)
|
||||
const handleTitleChange = (value: string) => {
|
||||
setTitle(value)
|
||||
if (titleMissing && value.trim() !== '') setTitleMissing(false)
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
setEditing(false)
|
||||
const handleSubmit = () => {
|
||||
if (title.trim() === '') {
|
||||
setTitleMissing(true)
|
||||
return
|
||||
}
|
||||
const safeTitle = title.trim()
|
||||
if (props.mode === 'create') {
|
||||
props.onCreate(safeTitle, content)
|
||||
} else {
|
||||
props.onUpdate(props.note.id, { title: safeTitle, content })
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
onUpdate(note.id, { title, content })
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
if (props.mode === 'create' || editing) {
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-4 rounded-xl border border-gray-200 bg-white p-4 shadow-sm"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
save()
|
||||
}}
|
||||
>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-gray-500">Titre</span>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-3 text-base focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
placeholder="Sans titre"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-gray-500">Contenu</span>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
className="w-full min-h-48 rounded-lg border border-gray-300 px-3 py-3 text-base focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
placeholder="Écrire ici…"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancel}
|
||||
className="rounded-lg border border-gray-300 px-5 py-3 text-gray-600 hover:bg-gray-50 active:bg-gray-100"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-lg bg-blue-600 px-5 py-3 text-white hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<NoteForm
|
||||
title={title}
|
||||
content={content}
|
||||
titleMissing={titleMissing}
|
||||
cancelLabel="Annuler"
|
||||
onTitleChange={handleTitleChange}
|
||||
onContentChange={setContent}
|
||||
onCancel={props.mode === 'create' ? props.onCancelCreate : () => setEditing(false)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { note } = props
|
||||
return (
|
||||
<article className="flex flex-col gap-5 rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
@@ -95,7 +157,11 @@ function NoteEditor({ note, onUpdate, onDelete, autoEdit = false }: NoteEditorPr
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={startEditing}
|
||||
onClick={() => {
|
||||
setTitle(props.note.title)
|
||||
setContent(props.note.content)
|
||||
setEditing(true)
|
||||
}}
|
||||
className="rounded-lg bg-blue-600 px-5 py-3 text-white hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
Éditer
|
||||
@@ -106,7 +172,7 @@ function NoteEditor({ note, onUpdate, onDelete, autoEdit = false }: NoteEditorPr
|
||||
title={note.title}
|
||||
onConfirm={() => {
|
||||
setConfirmDelete(false)
|
||||
onDelete(note.id)
|
||||
props.onDelete(note.id)
|
||||
}}
|
||||
onCancel={() => setConfirmDelete(false)}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user