all: Aggiunte varie feature e riscritte svariate cose

This commit is contained in:
2022-12-01 03:45:29 +01:00
parent bd055f484d
commit 8fdef2981d
22 changed files with 714 additions and 422 deletions
@@ -1,18 +1,30 @@
import { route } from 'preact-router'
import { useState } from 'preact/hooks'
import { isStudent, Solution as SolutionModel, SolutionId } from '../../shared/model'
import { isAdministrator, isStudent, Solution as SolutionModel, SolutionId } from '../../shared/model'
import { sortByStringKey } from '../../shared/utils'
import { prependBaseUrl, server } from '../api'
import { Header } from '../components/Header'
import { MarkdownEditor } from '../components/MarkdownEditor'
import { Select } from '../components/Select'
import { Solution } from '../components/Solution'
import { useCurrentUser, useListResource } from '../hooks'
import { useListResource } from '../hooks'
import { useCurrentUser } from '../hooks/useCurrentUser'
const CreateProblem = ({}) => {
const [source, setSource] = useState('')
type ProblemFields = {
title?: string
content: string
}
const createProblem = async () => {
const id = await server.post('/api/problem', {
if (source.trim().length === 0) {
alert('Il campo di testo è vuoto!')
return
}
const id = await server.post<ProblemFields>('/api/problem', {
content: source,
})
@@ -21,7 +33,7 @@ const CreateProblem = ({}) => {
return (
<>
<MarkdownEditor {...{ source, setSource }} />
<MarkdownEditor placeholder="Scrivi un nuovo problema..." {...{ source, setSource }} />
<button onClick={createProblem}>Aggiungi Problema</button>
</>
)
@@ -30,30 +42,34 @@ const CreateProblem = ({}) => {
type SortOrder = 'latest' | 'oldest'
export const AdminPage = ({}) => {
const [user] = useCurrentUser(user => {
if (!user) {
route(prependBaseUrl('/login'), true)
} else if (isStudent(user.role)) {
route(prependBaseUrl('/'), true)
}
})
const [user, ready] = useCurrentUser()
if (!ready) {
return <></>
}
if (user === null) {
route(prependBaseUrl('/login'), true)
return <></>
}
if (!isAdministrator(user.role)) {
route(prependBaseUrl('/'), true)
return <></>
}
const [solutions, refreshSolutions, setSolutionHeuristic] = useListResource<SolutionModel>(`/api/solutions`)
const [sortOrder, setSortOrder] = useState<SortOrder>('oldest')
const sortedSolutions = sortByStringKey(solutions, s => s.createdAt, sortOrder === 'oldest')
const [trackInteracted, setTrackedInteracted] = useState<Set<SolutionId>>(new Set())
const hasUntrackedPending = sortedSolutions.filter(s => s.status === 'pending' || trackInteracted.has(s.id)).length > 0
return (
user && (
<>
<Header />
<main class="page-admin">
<Header {...{ user }} />
<div class="subtitle">Nuovo problema</div>
<CreateProblem />
<hr />
<div class="subtitle">Soluzioni da correggere</div>
{hasUntrackedPending ? (
<div class="solution-list">
@@ -90,6 +106,6 @@ export const AdminPage = ({}) => {
</>
)}
</main>
)
</>
)
}
@@ -1,12 +1,12 @@
import { route } from 'preact-router'
import { useContext, useEffect, useState } from 'preact/hooks'
import { useContext, useState } from 'preact/hooks'
import { Problem as ProblemModel } from '../../shared/model'
import { sortByNumericKey, sortByStringKey } from '../../shared/utils'
import { Header } from '../components/Header'
import { Problem } from '../components/Problem'
import { Select } from '../components/Select'
import { useResource, useCurrentUser, MetadataContext } from '../hooks'
import { useResource, MetadataContext } from '../hooks'
import { useCurrentUser } from '../hooks/useCurrentUser'
function byTime(p: ProblemModel): string {
return p.createdAt
@@ -30,7 +30,6 @@ export const HomePage = () => {
metadata.title = `PHC Problemi`
metadata.description = 'Bacheca di problemi del PHC'
const [user] = useCurrentUser()
const [problems] = useResource<(ProblemModel & { solutionsCount: number })[]>('/api/problems', [])
const [sortOrder, setSortOrder] = useState<SortOrder>('oldest')
@@ -41,28 +40,30 @@ export const HomePage = () => {
: sortByNumericKey(problems, bySolvedProblems, SORT_ORDER[sortOrder])
return (
<main class="page-home">
<Header {...{ user }} />
<div class="board">
<div class="fill-row board-controls">
<div class="sort-order">
<Select
value={sortOrder}
setValue={setSortOrder}
options={{
'latest': 'Prima più recenti',
'oldest': 'Prima più antichi',
'top-solved': 'Prima più risolti',
'least-solved': 'Prima meno risolti',
}}
/>
<>
<Header />
<main class="page-home">
<div class="board">
<div class="fill-row board-controls">
<div class="sort-order">
<Select
value={sortOrder}
setValue={setSortOrder}
options={{
'latest': 'Prima più recenti',
'oldest': 'Prima più antichi',
'top-solved': 'Prima più risolti',
'least-solved': 'Prima meno risolti',
}}
/>
</div>
</div>
</div>
{sortedProblems.map(p => (
<Problem {...p} />
))}
</div>
</main>
{sortedProblems.map(p => (
<Problem {...p} />
))}
</div>
</main>
</>
)
}
-45
View File
@@ -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>
)
}
+39
View File
@@ -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>
</>
)
}
-76
View File
@@ -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>
)
}
+165
View File
@@ -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>
</>
)
}
-45
View File
@@ -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>
)
)
}
+45
View File
@@ -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>
</>
)
}