157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
import { useRef, useState } from 'react';
|
||
import { useDialogueStore } from '../../store/dialogueStore';
|
||
import { parseDialogueFile } from '../../utils/fileIO';
|
||
import { useValidation } from '../../hooks/useValidation';
|
||
import styles from './LeftPanel.module.css';
|
||
|
||
export function LeftPanel() {
|
||
const { file, fileName, selectedDialogueId, loadFile, selectDialogue, createDialogue, deleteDialogue, duplicateDialogue, exportFile } = useDialogueStore();
|
||
const { issuesByNodeId } = useValidation();
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const [newId, setNewId] = useState('');
|
||
const [creating, setCreating] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||
const f = e.target.files?.[0];
|
||
if (!f) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
try {
|
||
const parsed = parseDialogueFile(ev.target?.result as string);
|
||
loadFile(parsed, f.name);
|
||
setError('');
|
||
} catch (err) {
|
||
setError(String(err));
|
||
}
|
||
};
|
||
reader.readAsText(f);
|
||
e.target.value = '';
|
||
}
|
||
|
||
function handleCreate() {
|
||
if (!newId.trim()) return;
|
||
if (file?.dialogues.some(d => d.id === newId.trim())) {
|
||
setError(`ID "${newId.trim()}" already exists`);
|
||
return;
|
||
}
|
||
createDialogue(newId.trim());
|
||
setNewId('');
|
||
setCreating(false);
|
||
setError('');
|
||
}
|
||
|
||
function dialogueHasIssues(dialogueId: string) {
|
||
// Simple check: are there any issues for nodes in this dialogue?
|
||
// We only have current dialogue issues so we check if this is selected
|
||
return selectedDialogueId === dialogueId && Object.keys(issuesByNodeId).length > 0;
|
||
}
|
||
|
||
function dialogueHasErrors(dialogueId: string) {
|
||
return selectedDialogueId === dialogueId &&
|
||
Object.values(issuesByNodeId).some(list => list.some(i => i.severity === 'error'));
|
||
}
|
||
|
||
return (
|
||
<div className={styles.panel}>
|
||
<div className={styles.header}>
|
||
<span className={styles.title}>Dialogues</span>
|
||
{file && <span className={styles.fileName}>{fileName}</span>}
|
||
</div>
|
||
|
||
<div className={styles.fileButtons}>
|
||
<button className={styles.btn} onClick={() => fileInputRef.current?.click()}>
|
||
📂 Load
|
||
</button>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept=".json"
|
||
style={{ display: 'none' }}
|
||
onChange={handleFileChange}
|
||
/>
|
||
{file && (
|
||
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={exportFile}>
|
||
💾 Save
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{error && <div className={styles.errorMsg}>{error}</div>}
|
||
|
||
<div className={styles.list}>
|
||
{!file && (
|
||
<div className={styles.emptyMsg}>Load a JSON file to start</div>
|
||
)}
|
||
{file?.dialogues.map(d => {
|
||
const hasErr = dialogueHasErrors(d.id);
|
||
const hasWarn = !hasErr && dialogueHasIssues(d.id);
|
||
return (
|
||
<div
|
||
key={d.id}
|
||
className={[
|
||
styles.dialogueItem,
|
||
d.id === selectedDialogueId ? styles.active : '',
|
||
].join(' ')}
|
||
onClick={() => selectDialogue(d.id)}
|
||
>
|
||
<span className={styles.dialogueId}>
|
||
{hasErr && <span className={styles.errDot} title="Has errors">●</span>}
|
||
{hasWarn && <span className={styles.warnDot} title="Has warnings">●</span>}
|
||
{d.mobileMode && <span className={styles.mobileDot} title="Mobile mode">📱</span>}
|
||
{d.id}
|
||
</span>
|
||
<button
|
||
className={styles.copyBtn}
|
||
title="Duplicate dialogue"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
duplicateDialogue(d.id);
|
||
}}
|
||
>
|
||
⧉
|
||
</button>
|
||
<button
|
||
className={styles.deleteBtn}
|
||
title="Delete dialogue"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
if (confirm(`Delete "${d.id}"?`)) deleteDialogue(d.id);
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div className={styles.footer}>
|
||
{creating ? (
|
||
<div className={styles.createForm}>
|
||
<input
|
||
className={styles.createInput}
|
||
value={newId}
|
||
onChange={e => setNewId(e.target.value)}
|
||
placeholder="dialogue_id"
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter') handleCreate();
|
||
if (e.key === 'Escape') { setCreating(false); setNewId(''); }
|
||
}}
|
||
autoFocus
|
||
/>
|
||
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={handleCreate}>✓</button>
|
||
<button className={styles.btn} onClick={() => { setCreating(false); setNewId(''); }}>✗</button>
|
||
</div>
|
||
) : (
|
||
file && (
|
||
<button className={[styles.btn, styles.btnNew].join(' ')} onClick={() => setCreating(true)}>
|
||
+ New Dialogue
|
||
</button>
|
||
)
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|