320 lines
14 KiB
Plaintext
320 lines
14 KiB
Plaintext
import React, { useState, useEffect, useRef } from 'react';
|
|
import './DatasetManager.css';
|
|
import { Dataset, LibraryEntry, DataRecord, DataValue } from '../../types';
|
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
import { AddCircleIcon, Delete02Icon, FolderAddIcon, Edit02Icon } from "@hugeicons/core-free-icons";
|
|
|
|
// Reusable Modal Component
|
|
const Modal: React.FC<{
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
actions: { label: string; onClick: () => void; className?: string }[];
|
|
size?: 'default' | 'large';
|
|
}> = ({ isOpen, onClose, title, children, actions, size = 'default' }) => {
|
|
if (!isOpen) return null;
|
|
|
|
const modalContentClass = `modal-content ${size === 'large' ? 'entry-editor-content' : ''}`;
|
|
|
|
return (
|
|
<div className="modal-overlay" onClick={onClose}>
|
|
<div className={modalContentClass} onClick={e => e.stopPropagation()}>
|
|
<h3>{title}</h3>
|
|
<div className="modal-body">{children}</div>
|
|
<div className="modal-actions">
|
|
{actions.map((action, index) => (
|
|
<button key={index} onClick={action.onClick} className={`builder-btn ${action.className || ''}`}>
|
|
{action.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Data Record Editor Component
|
|
const DataRecordEditor = ({ data, onChange }: { data: DataRecord, onChange: (newData: DataRecord) => void }) => {
|
|
const [addingInfo, setAddingInfo] = useState<{type: 'field' | 'group'} | null>(null);
|
|
const [newKey, setNewKey] = useState('');
|
|
const newKeyInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (addingInfo) {
|
|
newKeyInputRef.current?.focus();
|
|
}
|
|
}, [addingInfo]);
|
|
|
|
const handleFieldChange = (key: string, value: DataValue) => {
|
|
onChange({ ...data, [key]: value });
|
|
};
|
|
|
|
const handleRemoveField = (key: string) => {
|
|
const { [key]: _, ...rest } = data;
|
|
onChange(rest);
|
|
};
|
|
|
|
const handleInitiateAdd = (type: 'field' | 'group') => {
|
|
setAddingInfo({ type });
|
|
};
|
|
|
|
const handleConfirmAdd = () => {
|
|
const key = newKey.trim();
|
|
if (key && !data.hasOwnProperty(key)) {
|
|
onChange({ ...data, [key]: addingInfo?.type === 'group' ? {} : '' });
|
|
setNewKey('');
|
|
setAddingInfo(null);
|
|
} else if (key) {
|
|
alert(`The key "${key}" already exists.`);
|
|
} else {
|
|
setAddingInfo(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="data-record-editor">
|
|
{Object.keys(data).length === 0 && <p className="placeholder-text">This entry is empty. Add a field or group to begin.</p>}
|
|
{Object.entries(data).map(([key, value]) => (
|
|
<div key={key} className="data-field-row">
|
|
<label>{key}</label>
|
|
<div className="data-field-input-group">
|
|
{typeof value === 'object' && value !== null ?
|
|
(
|
|
<div className="data-group-container">
|
|
<DataRecordEditor
|
|
data={value as DataRecord}
|
|
onChange={(newValue) => handleFieldChange(key, newValue)}
|
|
/>
|
|
</div>
|
|
) :
|
|
( <textarea value={value as string || ''} onChange={e => handleFieldChange(key, e.target.value)} rows={3} /> )
|
|
}
|
|
</div>
|
|
<button onClick={() => handleRemoveField(key)} className="builder-btn remove-btn">
|
|
<HugeiconsIcon icon={Delete02Icon} size={16}/>
|
|
</button>
|
|
</div>
|
|
))}
|
|
{addingInfo ? (
|
|
<div className="add-field-form">
|
|
<input
|
|
ref={newKeyInputRef}
|
|
type="text"
|
|
value={newKey}
|
|
onChange={e => setNewKey(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && handleConfirmAdd()}
|
|
placeholder={`New ${addingInfo.type} name...`}
|
|
/>
|
|
<button className="builder-btn save-btn" onClick={handleConfirmAdd}>Save</button>
|
|
<button className="builder-btn load-btn" onClick={() => setAddingInfo(null)}>Cancel</button>
|
|
</div>
|
|
) : (
|
|
<div className="data-editor-actions">
|
|
<button className="builder-btn add-btn" onClick={() => handleInitiateAdd('field')}><HugeiconsIcon icon={AddCircleIcon} size={14}/> Add Field</button>
|
|
<button className="builder-btn add-btn" onClick={() => handleInitiateAdd('group')}><HugeiconsIcon icon={FolderAddIcon} size={14}/> Add Group</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Main Dataset Manager Component
|
|
function DatasetManager() {
|
|
const [datasets, setDatasets] = useState<Dataset[]>(() => {
|
|
try {
|
|
const stored = localStorage.getItem('libraryDatasets');
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch (error) {
|
|
console.error("Failed to parse datasets from localStorage", error);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const [selectedDatasetId, setSelectedDatasetId] = useState<number | null>(null);
|
|
const [editingEntry, setEditingEntry] = useState<LibraryEntry | null>(null);
|
|
const [showCreateDatasetModal, setShowCreateDatasetModal] = useState(false);
|
|
const [newDatasetInfo, setNewDatasetInfo] = useState({ name: '', type: 'generic' });
|
|
const [confirmDeleteInfo, setConfirmDeleteInfo] = useState<{type: 'dataset'|'entry', id: number, name: string} | null>(null);
|
|
|
|
// Derive selectedDataset from state - this is our single source of truth
|
|
const selectedDataset = datasets.find(d => d.id === selectedDatasetId) || null;
|
|
|
|
// Effect to save to localStorage whenever datasets change
|
|
useEffect(() => {
|
|
try {
|
|
localStorage.setItem('libraryDatasets', JSON.stringify(datasets));
|
|
} catch (error) {
|
|
console.error("Failed to save datasets to localStorage", error);
|
|
}
|
|
}, [datasets]);
|
|
|
|
const handleSaveNewDataset = () => {
|
|
if (!newDatasetInfo.name.trim()) {
|
|
alert("Dataset name is required.");
|
|
return;
|
|
}
|
|
const newDataset: Dataset = { id: Date.now(), name: newDatasetInfo.name, type: newDatasetInfo.type || 'generic', entries: [] };
|
|
setDatasets([...datasets, newDataset]);
|
|
setShowCreateDatasetModal(false);
|
|
setNewDatasetInfo({ name: '', type: 'generic' });
|
|
};
|
|
|
|
const handleExecuteDelete = () => {
|
|
if (!confirmDeleteInfo) return;
|
|
const { type, id } = confirmDeleteInfo;
|
|
|
|
if (type === 'dataset') {
|
|
setDatasets(datasets.filter(d => d.id !== id));
|
|
if (selectedDatasetId === id) {
|
|
setSelectedDatasetId(null);
|
|
setEditingEntry(null);
|
|
}
|
|
} else if (type === 'entry' && selectedDatasetId) {
|
|
const updatedDatasets = datasets.map(d => {
|
|
if (d.id === selectedDatasetId) {
|
|
return { ...d, entries: d.entries.filter(e => e.id !== id) };
|
|
}
|
|
return d;
|
|
});
|
|
setDatasets(updatedDatasets);
|
|
}
|
|
setConfirmDeleteInfo(null);
|
|
};
|
|
|
|
const handleAddOrUpdateEntry = (entry: LibraryEntry) => {
|
|
if (!selectedDatasetId || !entry.name.trim()) return;
|
|
|
|
const updatedDatasets = datasets.map(d => {
|
|
if (d.id === selectedDatasetId) {
|
|
const isNew = entry.id === 0;
|
|
const updatedEntries = isNew
|
|
? [...d.entries, { ...entry, id: Date.now() }]
|
|
: d.entries.map(e => e.id === entry.id ? entry : e);
|
|
return { ...d, entries: updatedEntries };
|
|
}
|
|
return d;
|
|
});
|
|
setDatasets(updatedDatasets);
|
|
setEditingEntry(null);
|
|
};
|
|
|
|
return (
|
|
<div className="dataset-manager-container">
|
|
{/* Entry Editor Modal */}
|
|
{editingEntry && (
|
|
<Modal
|
|
isOpen={!!editingEntry}
|
|
onClose={() => setEditingEntry(null)}
|
|
title={editingEntry.id === 0 ? 'Create New Entry' : 'Edit Entry'}
|
|
size="large"
|
|
actions={[
|
|
{ label: 'Cancel', onClick: () => setEditingEntry(null), className: 'load-btn' },
|
|
{ label: 'Save Entry', onClick: () => handleAddOrUpdateEntry(editingEntry), className: 'save-btn' }
|
|
]}
|
|
>
|
|
<div className="form-group">
|
|
<label>Entry Name</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g., 'Longsword' or 'Fireball'"
|
|
value={editingEntry.name}
|
|
onChange={(e) => setEditingEntry({ ...editingEntry, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Data</label>
|
|
<DataRecordEditor
|
|
data={editingEntry.data}
|
|
onChange={(newData) => setEditingEntry({ ...editingEntry, data: newData })}
|
|
/>
|
|
</div>
|
|
</Modal>
|
|
)}
|
|
|
|
{/* Create Dataset Modal */}
|
|
<Modal
|
|
isOpen={showCreateDatasetModal}
|
|
onClose={() => setShowCreateDatasetModal(false)}
|
|
title="Create New Dataset"
|
|
actions={[
|
|
{ label: 'Cancel', onClick: () => setShowCreateDatasetModal(false), className: 'load-btn' },
|
|
{ label: 'Create', onClick: handleSaveNewDataset, className: 'save-btn' }
|
|
]}
|
|
>
|
|
<div className="form-group">
|
|
<label htmlFor="datasetName">Dataset Name</label>
|
|
<input id="datasetName" type="text" placeholder='e.g., "Core Rulebook Items"' value={newDatasetInfo.name} onChange={e => setNewDatasetInfo({...newDatasetInfo, name: e.target.value})}/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="datasetType">Dataset Type</label>
|
|
<input id="datasetType" type="text" placeholder='e.g., "spells" or "equipment"' value={newDatasetInfo.type} onChange={e => setNewDatasetInfo({...newDatasetInfo, type: e.target.value})}/>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* Confirm Deletion Modal */}
|
|
<Modal
|
|
isOpen={!!confirmDeleteInfo}
|
|
onClose={() => setConfirmDeleteInfo(null)}
|
|
title="Confirm Deletion"
|
|
actions={[
|
|
{ label: 'Cancel', onClick: () => setConfirmDeleteInfo(null), className: 'load-btn' },
|
|
{ label: 'Delete', onClick: handleExecuteDelete, className: 'delete-btn' }
|
|
]}
|
|
>
|
|
<p>Are you sure you want to delete the {confirmDeleteInfo?.type} "<strong>{confirmDeleteInfo?.name}</strong>"? This action cannot be undone.</p>
|
|
</Modal>
|
|
|
|
|
|
<div className="datasets-list-panel">
|
|
<div className="panel-header">
|
|
<h3>Datasets</h3>
|
|
</div>
|
|
<button className="builder-btn new-dataset-btn" onClick={() => { setNewDatasetInfo({name: '', type: 'generic'}); setShowCreateDatasetModal(true); }}>
|
|
<HugeiconsIcon icon={FolderAddIcon} size={16}/> New Dataset
|
|
</button>
|
|
<ul className="list-group">
|
|
{datasets.map(d => (
|
|
<li key={d.id}
|
|
className={`list-group-item ${selectedDatasetId === d.id ? 'active' : ''}`}
|
|
onClick={() => setSelectedDatasetId(d.id)}>
|
|
<div className="dataset-info">
|
|
<span className="dataset-name">{d.name}</span>
|
|
<span className="dataset-type">{d.type}</span>
|
|
</div>
|
|
<button className="builder-btn delete-btn" onClick={(e) => { e.stopPropagation(); setConfirmDeleteInfo({type: 'dataset', id: d.id, name: d.name})}}>
|
|
<HugeiconsIcon icon={Delete02Icon} size={16}/>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="entries-list-panel">
|
|
{selectedDataset ? (
|
|
<>
|
|
<div className="panel-header">
|
|
<h3>{selectedDataset.name}</h3>
|
|
<button className="builder-btn save-btn" onClick={() => setEditingEntry({id: 0, name: '', data: { 'Description': '' }})}>
|
|
<HugeiconsIcon icon={AddCircleIcon} size={16}/> New Entry
|
|
</button>
|
|
</div>
|
|
<ul className="list-group">
|
|
{selectedDataset.entries.map(entry => (
|
|
<li key={entry.id} className="list-group-item">
|
|
<span className="dataset-info">{entry.name}</span>
|
|
<div className="entry-actions">
|
|
<button className="builder-btn load-btn" onClick={() => setEditingEntry(entry)}><HugeiconsIcon icon={Edit02Icon} size={16}/></button>
|
|
<button className="builder-btn delete-btn" onClick={() => setConfirmDeleteInfo({type: 'entry', id: entry.id, name: entry.name})}><HugeiconsIcon icon={Delete02Icon} size={16}/></button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
) : <div className="placeholder-text">Select a dataset to view its entries.</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default DatasetManager; |