mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
Compare commits
3 commits
2414acf0df
...
c85cce579b
| Author | SHA1 | Date | |
|---|---|---|---|
| c85cce579b | |||
| 44f67390be | |||
| c4f1c7a137 |
6 changed files with 87 additions and 50 deletions
|
|
@ -90,8 +90,8 @@ fn save_with(
|
||||||
.arg(&out_file)
|
.arg(&out_file)
|
||||||
.arg(format!("--browser-executable-path={chrome}"))
|
.arg(format!("--browser-executable-path={chrome}"))
|
||||||
.arg("--browser-headless")
|
.arg("--browser-headless")
|
||||||
.arg("--browser-wait-until=networkAlmostIdle")
|
.arg("--browser-wait-until=networkidle2")
|
||||||
// Extra delay after networkAlmostIdle: Cloudflare Fonts injects @font-face
|
// Extra delay after networkidle2: Cloudflare Fonts injects @font-face
|
||||||
// CSS after HTML parse, so the font hook needs more time to see it.
|
// CSS after HTML parse, so the font hook needs more time to see it.
|
||||||
.arg("--browser-wait-delay=2000")
|
.arg("--browser-wait-delay=2000")
|
||||||
// Realistic UA: some origins block headless Chrome's default UA string.
|
// Realistic UA: some origins block headless Chrome's default UA string.
|
||||||
|
|
|
||||||
40
crates/archivr-server/static/assets/index-BEbGvi2K.js
Normal file
40
crates/archivr-server/static/assets/index-BEbGvi2K.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CbWD4KAy.js"></script>
|
<script type="module" crossorigin src="/assets/index-BEbGvi2K.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-MrdP6h9x.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-MrdP6h9x.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,22 @@ import ContextRail from './components/ContextRail'
|
||||||
|
|
||||||
export const AuthContext = createContext(null);
|
export const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
const VIEWS = ['archive','runs','admin','tags','collections','settings']
|
||||||
|
const SETTINGS_TABS = ['profile','tokens','instance']
|
||||||
|
|
||||||
|
function parseLocation() {
|
||||||
|
const parts = window.location.pathname.split('/').filter(Boolean)
|
||||||
|
const view = VIEWS.includes(parts[0]) ? parts[0] : 'archive'
|
||||||
|
const settingsTab = (view === 'settings' && SETTINGS_TABS.includes(parts[1])) ? parts[1] : 'profile'
|
||||||
|
return { view, settingsTab }
|
||||||
|
}
|
||||||
|
|
||||||
|
function locationPath(view, settingsTab) {
|
||||||
|
if (view === 'archive') return '/'
|
||||||
|
if (view === 'settings') return `/settings/${settingsTab}`
|
||||||
|
return `/${view}`
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [authState, setAuthState] = useState('loading');
|
const [authState, setAuthState] = useState('loading');
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
|
@ -36,13 +52,25 @@ export default function App() {
|
||||||
return () => window.removeEventListener('auth:expired', handler);
|
return () => window.removeEventListener('auth:expired', handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Sync URL → state on back/forward
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
const { view, settingsTab } = parseLocation()
|
||||||
|
setView(view)
|
||||||
|
setSettingsTab(settingsTab)
|
||||||
|
}
|
||||||
|
window.addEventListener('popstate', handler)
|
||||||
|
return () => window.removeEventListener('popstate', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [archives, setArchives] = useState([])
|
const [archives, setArchives] = useState([])
|
||||||
const [archiveId, setArchiveId] = useState(null)
|
const [archiveId, setArchiveId] = useState(null)
|
||||||
const [entries, setEntries] = useState([])
|
const [entries, setEntries] = useState([])
|
||||||
const [selectedEntryUid, setSelectedEntryUid] = useState(null)
|
const [selectedEntryUid, setSelectedEntryUid] = useState(null)
|
||||||
const [selectedEntry, setSelectedEntry] = useState(null)
|
const [selectedEntry, setSelectedEntry] = useState(null)
|
||||||
const [tagFilter, setTagFilter] = useState(null)
|
const [tagFilter, setTagFilter] = useState(null)
|
||||||
const [view, setView] = useState('archive')
|
const [view, setView] = useState(() => parseLocation().view)
|
||||||
|
const [settingsTab, setSettingsTab] = useState(() => parseLocation().settingsTab)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [resultCount, setResultCount] = useState('')
|
const [resultCount, setResultCount] = useState('')
|
||||||
const [searchBusy, setSearchBusy] = useState(false)
|
const [searchBusy, setSearchBusy] = useState(false)
|
||||||
|
|
@ -105,10 +133,12 @@ export default function App() {
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [searchQuery, archiveId]) // tagFilter handled separately below
|
}, [searchQuery, archiveId]) // tagFilter handled separately below
|
||||||
|
|
||||||
// Tag filter change: switch to archive view, reload
|
// Tag filter applied: switch to archive view and reload.
|
||||||
|
// Only reset view when tagFilter is non-null; archive change alone (tagFilter=null)
|
||||||
|
// must not stomp the URL-initialised view on first load.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (archiveId === null) return
|
if (archiveId === null) return
|
||||||
setView('archive')
|
if (tagFilter !== null) setView('archive')
|
||||||
loadEntries(archiveId, searchQuery, tagFilter)
|
loadEntries(archiveId, searchQuery, tagFilter)
|
||||||
}, [tagFilter, archiveId]) // intentional: searchQuery excluded to avoid double-fire
|
}, [tagFilter, archiveId]) // intentional: searchQuery excluded to avoid double-fire
|
||||||
|
|
||||||
|
|
@ -123,6 +153,14 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, [archiveId])
|
}, [archiveId])
|
||||||
|
|
||||||
|
// Sync view + settingsTab → URL
|
||||||
|
useEffect(() => {
|
||||||
|
const path = locationPath(view, settingsTab)
|
||||||
|
if (window.location.pathname !== path) {
|
||||||
|
history.pushState(null, '', path)
|
||||||
|
}
|
||||||
|
}, [view, settingsTab])
|
||||||
|
|
||||||
const selectEntry = useCallback((entry) => {
|
const selectEntry = useCallback((entry) => {
|
||||||
setSelectedEntryUid(entry ? entry.entry_uid : null)
|
setSelectedEntryUid(entry ? entry.entry_uid : null)
|
||||||
setSelectedEntry(entry)
|
setSelectedEntry(entry)
|
||||||
|
|
@ -230,7 +268,7 @@ export default function App() {
|
||||||
<CollectionsView archiveId={archiveId} />
|
<CollectionsView archiveId={archiveId} />
|
||||||
)}
|
)}
|
||||||
{view === 'settings' && (
|
{view === 'settings' && (
|
||||||
<SettingsView />
|
<SettingsView tab={settingsTab} onTabChange={setSettingsTab} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ContextRail
|
<ContextRail
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@ import {
|
||||||
|
|
||||||
const ROLE_ADMIN = 4
|
const ROLE_ADMIN = 4
|
||||||
|
|
||||||
export default function SettingsView() {
|
export default function SettingsView({ tab, onTabChange }) {
|
||||||
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
||||||
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
||||||
const [tab, setTab] = useState('profile')
|
|
||||||
|
|
||||||
const tabs = ['profile', 'tokens', ...(isAdmin ? ['instance'] : [])]
|
const tabs = ['profile', 'tokens', ...(isAdmin ? ['instance'] : [])]
|
||||||
const tabLabels = { profile: 'Profile', tokens: 'API Tokens', instance: 'Instance' }
|
const tabLabels = { profile: 'Profile', tokens: 'API Tokens', instance: 'Instance' }
|
||||||
|
|
@ -23,7 +22,7 @@ export default function SettingsView() {
|
||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<button key={t}
|
<button key={t}
|
||||||
className={`view-tab${tab === t ? ' is-active' : ''}`}
|
className={`view-tab${tab === t ? ' is-active' : ''}`}
|
||||||
onClick={() => setTab(t)}>
|
onClick={() => onTabChange(t)}>
|
||||||
{tabLabels[t]}
|
{tabLabels[t]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue