A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
at main 170 lines 5.8 kB view raw
1import { useState, useEffect } from 'react'; 2 3interface FrontmatterEditorProps { 4 frontmatter: Record<string, any>; 5 onChange: (frontmatter: Record<string, any>) => void; 6 className?: string; 7} 8 9export function FrontmatterEditor({ frontmatter, onChange, className = '' }: FrontmatterEditorProps) { 10 const [fields, setFields] = useState<Record<string, any>>(frontmatter || {}); 11 const [newKey, setNewKey] = useState(''); 12 const [newValue, setNewValue] = useState(''); 13 14 useEffect(() => { 15 setFields(frontmatter || {}); 16 }, [frontmatter]); 17 18 const updateField = (key: string, value: any) => { 19 const updated = { ...fields, [key]: value }; 20 setFields(updated); 21 onChange(updated); 22 }; 23 24 const deleteField = (key: string) => { 25 const updated = { ...fields }; 26 delete updated[key]; 27 setFields(updated); 28 onChange(updated); 29 }; 30 31 const addField = () => { 32 if (!newKey.trim()) return; 33 34 const updated = { ...fields, [newKey]: newValue }; 35 setFields(updated); 36 onChange(updated); 37 setNewKey(''); 38 setNewValue(''); 39 }; 40 41 const inferType = (value: any): string => { 42 if (Array.isArray(value)) return 'array'; 43 if (typeof value === 'boolean') return 'boolean'; 44 if (typeof value === 'number') return 'number'; 45 return 'string'; 46 }; 47 48 const renderFieldInput = (key: string, value: any) => { 49 const type = inferType(value); 50 51 switch (type) { 52 case 'boolean': 53 return ( 54 <input 55 type="checkbox" 56 checked={value} 57 onChange={(e) => updateField(key, e.target.checked)} 58 className="w-5 h-5 text-amber-600 border-2 border-gray-900 focus:ring-0 focus:ring-offset-0" 59 /> 60 ); 61 62 case 'array': 63 return ( 64 <input 65 type="text" 66 value={Array.isArray(value) ? value.join(', ') : ''} 67 onChange={(e) => updateField(key, e.target.value.split(',').map((v) => v.trim()))} 68 className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 69 placeholder="comma, separated, values" 70 /> 71 ); 72 73 case 'number': 74 return ( 75 <input 76 type="number" 77 value={value} 78 onChange={(e) => updateField(key, parseFloat(e.target.value) || 0)} 79 className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 80 /> 81 ); 82 83 default: 84 return ( 85 <input 86 type="text" 87 value={value} 88 onChange={(e) => updateField(key, e.target.value)} 89 className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 90 /> 91 ); 92 } 93 }; 94 95 return ( 96 <div className={`bg-amber-50 border-2 border-gray-900 p-6 ${className}`}> 97 <div className="flex items-center justify-between mb-4"> 98 <h3 className="text-lg font-bold" style={{ fontFamily: 'Archivo Black, sans-serif' }}> 99 Frontmatter 100 </h3> 101 <span className="text-xs text-gray-600 font-mono">YAML Metadata</span> 102 </div> 103 104 {Object.keys(fields).length === 0 ? ( 105 <div className="text-sm text-gray-600 italic mb-4 p-4 bg-white border-2 border-dashed border-gray-300"> 106 No frontmatter fields. Add one below. 107 </div> 108 ) : ( 109 <div className="space-y-3 mb-4"> 110 {Object.entries(fields).map(([key, value]) => ( 111 <div key={key} className="flex items-center gap-2"> 112 <div className="flex-1 grid grid-cols-2 gap-2"> 113 <div className="px-3 py-2 bg-gray-900 text-white font-mono text-sm font-semibold flex items-center"> 114 {key} 115 </div> 116 {renderFieldInput(key, value)} 117 </div> 118 <button 119 onClick={() => deleteField(key)} 120 className="px-3 py-2 bg-red-600 text-white font-semibold hover:bg-red-700 transition-colors border-2 border-gray-900" 121 title="Delete field" 122 > 123 × 124 </button> 125 </div> 126 ))} 127 </div> 128 )} 129 130 <div className="border-t-2 border-gray-900 pt-4"> 131 <div className="text-sm font-semibold mb-2" style={{ fontFamily: 'Archivo Black, sans-serif' }}> 132 Add Field 133 </div> 134 <div className="flex gap-2"> 135 <input 136 type="text" 137 value={newKey} 138 onChange={(e) => setNewKey(e.target.value)} 139 placeholder="Key (e.g., title)" 140 className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 141 onKeyDown={(e) => { 142 if (e.key === 'Enter' && newKey.trim()) { 143 addField(); 144 } 145 }} 146 /> 147 <input 148 type="text" 149 value={newValue} 150 onChange={(e) => setNewValue(e.target.value)} 151 placeholder="Value" 152 className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 153 onKeyDown={(e) => { 154 if (e.key === 'Enter' && newKey.trim()) { 155 addField(); 156 } 157 }} 158 /> 159 <button 160 onClick={addField} 161 disabled={!newKey.trim()} 162 className="px-4 py-2 bg-gray-900 text-white font-semibold hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors border-2 border-gray-900" 163 > 164 + Add 165 </button> 166 </div> 167 </div> 168 </div> 169 ); 170}