= {
+ command: { command: '', description: '', whenToUse: '', example: '' },
+ snippet: { language: '', code: '', description: '', notes: '' },
+ decision: { context: '', decision: '', alternatives: '', consequences: '' },
+ recipe: { ingredients: '', steps: '', time: '', notes: '' },
+ procedure: { objective: '', steps: '', requirements: '', commonProblems: '' },
+ inventory: { item: '', quantity: '', location: '', notes: '' },
+ note: { content: '' },
+}
+
+function serializeToMarkdown(type: NoteType, fields: TypeFields): string {
+ switch (type) {
+ case 'command': {
+ const f = fields as CommandFields
+ return `## Comando\n\n${f.command}\n\n## Qué hace\n\n${f.description}\n\n## Cuándo usarlo\n\n${f.whenToUse}\n\n## Ejemplo\n\`\`\`bash\n${f.example}\n\`\`\``
+ }
+ case 'snippet': {
+ const f = fields as SnippetFields
+ return `## Snippet\n\n## Lenguaje\n\n${f.language}\n\n## Código\n\n\`\`\`${f.language}\n${f.code}\n\`\`\`\n\n## Qué resuelve\n\n${f.description}\n\n## Notas\n\n${f.notes}`
+ }
+ case 'decision': {
+ const f = fields as DecisionFields
+ return `## Contexto\n\n${f.context}\n\n## Decisión\n\n${f.decision}\n\n## Alternativas consideradas\n\n${f.alternatives}\n\n## Consecuencias\n\n${f.consequences}`
+ }
+ case 'recipe': {
+ const f = fields as RecipeFields
+ return `## Ingredientes\n\n${f.ingredients}\n\n## Pasos\n\n${f.steps}\n\n## Tiempo\n\n${f.time}\n\n## Notas\n\n${f.notes}`
+ }
+ case 'procedure': {
+ const f = fields as ProcedureFields
+ return `## Objetivo\n\n${f.objective}\n\n## Pasos\n\n${f.steps}\n\n## Requisitos\n\n${f.requirements}\n\n## Problemas comunes\n\n${f.commonProblems}`
+ }
+ case 'inventory': {
+ const f = fields as InventoryFields
+ return `## Item\n\n${f.item}\n\n## Cantidad\n\n${f.quantity}\n\n## Ubicación\n\n${f.location}\n\n## Notas\n\n${f.notes}`
+ }
+ case 'note':
+ default: {
+ const f = fields as NoteFields
+ return `## Notas\n\n${f.content}`
+ }
+ }
+}
+
+function parseMarkdownToFields(type: NoteType, content: string): TypeFields {
+ const sections = content.split(/^##\s+/m).filter(Boolean)
+
+ switch (type) {
+ case 'command': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ const exampleMatch = content.match(/```bash\n([\s\S]*?)```/)
+ return {
+ command: getSection('Comando'),
+ description: getSection('Qué hace'),
+ whenToUse: getSection('Cuándo usarlo'),
+ example: exampleMatch ? exampleMatch[1].trim() : '',
+ }
+ }
+ case 'snippet': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ const codeMatch = content.match(/```(\w+)?\n([\s\S]*?)```/)
+ return {
+ language: codeMatch?.[1] || getSection('Lenguaje') || '',
+ code: codeMatch?.[2]?.trim() || '',
+ description: getSection('Qué resuelve'),
+ notes: getSection('Notas'),
+ }
+ }
+ case 'decision': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ return {
+ context: getSection('Contexto'),
+ decision: getSection('Decisión'),
+ alternatives: getSection('Alternativas consideradas'),
+ consequences: getSection('Consecuencias'),
+ }
+ }
+ case 'recipe': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ return {
+ ingredients: getSection('Ingredientes'),
+ steps: getSection('Pasos'),
+ time: getSection('Tiempo'),
+ notes: getSection('Notas'),
+ }
+ }
+ case 'procedure': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ return {
+ objective: getSection('Objetivo'),
+ steps: getSection('Pasos'),
+ requirements: getSection('Requisitos'),
+ commonProblems: getSection('Problemas comunes'),
+ }
+ }
+ case 'inventory': {
+ const getSection = (name: string) => sections.find(s => s.startsWith(name + '\n'))?.split('\n').slice(1).join('\n').trim() || ''
+ return {
+ item: getSection('Item'),
+ quantity: getSection('Cantidad'),
+ location: getSection('Ubicación'),
+ notes: getSection('Notas'),
+ }
+ }
+ case 'note':
+ default: {
+ const noteContent = sections.find(s => s.startsWith('Notas\n'))?.split('\n').slice(1).join('\n').trim() || content
+ return { content: noteContent }
+ }
+ }
+}
+
+// Type-specific form components
+function CommandForm({ fields, onChange }: { fields: CommandFields; onChange: (f: CommandFields) => void }) {
+ return (
+
+
+
+ onChange({ ...fields, command: e.target.value })}
+ placeholder="git commit -m 'fix: resolve issue'"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function SnippetForm({ fields, onChange }: { fields: SnippetFields; onChange: (f: SnippetFields) => void }) {
+ return (
+
+
+
+ onChange({ ...fields, language: e.target.value })}
+ placeholder="typescript, python, bash, etc."
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function DecisionForm({ fields, onChange }: { fields: DecisionFields; onChange: (f: DecisionFields) => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function RecipeForm({ fields, onChange }: { fields: RecipeFields; onChange: (f: RecipeFields) => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+ onChange({ ...fields, time: e.target.value })}
+ placeholder="Ej: 30 minutos, 2 horas"
+ />
+
+
+
+
+
+ )
+}
+
+function ProcedureForm({ fields, onChange }: { fields: ProcedureFields; onChange: (f: ProcedureFields) => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function InventoryForm({ fields, onChange }: { fields: InventoryFields; onChange: (f: InventoryFields) => void }) {
+ return (
+
+
+
+ onChange({ ...fields, item: e.target.value })}
+ placeholder="Nombre del item"
+ />
+
+
+
+ onChange({ ...fields, quantity: e.target.value })}
+ placeholder="Cantidad o número de unidades"
+ />
+
+
+
+ onChange({ ...fields, location: e.target.value })}
+ placeholder="Dónde está guardado"
+ />
+
+
+
+
+
+ )
+}
+
+function NoteTypeForm({ fields, onChange }: { fields: NoteFields; onChange: (f: NoteFields) => void }) {
+ return (
+
+ )
+}
+
+function TagInput({
+ value,
+ onChange,
+}: {
+ value: string[]
+ onChange: (tags: string[]) => void
+}) {
+ const [inputValue, setInputValue] = useState('')
+ const [suggestions, setSuggestions] = useState([])
+ const [showSuggestions, setShowSuggestions] = useState(false)
+ const [selectedIndex, setSelectedIndex] = useState(-1)
+ const inputRef = useRef(null)
+ const containerRef = useRef(null)
+
+ useEffect(() => {
+ const fetchSuggestions = async () => {
+ if (inputValue.trim().length < 1) {
+ setSuggestions([])
+ return
+ }
+
+ try {
+ const res = await fetch(`/api/tags?q=${encodeURIComponent(inputValue)}`)
+ if (res.ok) {
+ const tags: Tag[] = await res.json()
+ setSuggestions(tags.filter(t => !value.includes(t.name)))
+ }
+ } catch (error) {
+ console.error('Error fetching tag suggestions:', error)
+ }
+ }
+
+ const debounce = setTimeout(fetchSuggestions, 150)
+ return () => clearTimeout(debounce)
+ }, [inputValue, value])
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setShowSuggestions(false)
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ const addTag = (tag: string) => {
+ const trimmed = tag.trim()
+ if (trimmed && !value.includes(trimmed)) {
+ onChange([...value, trimmed])
+ }
+ setInputValue('')
+ setSuggestions([])
+ setShowSuggestions(false)
+ inputRef.current?.focus()
+ }
+
+ const removeTag = (tag: string) => {
+ onChange(value.filter(t => t !== tag))
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ if (selectedIndex >= 0 && suggestions[selectedIndex]) {
+ addTag(suggestions[selectedIndex].name)
+ } else if (inputValue.trim()) {
+ addTag(inputValue)
+ }
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1))
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ setSelectedIndex(prev => Math.max(prev - 1, -1))
+ } else if (e.key === 'Escape') {
+ setShowSuggestions(false)
+ setSelectedIndex(-1)
+ } else if (e.key === 'Backspace' && !inputValue && value.length > 0) {
+ removeTag(value[value.length - 1])
+ }
+ }
+
+ return (
+
+
{
+ setInputValue(e.target.value)
+ setShowSuggestions(true)
+ setSelectedIndex(-1)
+ }}
+ onFocus={() => setShowSuggestions(true)}
+ onKeyDown={handleKeyDown}
+ placeholder="Escribe un tag y presiona Enter"
+ />
+
+ {value.length > 0 && (
+
+ {value.map((tag) => (
+
+ {tag}
+
+
+ ))}
+
+ )}
+
+ {showSuggestions && suggestions.length > 0 && (
+
+ {suggestions.map((tag, index) => (
+
+ ))}
+
+ )}
+
+ {showSuggestions && inputValue.trim() && suggestions.length === 0 && (
+
+
+
+ )}
+
+ )
+}
const noteTypes: NoteType[] = ['command', 'snippet', 'decision', 'recipe', 'procedure', 'inventory', 'note']
@@ -20,29 +606,29 @@ interface NoteFormProps {
export function NoteForm({ initialData, isEdit = false }: NoteFormProps) {
const router = useRouter()
const [title, setTitle] = useState(initialData?.title || '')
- const [content, setContent] = useState(initialData?.content || '')
const [type, setType] = useState(initialData?.type || 'note')
- const [tagsInput, setTagsInput] = useState(initialData?.tags.map(t => t.tag.name).join(', ') || '')
+ const [fields, setFields] = useState(() => {
+ if (initialData?.content) {
+ return parseMarkdownToFields(initialData.type, initialData.content)
+ }
+ return defaultFields[type]
+ })
+ const [tags, setTags] = useState(initialData?.tags.map(t => t.tag.name) || [])
const [isFavorite, setIsFavorite] = useState(initialData?.isFavorite || false)
const [isPinned, setIsPinned] = useState(initialData?.isPinned || false)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleTypeChange = (newType: NoteType) => {
setType(newType)
- if (!isEdit && !content) {
- setContent(getTemplate(newType))
- }
+ setFields(defaultFields[newType])
}
+ const content = useMemo(() => serializeToMarkdown(type, fields), [type, fields])
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
- const tags = tagsInput
- .split(',')
- .map(t => t.trim())
- .filter(t => t.length > 0)
-
const noteData = {
title,
content,
@@ -73,6 +659,26 @@ export function NoteForm({ initialData, isEdit = false }: NoteFormProps) {
}
}
+ const renderTypeForm = () => {
+ switch (type) {
+ case 'command':
+ return setFields(f)} />
+ case 'snippet':
+ return setFields(f)} />
+ case 'decision':
+ return setFields(f)} />
+ case 'recipe':
+ return setFields(f)} />
+ case 'procedure':
+ return setFields(f)} />
+ case 'inventory':
+ return setFields(f)} />
+ case 'note':
+ default:
+ return setFields(f)} />
+ }
+ }
+
return (