first commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user