A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
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}