123 lines
3.7 KiB
React
123 lines
3.7 KiB
React
import { useState } from 'react';
|
|
import { Upload, Plus, Trash2 } from 'lucide-react';
|
|
import { adminApi } from '../api';
|
|
|
|
export function ImageUpload({ value, onChange, label = 'Image' }) {
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
const handleUpload = async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
setUploading(true);
|
|
try {
|
|
const { url } = await adminApi.upload(file);
|
|
onChange(url);
|
|
} catch (err) {
|
|
alert(err.message);
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="form-field">
|
|
<label className="form-label">{label}</label>
|
|
{value && (
|
|
<div className="form-preview">
|
|
{value.match(/\.(mp4|webm)$/i) ? (
|
|
<video src={value} controls />
|
|
) : (
|
|
<img src={value} alt="" />
|
|
)}
|
|
</div>
|
|
)}
|
|
<input
|
|
type="text"
|
|
className="form-input"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder="Paste URL or upload a file below"
|
|
/>
|
|
<label className="form-upload-btn">
|
|
<Upload size={14} />
|
|
{uploading ? 'Uploading...' : 'Choose file'}
|
|
<input type="file" accept="image/*,video/mp4" onChange={handleUpload} hidden disabled={uploading} />
|
|
</label>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Field({ label, value, onChange, multiline = false, type = 'text' }) {
|
|
return (
|
|
<div className="form-field">
|
|
<label className="form-label">{label}</label>
|
|
{multiline ? (
|
|
<textarea
|
|
className="form-textarea"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
rows={4}
|
|
/>
|
|
) : (
|
|
<input
|
|
type={type}
|
|
className="form-input"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ArrayEditor({ label, items, onChange, fields }) {
|
|
const updateItem = (index, key, val) => {
|
|
const next = items.map((item, i) => (i === index ? { ...item, [key]: val } : item));
|
|
onChange(next);
|
|
};
|
|
|
|
const addItem = () => {
|
|
const blank = fields.reduce((acc, f) => ({ ...acc, [f.key]: f.default ?? '' }), { id: Date.now() });
|
|
onChange([...items, blank]);
|
|
};
|
|
|
|
const removeItem = (index) => onChange(items.filter((_, i) => i !== index));
|
|
|
|
return (
|
|
<div className="array-section">
|
|
<div className="array-header">
|
|
<h4 className="array-title">{label}</h4>
|
|
<button type="button" onClick={addItem} className="array-add-btn">
|
|
<Plus size={14} /> Add item
|
|
</button>
|
|
</div>
|
|
{items.length === 0 && (
|
|
<p style={{ color: '#94a3b8', fontSize: '0.85rem', margin: '0 0 1rem' }}>No items yet. Click "Add item" to create one.</p>
|
|
)}
|
|
{items.map((item, index) => (
|
|
<div key={item.id ?? index} className="array-item">
|
|
<div className="array-item-header">
|
|
<span className="array-item-badge">Item {index + 1}</span>
|
|
<button type="button" onClick={() => removeItem(index)} className="array-remove-btn">
|
|
<Trash2 size={12} /> Remove
|
|
</button>
|
|
</div>
|
|
{fields.map((f) =>
|
|
f.type === 'image' ? (
|
|
<ImageUpload key={f.key} label={f.label} value={item[f.key]} onChange={(v) => updateItem(index, f.key, v)} />
|
|
) : (
|
|
<Field
|
|
key={f.key}
|
|
label={f.label}
|
|
value={item[f.key]}
|
|
onChange={(v) => updateItem(index, f.key, v)}
|
|
multiline={f.multiline}
|
|
/>
|
|
)
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|