all: Aggiunte varie feature e riscritte svariate cose
parent
bd055f484d
commit
8fdef2981d
@ -0,0 +1,78 @@
|
|||||||
|
import { ComponentChildren, createContext, JSX } from 'preact'
|
||||||
|
import { StateUpdater, useContext, useEffect, useState } from 'preact/hooks'
|
||||||
|
import { User } from '../../shared/model'
|
||||||
|
import { prependBaseUrl, server } from '../api'
|
||||||
|
|
||||||
|
type UserContextValue = {
|
||||||
|
ready: boolean
|
||||||
|
user: User | null
|
||||||
|
setUser: StateUpdater<User | null>
|
||||||
|
refreshUser: () => Promise<User | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContext = createContext<UserContextValue | null>(null)
|
||||||
|
|
||||||
|
type CurrentUserHook = () => [User | null, boolean]
|
||||||
|
|
||||||
|
export const useCurrentUser: CurrentUserHook = () => {
|
||||||
|
const userContext = useContext(UserContext)
|
||||||
|
if (!userContext) {
|
||||||
|
return [null, false]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ready, user } = userContext
|
||||||
|
return [user, ready]
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserFunctionsHook = () => {
|
||||||
|
login: (username: string) => Promise<void>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserFunctions: UserFunctionsHook = () => {
|
||||||
|
const login = async (username: string) => {
|
||||||
|
await fetch(prependBaseUrl(`/api/login`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: username,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await refreshUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
const userContext = useContext(UserContext)
|
||||||
|
if (!userContext) {
|
||||||
|
return { login, logout: async () => {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { setUser, refreshUser } = userContext
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await server.post('/api/logout')
|
||||||
|
setUser(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { login, logout }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProvider = ({ children }: { children: ComponentChildren }): JSX.Element => {
|
||||||
|
const [ready, setReady] = useState<boolean>(false)
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
|
||||||
|
const refreshUser = async () => {
|
||||||
|
const user = await server.get('/api/current-user')
|
||||||
|
setUser(user)
|
||||||
|
setReady(true)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user when first mounted
|
||||||
|
useEffect(() => {
|
||||||
|
refreshUser()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <UserContext.Provider value={{ ready, user, setUser, refreshUser }}>{children}</UserContext.Provider>
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
import { route } from 'preact-router'
|
|
||||||
|
|
||||||
import { useState } from 'preact/hooks'
|
|
||||||
import { prependBaseUrl } from '../api.js'
|
|
||||||
import { Header } from '../components/Header.jsx'
|
|
||||||
|
|
||||||
export const LoginPage = () => {
|
|
||||||
const [username, setUsername] = useState('')
|
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
// @ts-ignore
|
|
||||||
await fetch(prependBaseUrl(`/api/login`), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: username,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
route(prependBaseUrl('/'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main class="page-login">
|
|
||||||
<Header noLogin />
|
|
||||||
<div class="subtitle">Accedi</div>
|
|
||||||
<div class="form">
|
|
||||||
<label for="login-username">Username</label>
|
|
||||||
<input
|
|
||||||
id="login-username"
|
|
||||||
type="text"
|
|
||||||
value={username}
|
|
||||||
onInput={e => setUsername(e.target instanceof HTMLInputElement ? e.target.value : '')}
|
|
||||||
onKeyDown={e => e.key === 'Enter' && login()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="fill">
|
|
||||||
<button onClick={login}>Accedi</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
import { route } from 'preact-router'
|
||||||
|
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { prependBaseUrl } from '../api.js'
|
||||||
|
import { Header } from '../components/Header.jsx'
|
||||||
|
import { useUserFunctions } from '../hooks/useCurrentUser.js'
|
||||||
|
|
||||||
|
export const LoginPage = () => {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const { login } = useUserFunctions()
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
await login(username)
|
||||||
|
route(prependBaseUrl('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header noLogin />
|
||||||
|
<main class="page-login">
|
||||||
|
<div class="subtitle">Accedi</div>
|
||||||
|
<div class="form">
|
||||||
|
<label for="login-username">Username</label>
|
||||||
|
<input
|
||||||
|
id="login-username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onInput={e => setUsername(e.target instanceof HTMLInputElement ? e.target.value : '')}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleLogin()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="fill">
|
||||||
|
<button onClick={handleLogin}>Accedi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
import { route } from 'preact-router'
|
|
||||||
import { useContext, useState } from 'preact/hooks'
|
|
||||||
import { isAdministrator, Problem as ProblemModel, Solution as SolutionModel } from '../../shared/model'
|
|
||||||
import { prependBaseUrl, server } from '../api'
|
|
||||||
import { Header } from '../components/Header'
|
|
||||||
import { MarkdownEditor } from '../components/MarkdownEditor'
|
|
||||||
import { Problem } from '../components/Problem'
|
|
||||||
import { Solution } from '../components/Solution'
|
|
||||||
import { MetadataContext, useCurrentUser, useListResource, useResource } from '../hooks'
|
|
||||||
|
|
||||||
type RouteProps = {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProblemPage = ({ id }: RouteProps) => {
|
|
||||||
const metadata = useContext(MetadataContext)
|
|
||||||
metadata.title = `Problem ${id}`
|
|
||||||
metadata.description = 'Bacheca di problemi del PHC'
|
|
||||||
|
|
||||||
const [user] = useCurrentUser()
|
|
||||||
|
|
||||||
const [source, setSource] = useState('')
|
|
||||||
|
|
||||||
const [problem] = useResource<ProblemModel | null>(`/api/problem/${id}`, null, problem => {
|
|
||||||
if (problem === null) {
|
|
||||||
route(prependBaseUrl('/'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const content = problem?.content ?? ''
|
|
||||||
|
|
||||||
const [solutions, refreshSolutions, setSolutionHeuristic] = useListResource<SolutionModel>(
|
|
||||||
`/api/solutions?problem=${id}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const sendSolution = async () => {
|
|
||||||
await server.post('/api/solution', {
|
|
||||||
forProblem: id,
|
|
||||||
content: source,
|
|
||||||
})
|
|
||||||
|
|
||||||
setSource('')
|
|
||||||
|
|
||||||
refreshSolutions()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main class="page-problem">
|
|
||||||
<Header {...{ user }} />
|
|
||||||
<div class="subtitle">Testo del problema</div>
|
|
||||||
<Problem id={id} content={content} />
|
|
||||||
{solutions.length > 0 && (
|
|
||||||
<details>
|
|
||||||
<summary>Soluzioni</summary>
|
|
||||||
<div class="solution-list">
|
|
||||||
{solutions.map((s, index) => (
|
|
||||||
<Solution
|
|
||||||
{...s}
|
|
||||||
adminControls={user !== null && isAdministrator(user.role)}
|
|
||||||
refreshSolution={refreshSolutions}
|
|
||||||
setSolution={solFn => setSolutionHeuristic(index, solFn)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
{user && (
|
|
||||||
<>
|
|
||||||
<div class="subtitle">Invia una soluzione al problema</div>
|
|
||||||
<MarkdownEditor {...{ source, setSource }} />
|
|
||||||
<button onClick={sendSolution}>Invia Soluzione</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,165 @@
|
|||||||
|
import { route } from 'preact-router'
|
||||||
|
import { useContext, useEffect, useState } from 'preact/hooks'
|
||||||
|
import { isAdministrator, Problem as ProblemModel, Solution as SolutionModel } from '../../shared/model'
|
||||||
|
import { prependBaseUrl, server } from '../api'
|
||||||
|
import { Header } from '../components/Header'
|
||||||
|
import { MarkdownEditor } from '../components/MarkdownEditor'
|
||||||
|
import { Problem } from '../components/Problem'
|
||||||
|
import { Solution } from '../components/Solution'
|
||||||
|
import { MetadataContext, useListResource, useResource } from '../hooks'
|
||||||
|
import { useCurrentUser } from '../hooks/useCurrentUser'
|
||||||
|
|
||||||
|
type RouteProps = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProblemPage = ({ id }: RouteProps) => {
|
||||||
|
const metadata = useContext(MetadataContext)
|
||||||
|
metadata.title = `Problem ${id}`
|
||||||
|
metadata.description = 'Bacheca di problemi del PHC'
|
||||||
|
|
||||||
|
const [user] = useCurrentUser()
|
||||||
|
|
||||||
|
const [source, setSource] = useState('')
|
||||||
|
|
||||||
|
const [problem, refreshProblem] = useResource<ProblemModel | null>(`/api/problem/${id}`, null, problem => {
|
||||||
|
if (problem === null) {
|
||||||
|
route(prependBaseUrl('/'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [solutions, refreshSolutions, setSolutionHeuristic] = useListResource<SolutionModel>(`/api/solutions?problem=${id}`)
|
||||||
|
const userSolutions: [number, SolutionModel][] = solutions.flatMap((s, index) => (user && s.sentBy === user.id ? [[index, s]] : []))
|
||||||
|
const otherSolutions: [number, SolutionModel][] = solutions.flatMap((s, index) => (user && s.sentBy !== user.id ? [[index, s]] : []))
|
||||||
|
|
||||||
|
const notLoggedSolutions: [number, SolutionModel][] = user ? [] : solutions.map((s, index) => [index, s])
|
||||||
|
|
||||||
|
const sendSolution = async () => {
|
||||||
|
if (source.trim().length === 0) {
|
||||||
|
alert('La soluzione che hai inserito è vuota!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.post('/api/solution', {
|
||||||
|
forProblem: id,
|
||||||
|
content: source,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSource('')
|
||||||
|
|
||||||
|
refreshSolutions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [editing, setEditing] = useState<boolean>(false)
|
||||||
|
const [modifiedProblemSource, setModifiedProblemSource] = useState<string>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (problem) {
|
||||||
|
setModifiedProblemSource(problem.content)
|
||||||
|
}
|
||||||
|
}, [problem?.content])
|
||||||
|
|
||||||
|
const updateProblem = async () => {
|
||||||
|
await server.patch(`/api/problem/${id}`, {
|
||||||
|
content: modifiedProblemSource,
|
||||||
|
})
|
||||||
|
refreshProblem()
|
||||||
|
setEditing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteProblem = async () => {
|
||||||
|
if (confirm('Sei veramente sicuro di voler eliminare questo problema?')) {
|
||||||
|
await server.delete(`/api/problem/${id}`)
|
||||||
|
route(prependBaseUrl('/'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<main class="page-problem">
|
||||||
|
<div class="subtitle">Testo del problema</div>
|
||||||
|
{!editing ? (
|
||||||
|
<>
|
||||||
|
{problem && <Problem {...problem} />}
|
||||||
|
{user && isAdministrator(user.role) && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setEditing(true)}>Modifica problema</button>
|
||||||
|
<button onClick={() => deleteProblem()}>Elimina problema</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MarkdownEditor
|
||||||
|
placeholder="Modifica testo del problema..."
|
||||||
|
source={modifiedProblemSource}
|
||||||
|
setSource={setModifiedProblemSource}
|
||||||
|
/>
|
||||||
|
<div class="col">
|
||||||
|
<button onClick={() => updateProblem()}>Aggiorna problema</button>
|
||||||
|
<button onClick={() => setEditing(false)}>Annulla</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{userSolutions.length > 0 && (
|
||||||
|
<details open>
|
||||||
|
<summary>Le tue soluzioni</summary>
|
||||||
|
<div class="solution-list">
|
||||||
|
{userSolutions.map(([index, s]) => (
|
||||||
|
<Solution
|
||||||
|
{...s}
|
||||||
|
adminControls={user !== null && isAdministrator(user.role)}
|
||||||
|
refreshSolution={refreshSolutions}
|
||||||
|
setSolution={solFn => setSolutionHeuristic(index, solFn)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{otherSolutions.length > 0 && (
|
||||||
|
<details>
|
||||||
|
<summary>Altre soluzioni</summary>
|
||||||
|
<div class="solution-list">
|
||||||
|
{otherSolutions.map(([index, s]) => (
|
||||||
|
<Solution
|
||||||
|
{...s}
|
||||||
|
adminControls={user !== null && isAdministrator(user.role)}
|
||||||
|
refreshSolution={refreshSolutions}
|
||||||
|
setSolution={solFn => setSolutionHeuristic(index, solFn)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{notLoggedSolutions.length > 0 && (
|
||||||
|
<details>
|
||||||
|
<summary>Soluzioni</summary>
|
||||||
|
<div class="solution-list">
|
||||||
|
{notLoggedSolutions.map(([index, s]) => (
|
||||||
|
<Solution
|
||||||
|
{...s}
|
||||||
|
adminControls={user !== null && isAdministrator(user.role)}
|
||||||
|
refreshSolution={refreshSolutions}
|
||||||
|
setSolution={solFn => setSolutionHeuristic(index, solFn)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div class="subtitle">Invia una soluzione al problema</div>
|
||||||
|
<MarkdownEditor placeholder="Scrivi una soluzione al problema..." {...{ source, setSource }} />
|
||||||
|
<button onClick={sendSolution}>Invia Soluzione</button>
|
||||||
|
<p class="icon-text">
|
||||||
|
<span class="material-symbols-outlined">warning</span>
|
||||||
|
Attenzione, una soluzione inviata non può essere modificata!
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
import { route } from 'preact-router'
|
|
||||||
import { useState } from 'preact/hooks'
|
|
||||||
import { isAdministrator, Solution as SolutionModel, User } from '../../shared/model'
|
|
||||||
import { prependBaseUrl, server } from '../api'
|
|
||||||
import { Header } from '../components/Header'
|
|
||||||
import { Select } from '../components/Select'
|
|
||||||
import { Solution } from '../components/Solution'
|
|
||||||
import { useCurrentUser, useResource } from '../hooks'
|
|
||||||
|
|
||||||
const SolutionList = ({ user }: { user: User }) => {
|
|
||||||
const [solutions, refresh] = useResource<SolutionModel[]>(`/api/solutions?user=${user.id}`, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="solution-list">
|
|
||||||
{solutions.map(solution => (
|
|
||||||
<Solution {...solution} adminControls={isAdministrator(user.role)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProfilePage = ({}) => {
|
|
||||||
const [user, logout] = useCurrentUser(user => {
|
|
||||||
if (!user) {
|
|
||||||
route(prependBaseUrl('/login'), true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await logout()
|
|
||||||
route(prependBaseUrl('/'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
user && (
|
|
||||||
<main class="page-profile">
|
|
||||||
<Header {...{ user }} />
|
|
||||||
<div class="subtitle">Profilo</div>
|
|
||||||
<button onClick={handleLogout}>Logout</button>
|
|
||||||
<div class="subtitle">Le tue soluzioni</div>
|
|
||||||
<SolutionList {...{ user }} />
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
import { route } from 'preact-router'
|
||||||
|
import { isAdministrator, Solution as SolutionModel, User } from '../../shared/model'
|
||||||
|
import { sortByStringKey } from '../../shared/utils'
|
||||||
|
import { prependBaseUrl } from '../api'
|
||||||
|
import { Header } from '../components/Header'
|
||||||
|
import { Solution } from '../components/Solution'
|
||||||
|
import { useResource } from '../hooks'
|
||||||
|
import { useCurrentUser, useUserFunctions } from '../hooks/useCurrentUser'
|
||||||
|
|
||||||
|
export const ProfilePage = ({}) => {
|
||||||
|
const [user, ready] = useCurrentUser()
|
||||||
|
if (!ready) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
route(prependBaseUrl('/login'), true)
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { logout } = useUserFunctions()!
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout()
|
||||||
|
route(prependBaseUrl('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [solutions, refreshSolutions] = useResource<SolutionModel[]>(`/api/solutions?user=${user.id}`, [])
|
||||||
|
const sortedSolutions = sortByStringKey(solutions, s => s.createdAt, false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<main class="page-profile">
|
||||||
|
<div class="subtitle">Profilo</div>
|
||||||
|
<button onClick={handleLogout}>Logout</button>
|
||||||
|
<div class="subtitle">Le tue soluzioni</div>
|
||||||
|
<div class="solution-list">
|
||||||
|
{sortedSolutions.map(solution => (
|
||||||
|
<Solution refreshSolution={refreshSolutions} {...solution} adminControls={isAdministrator(user.role)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue