load project
This commit is contained in:
42
frontend/src/components/Navbar.tsx
Normal file
42
frontend/src/components/Navbar.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
export default function Navbar() {
|
||||
const { user, logout } = useAuth()
|
||||
const location = useLocation()
|
||||
|
||||
const isActive = (path: string) =>
|
||||
location.pathname === path || location.pathname.startsWith(path + '/')
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<Link to="/" className="navbar-brand">
|
||||
Document Template Editor
|
||||
</Link>
|
||||
<div className="navbar-links">
|
||||
<Link to="/templates" className={isActive('/templates') ? 'active' : ''}>
|
||||
Templates
|
||||
</Link>
|
||||
<Link to="/documents" className={isActive('/documents') ? 'active' : ''}>
|
||||
Documents
|
||||
</Link>
|
||||
{user?.role === 'admin' && (
|
||||
<Link to="/users" className={isActive('/users') ? 'active' : ''}>
|
||||
Users
|
||||
</Link>
|
||||
)}
|
||||
{user && (
|
||||
<>
|
||||
<span>
|
||||
{user.username}{' '}
|
||||
<span className={`badge badge-${user.role}`}>{user.role}</span>
|
||||
</span>
|
||||
<button className="btn btn-secondary btn-sm" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
80
frontend/src/components/TableRowEditor.tsx
Normal file
80
frontend/src/components/TableRowEditor.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { TemplateVariable } from '../types'
|
||||
import VariableField from './VariableField'
|
||||
|
||||
interface TableRowEditorProps {
|
||||
tableVariable: TemplateVariable
|
||||
childVariables: TemplateVariable[]
|
||||
rows: Record<string, unknown>[]
|
||||
onChange: (rows: Record<string, unknown>[]) => void
|
||||
}
|
||||
|
||||
export default function TableRowEditor({
|
||||
tableVariable,
|
||||
childVariables,
|
||||
rows,
|
||||
onChange,
|
||||
}: TableRowEditorProps) {
|
||||
const addRow = () => {
|
||||
const newRow: Record<string, unknown> = {}
|
||||
childVariables.forEach((v) => {
|
||||
newRow[v.name] = v.default_value ?? ''
|
||||
})
|
||||
onChange([...rows, newRow])
|
||||
}
|
||||
|
||||
const removeRow = (index: number) => {
|
||||
onChange(rows.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const updateRowField = (rowIndex: number, field: string, value: unknown) => {
|
||||
const updated = rows.map((row, i) =>
|
||||
i === rowIndex ? { ...row, [field]: value } : row,
|
||||
)
|
||||
onChange(updated)
|
||||
}
|
||||
|
||||
const tableStyle = tableVariable.style_params?.table_style as
|
||||
| { row_styles?: Record<string, unknown>[][] }
|
||||
| undefined
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label>{tableVariable.label} (repeating table rows)</label>
|
||||
{tableStyle && (
|
||||
<div className="style-hint">
|
||||
Table styles preserved from template ({tableStyle.row_styles?.length ?? 0} reference
|
||||
rows)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div key={rowIndex} className="table-row-editor">
|
||||
<div className="table-row-editor-header">
|
||||
<strong>Row {rowIndex + 1}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => removeRow(rowIndex)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-row-fields">
|
||||
{childVariables.map((child) => (
|
||||
<VariableField
|
||||
key={child.id}
|
||||
variable={child}
|
||||
value={row[child.name]}
|
||||
onChange={(val) => updateRowField(rowIndex, child.name, val)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button type="button" className="btn btn-secondary" onClick={addRow}>
|
||||
+ Add Row
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
frontend/src/components/VariableField.tsx
Normal file
71
frontend/src/components/VariableField.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { TemplateVariable } from '../types'
|
||||
|
||||
interface VariableFieldProps {
|
||||
variable: TemplateVariable
|
||||
value: unknown
|
||||
onChange: (value: unknown) => void
|
||||
}
|
||||
|
||||
export default function VariableField({ variable, value, onChange }: VariableFieldProps) {
|
||||
const styleHint = variable.style_params
|
||||
? [
|
||||
variable.style_params.font_name && `Font: ${variable.style_params.font_name}`,
|
||||
variable.style_params.font_size && `Size: ${variable.style_params.font_size}pt`,
|
||||
variable.style_params.bold && 'Bold',
|
||||
variable.style_params.alignment && `Align: ${variable.style_params.alignment}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
: null
|
||||
|
||||
const renderInput = () => {
|
||||
switch (variable.field_type) {
|
||||
case 'textarea':
|
||||
return (
|
||||
<textarea
|
||||
value={(value as string) ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
required={variable.is_required}
|
||||
/>
|
||||
)
|
||||
case 'number':
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
value={(value as number) ?? ''}
|
||||
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}
|
||||
required={variable.is_required}
|
||||
/>
|
||||
)
|
||||
case 'date':
|
||||
return (
|
||||
<input
|
||||
type="date"
|
||||
value={(value as string) ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
required={variable.is_required}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={(value as string) ?? ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
required={variable.is_required}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{variable.label}
|
||||
{variable.is_required && ' *'}
|
||||
</label>
|
||||
{renderInput()}
|
||||
{styleHint && <div className="style-hint">Style: {styleHint}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user