refactor: split frontend application features
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
83d14d23a2
commit
5d7a57fd00
32 changed files with 2563 additions and 2433 deletions
47
frontend/src/components/actions/Account.tsx
Normal file
47
frontend/src/components/actions/Account.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { authStorage, invalidateSession, SESSION_EXPIRED_EVENT } from '../../api/client'
|
||||
|
||||
export function useAuthSession() {
|
||||
const [authenticated, setAuthenticated] = useState(() => Boolean(authStorage.token()))
|
||||
const [sessionExpired, setSessionExpired] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
function handleSessionExpired() {
|
||||
setAuthenticated(false)
|
||||
setSessionExpired(true)
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
window.addEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired)
|
||||
return () => window.removeEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticated) return
|
||||
const expiresAt = authStorage.expiresAt()
|
||||
if (expiresAt === null) return
|
||||
const remaining = expiresAt - Date.now()
|
||||
if (remaining <= 0) {
|
||||
invalidateSession()
|
||||
return
|
||||
}
|
||||
const timeout = window.setTimeout(invalidateSession, remaining)
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [authenticated])
|
||||
|
||||
function completeLogin() {
|
||||
setAuthenticated(true)
|
||||
setSessionExpired(false)
|
||||
navigate('/jobs', { replace: true })
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authStorage.clear()
|
||||
setAuthenticated(false)
|
||||
setSessionExpired(false)
|
||||
}
|
||||
|
||||
return { authenticated, sessionExpired, completeLogin, logout }
|
||||
}
|
||||
19
frontend/src/components/actions/ErrorAlert.tsx
Normal file
19
frontend/src/components/actions/ErrorAlert.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export function ErrorAlert({
|
||||
message,
|
||||
retry,
|
||||
}: {
|
||||
message: string
|
||||
retry?: () => void | Promise<void>
|
||||
}) {
|
||||
return (
|
||||
<div className="error-alert" role="alert">
|
||||
<strong>Request failed</strong>
|
||||
<span>{message}</span>
|
||||
{retry && (
|
||||
<button type="button" onClick={() => void retry()}>
|
||||
Try again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
8
frontend/src/components/actions/Loading.tsx
Normal file
8
frontend/src/components/actions/Loading.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function Loading() {
|
||||
return (
|
||||
<div className="empty-state" role="status">
|
||||
<span className="loading-spinner" />
|
||||
<p>Loading…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
frontend/src/components/actions/errorMessage.ts
Normal file
7
frontend/src/components/actions/errorMessage.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { ApiError } from '../../api/client'
|
||||
|
||||
export function errorMessage(error: unknown) {
|
||||
return error instanceof ApiError || error instanceof Error
|
||||
? error.message
|
||||
: 'Something went wrong'
|
||||
}
|
||||
4
frontend/src/components/actions/index.ts
Normal file
4
frontend/src/components/actions/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { useAuthSession } from './Account'
|
||||
export { ErrorAlert } from './ErrorAlert'
|
||||
export { errorMessage } from './errorMessage'
|
||||
export { Loading } from './Loading'
|
||||
Loading…
Add table
Add a link
Reference in a new issue