mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 11:15:41 +02:00
ui: comprehensive UI polish pass
- LoginPage/SetupPage: centered card layout with display font, styled fields and submit button - Topbar: fix duplicate Settings nav item; add styled user-menu with username + logout button - RunsView: format ISO timestamps to readable dates, add colored status badges (completed/failed/running) - AdminView: view-tabs system, styled admin-table, admin-input, status-badge (active/disabled), btn-primary - SettingsView: replace all inline styles with form-section/form-field/field-input/btn-primary/btn-danger - CollectionsView: restructure create form with proper field labels and btn-primary - TagsView: add Tags section heading with separator - ContextRail: show visibility as human-readable label not raw number; fix assign-error class - styles.css: add auth-loading, view-tabs, form utilities, btn variants, status badges, run-status pills, token-banner/row, checkbox-row, coll-create-form, tag-tree-header CSS
This commit is contained in:
parent
3ccfcce87b
commit
4311e85f95
21 changed files with 737 additions and 440 deletions
|
|
@ -114,13 +114,13 @@ export default function AdminView({ archives }) {
|
|||
<section id="admin-view" className="view admin-view is-active">
|
||||
<h1>Admin</h1>
|
||||
|
||||
<div className="admin-tabs">
|
||||
<button className={`admin-tab${tab === 'users' ? ' is-active' : ''}`} onClick={() => setTab('users')}>Users</button>
|
||||
<button className={`admin-tab${tab === 'roles' ? ' is-active' : ''}`} onClick={() => setTab('roles')}>Roles</button>
|
||||
<button className={`admin-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
||||
<div className="view-tabs">
|
||||
<button className={`view-tab${tab === 'users' ? ' is-active' : ''}`} onClick={() => setTab('users')}>Users</button>
|
||||
<button className={`view-tab${tab === 'roles' ? ' is-active' : ''}`} onClick={() => setTab('roles')}>Roles</button>
|
||||
<button className={`view-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="capture-error">{error}</div>}
|
||||
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||
|
||||
{tab === 'users' && (
|
||||
<div className="admin-section">
|
||||
|
|
@ -156,9 +156,9 @@ export default function AdminView({ archives }) {
|
|||
value={newPassword} onChange={e => setNewPassword(e.target.value)} required />
|
||||
<input className="admin-input" type="email" placeholder="Email (optional)"
|
||||
value={newEmail} onChange={e => setNewEmail(e.target.value)} />
|
||||
{createError && <div className="capture-error">{createError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating…' : 'Create User'}
|
||||
{createError && <div className="form-msg form-msg--err">{createError}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create User'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -192,9 +192,9 @@ export default function AdminView({ archives }) {
|
|||
onChange={e => setNewRoleSlug(e.target.value)} required />
|
||||
<input className="admin-input" placeholder="Display Name (e.g. Moderator)"
|
||||
value={newRoleName} onChange={e => setNewRoleName(e.target.value)} required />
|
||||
{roleCreateError && <div className="capture-error">{roleCreateError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creatingRole}>
|
||||
{creatingRole ? 'Creating…' : 'Create Role'}
|
||||
{roleCreateError && <div className="form-msg form-msg--err">{roleCreateError}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={creatingRole}>
|
||||
{creatingRole ? 'Creating\u2026' : 'Create Role'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -324,40 +324,43 @@ export default function CollectionsView({ archiveId }) {
|
|||
</div>
|
||||
|
||||
{/* Create form */}
|
||||
<details className="coll-create-details" style={{ marginTop: '1.5rem' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>Create collection</summary>
|
||||
<form onSubmit={handleCreate} style={{ marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: 400 }}>
|
||||
<label>
|
||||
Name
|
||||
<details className="coll-create-details">
|
||||
<summary>+ Create collection</summary>
|
||||
<form className="coll-create-form" onSubmit={handleCreate}>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-name">Name</label>
|
||||
<input
|
||||
className="capture-input"
|
||||
id="coll-name"
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
||||
placeholder="My Collection"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Slug
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-slug">Slug</label>
|
||||
<input
|
||||
className="capture-input"
|
||||
id="coll-slug"
|
||||
type="text"
|
||||
value={newSlug}
|
||||
onChange={e => setNewSlug(e.target.value)}
|
||||
placeholder="my-collection"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Default visibility
|
||||
<select className="capture-input" style={{ height: 42 }} value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-vis">Default visibility</label>
|
||||
<select className="capture-input" id="coll-vis" style={{ height: 42 }}
|
||||
value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
||||
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{createError && <div className="collections-error">{createError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creating} style={{ alignSelf: 'flex-start', padding: '8px 20px' }}>
|
||||
{creating ? 'Creating…' : 'Create'}
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create collection'}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Visibility', VIS_LABEL[detail.summary.visibility] ?? detail.summary.visibility],
|
||||
['Root', detail.structured_root_relpath],
|
||||
] : []
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
</div>
|
||||
)}
|
||||
{assignError && (
|
||||
<p style={{ color: 'var(--accent)', fontSize: 13, margin: '0 0 8px' }}>{assignError}</p>
|
||||
<p className="form-msg form-msg--err" style={{ margin: '0 0 8px' }}>{assignError}</p>
|
||||
)}
|
||||
<div className="tag-input-wrap">
|
||||
<span className="hash">/</span>
|
||||
|
|
|
|||
|
|
@ -23,32 +23,41 @@ export default function LoginPage({ onLogin }) {
|
|||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<h1>Archivr</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in\u2026' : 'Log in'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="login-card">
|
||||
<h1 className="login-brand">Archivr</h1>
|
||||
<p className="login-tagline">Sign in to your archive</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="login-field">
|
||||
<label className="login-label" htmlFor="login-username">Username</label>
|
||||
<input
|
||||
className="login-input"
|
||||
id="login-username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="login-field">
|
||||
<label className="login-label" htmlFor="login-password">Password</label>
|
||||
<input
|
||||
className="login-input"
|
||||
id="login-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="login-error">{error}</p>}
|
||||
<button className="login-submit" type="submit" disabled={loading}>
|
||||
{loading ? 'Signing in\u2026' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,50 @@
|
|||
function fmtDate(iso) {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
return new Date(iso).toLocaleString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
const cls = status === 'completed' ? 'run-status--completed'
|
||||
: status === 'failed' ? 'run-status--failed'
|
||||
: status === 'running' ? 'run-status--running'
|
||||
: '';
|
||||
return <span className={`run-status ${cls}`}>{status || '—'}</span>;
|
||||
}
|
||||
|
||||
export default function RunsView({ runs }) {
|
||||
return (
|
||||
<section id="runs-view" className="view is-active">
|
||||
<table className="entry-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Started</th><th>Status</th><th>Requested</th><th>Completed</th><th>Failed</th>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th>Requested</th>
|
||||
<th>Completed</th>
|
||||
<th>Failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((run, i) => (
|
||||
{runs.length === 0 ? (
|
||||
<tr><td colSpan={5} style={{ color: 'var(--muted)', padding: '24px 16px', textAlign: 'center' }}>No runs yet.</td></tr>
|
||||
) : runs.map((run, i) => (
|
||||
<tr key={i}>
|
||||
<td>{run.started_at ?? ''}</td>
|
||||
<td>{run.status ?? ''}</td>
|
||||
<td>{run.requested_count ?? ''}</td>
|
||||
<td>{run.completed_count ?? ''}</td>
|
||||
<td>{run.failed_count ?? ''}</td>
|
||||
<td>{fmtDate(run.started_at)}</td>
|
||||
<td><StatusBadge status={run.status} /></td>
|
||||
<td>{run.requested_count ?? '—'}</td>
|
||||
<td>{run.completed_count ?? '—'}</td>
|
||||
<td>{run.failed_count ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,23 +7,24 @@ import {
|
|||
} from '../api.js'
|
||||
|
||||
const ROLE_ADMIN = 4
|
||||
const ROLE_OWNER = 8
|
||||
|
||||
export default function SettingsView() {
|
||||
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
||||
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
||||
const [tab, setTab] = useState('profile')
|
||||
|
||||
const tabs = ['profile', 'tokens', ...(isAdmin ? ['instance'] : [])]
|
||||
const tabLabels = { profile: 'Profile', tokens: 'API Tokens', instance: 'Instance' }
|
||||
|
||||
return (
|
||||
<section className="admin-view">
|
||||
<h1>Settings</h1>
|
||||
<div className="admin-tabs" style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
|
||||
{['profile', 'tokens', ...(isAdmin ? ['instance'] : [])].map(t => (
|
||||
<div className="view-tabs">
|
||||
{tabs.map(t => (
|
||||
<button key={t}
|
||||
className={`nav-link${tab === t ? ' is-active' : ''}`}
|
||||
style={{ borderBottom: tab === t ? '2px solid var(--accent)' : undefined, color: 'var(--ink)' }}
|
||||
className={`view-tab${tab === t ? ' is-active' : ''}`}
|
||||
onClick={() => setTab(t)}>
|
||||
{t === 'profile' ? 'Profile' : t === 'tokens' ? 'API Tokens' : 'Instance'}
|
||||
{tabLabels[t]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -78,34 +79,43 @@ function ProfileTab({ currentUser, setCurrentUser }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<div className="admin-section">
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Display Name</h2>
|
||||
<form className="admin-form" onSubmit={handleSaveProfile}>
|
||||
<input className="admin-input" placeholder={currentUser?.username ?? ''}
|
||||
value={displayName} onChange={e => setDisplayName(e.target.value)} />
|
||||
{saveMsg && (
|
||||
<div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>
|
||||
)}
|
||||
<button className="capture-submit" type="submit" disabled={saving}>
|
||||
<div style={{ maxWidth: 440 }}>
|
||||
<div className="form-section">
|
||||
<h2>Display Name</h2>
|
||||
<form onSubmit={handleSaveProfile}>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="display-name">Name shown in the UI</label>
|
||||
<input className="field-input" id="display-name"
|
||||
placeholder={currentUser?.username ?? ''}
|
||||
value={displayName} onChange={e => setDisplayName(e.target.value)} />
|
||||
</div>
|
||||
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="admin-section" style={{ marginTop: 28 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Change Password</h2>
|
||||
<form className="admin-form" onSubmit={handleChangePassword}>
|
||||
<input className="admin-input" type="password" placeholder="Current password"
|
||||
value={curPw} onChange={e => setCurPw(e.target.value)} required />
|
||||
<input className="admin-input" type="password" placeholder="New password"
|
||||
value={newPw} onChange={e => setNewPw(e.target.value)} required />
|
||||
<input className="admin-input" type="password" placeholder="Confirm new password"
|
||||
value={confirmPw} onChange={e => setConfirmPw(e.target.value)} required />
|
||||
{pwMsg && (
|
||||
<div className={pwMsg.ok ? 'muted' : 'capture-error'}>{pwMsg.text}</div>
|
||||
)}
|
||||
<button className="capture-submit" type="submit" disabled={pwSaving}>
|
||||
<div className="form-section">
|
||||
<h2>Change Password</h2>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="cur-pw">Current password</label>
|
||||
<input className="field-input" id="cur-pw" type="password"
|
||||
value={curPw} onChange={e => setCurPw(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="new-pw">New password</label>
|
||||
<input className="field-input" id="new-pw" type="password"
|
||||
value={newPw} onChange={e => setNewPw(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="confirm-pw">Confirm new password</label>
|
||||
<input className="field-input" id="confirm-pw" type="password"
|
||||
value={confirmPw} onChange={e => setConfirmPw(e.target.value)} required />
|
||||
</div>
|
||||
{pwMsg && <div className={`form-msg form-msg--${pwMsg.ok ? 'ok' : 'err'}`}>{pwMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={pwSaving}>
|
||||
{pwSaving ? 'Changing\u2026' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -120,7 +130,7 @@ function TokensTab() {
|
|||
const [error, setError] = useState(null)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [newToken, setNewToken] = useState(null) // { raw_token, name }
|
||||
const [newToken, setNewToken] = useState(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true); setError(null)
|
||||
|
|
@ -157,48 +167,47 @@ function TokensTab() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 640 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>API Tokens</h2>
|
||||
{newToken && (
|
||||
<div style={{ background: '#e8f5e9', border: '1px solid #a5d6a7', padding: '12px 14px', marginBottom: 16, fontSize: 13 }}>
|
||||
<strong>Token created.</strong> Copy it now \u2014 it will not be shown again.<br />
|
||||
<code style={{ wordBreak: 'break-all', display: 'block', marginTop: 6, padding: '6px 8px', background: '#f5f5f5', border: '1px solid #ddd' }}>
|
||||
{newToken.raw_token}
|
||||
</code>
|
||||
<button style={{ marginTop: 8, fontSize: 12, border: '1px solid #aaa', background: 'none', cursor: 'pointer' }}
|
||||
onClick={() => setNewToken(null)}>Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
<form className="admin-form" onSubmit={handleCreate} style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<input className="admin-input" placeholder="Token name" value={newName}
|
||||
onChange={e => setNewName(e.target.value)} style={{ flex: 1 }} required />
|
||||
<button className="capture-submit" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <div className="capture-error">{error}</div>}
|
||||
{loading ? <div className="muted">Loading\u2026</div> : (
|
||||
<div className="admin-list">
|
||||
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
||||
{tokens.map(tok => (
|
||||
<div key={tok.token_uid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
border: '1px solid var(--line)', background: 'var(--paper-3)', padding: '10px 12px' }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: 14 }}>{tok.name}</strong>
|
||||
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
|
||||
Created {tok.created_at.slice(0, 10)}
|
||||
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<div className="form-section">
|
||||
<h2>API Tokens</h2>
|
||||
{newToken && (
|
||||
<div className="token-banner">
|
||||
<strong>Token created.</strong> Copy it now — it won't be shown again.
|
||||
<code>{newToken.raw_token}</code>
|
||||
<button className="token-dismiss" onClick={() => setNewToken(null)}>Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
<form className="token-create-row" onSubmit={handleCreate}>
|
||||
<input className="field-input field-input--flex" placeholder="Token name"
|
||||
value={newName} onChange={e => setNewName(e.target.value)} required />
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create token'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||
{loading ? (
|
||||
<div className="muted">Loading\u2026</div>
|
||||
) : (
|
||||
<div>
|
||||
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
||||
{tokens.map(tok => (
|
||||
<div key={tok.token_uid} className="token-row">
|
||||
<div className="token-row-info">
|
||||
<strong>{tok.name}</strong>
|
||||
<div className="muted">
|
||||
Created {tok.created_at.slice(0, 10)}
|
||||
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn-danger" style={{ fontSize: 12, padding: '4px 10px' }}
|
||||
onClick={() => handleDelete(tok.token_uid)}>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => handleDelete(tok.token_uid)}
|
||||
style={{ border: '1px solid var(--line)', background: 'none', cursor: 'pointer',
|
||||
color: 'var(--accent)', fontSize: 12, padding: '4px 10px' }}>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -232,40 +241,40 @@ function InstanceTab() {
|
|||
}
|
||||
|
||||
if (loading) return <div className="muted">Loading\u2026</div>
|
||||
if (error) return <div className="capture-error">{error}</div>
|
||||
if (error) return <div className="form-msg form-msg--err">{error}</div>
|
||||
if (!settings) return null
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Instance Settings</h2>
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="admin-section">
|
||||
<div style={{ maxWidth: 440 }}>
|
||||
<div className="form-section">
|
||||
<h2>Instance Settings</h2>
|
||||
<form onSubmit={handleSave}>
|
||||
{[
|
||||
['public_index_enabled', 'Public index (unauthenticated browsing)'],
|
||||
['public_entry_content_enabled', 'Public entry content'],
|
||||
['open_registration_enabled', 'Open registration'],
|
||||
].map(([key, label]) => (
|
||||
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, cursor: 'pointer' }}>
|
||||
<label key={key} className="checkbox-row">
|
||||
<input type="checkbox" checked={!!settings[key]}
|
||||
onChange={e => setSettings(s => ({ ...s, [key]: e.target.checked }))} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontWeight: 600, marginBottom: 4, fontSize: 13 }}>Default entry visibility</label>
|
||||
<select className="admin-input" value={settings.default_entry_visibility}
|
||||
<div className="form-field" style={{ marginTop: 4 }}>
|
||||
<label className="form-label">Default entry visibility</label>
|
||||
<select className="field-input" value={settings.default_entry_visibility}
|
||||
onChange={e => setSettings(s => ({ ...s, default_entry_visibility: Number(e.target.value) }))}>
|
||||
<option value={0}>Private (0)</option>
|
||||
<option value={2}>Unlisted (2)</option>
|
||||
<option value={3}>Public (3)</option>
|
||||
<option value={0}>Private</option>
|
||||
<option value={2}>Unlisted</option>
|
||||
<option value={3}>Public</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{saveMsg && <div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save Settings'}
|
||||
</button>
|
||||
</form>
|
||||
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save Settings'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,42 +32,53 @@ export default function SetupPage({ onComplete }) {
|
|||
|
||||
return (
|
||||
<div className="setup-page">
|
||||
<h1>Welcome to Archivr</h1>
|
||||
<p>Create your owner account to get started.</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<input
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={e => setConfirm(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating account\u2026' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="setup-card">
|
||||
<h1 className="setup-brand">Archivr</h1>
|
||||
<p className="setup-tagline">Create your owner account to get started.</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-username">Username</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-password">Password</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-confirm">Confirm password</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-confirm"
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={e => setConfirm(e.target.value)}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="setup-error">{error}</p>}
|
||||
<button className="setup-submit" type="submit" disabled={loading}>
|
||||
{loading ? 'Creating account\u2026' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,14 @@ export default function TagsView({ tagNodes, tagFilter, onTagFilterSet, onViewCh
|
|||
return (
|
||||
<section id="tags-view" className="view is-active">
|
||||
<div className="tag-tree">
|
||||
<div className="tag-tree-header">
|
||||
<span className="tag-tree-title">Tags</span>
|
||||
{tagFilter && (
|
||||
<span className="tag-tree-active">Filtering: {tagFilter}</span>
|
||||
)}
|
||||
</div>
|
||||
{tagNodes.length === 0 ? (
|
||||
<div>No tags yet.</div>
|
||||
<p className="muted" style={{ padding: '8px 0' }}>No tags yet.</p>
|
||||
) : (
|
||||
<ul className="tag-tree-list">
|
||||
{tagNodes.map(node => (
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV
|
|||
<button className="capture-button" onClick={onCaptureClick}>+ Capture</button>
|
||||
{currentUser && (
|
||||
<div className="user-menu">
|
||||
<span className="username">{currentUser.display_name || currentUser.username}</span>
|
||||
<button className="nav-link" onClick={() => onViewChange('settings')} style={{ color: '#d7cdbf', fontSize: 13 }}>Settings</button>
|
||||
<button onClick={handleLogout} disabled={loggingOut} className="logout-btn">
|
||||
<span className="user-name">{currentUser.display_name || currentUser.username}</span>
|
||||
<button className="logout-btn" onClick={handleLogout} disabled={loggingOut}>
|
||||
{loggingOut ? 'Logging out\u2026' : 'Log out'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue