From 14b9eee816738de8a9470b67921d27b2c8ce4990 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Wed, 11 Jan 2023 12:08:40 +0100 Subject: [PATCH] feat: aggiunte feature per l'admin e cambiamenti stilistici minori --- client/components/Header.tsx | 12 +- client/pages/AdminPage.tsx | 59 ++++++--- client/pages/ProfilePage.tsx | 7 +- client/pages/ScoresPage.tsx | 4 +- client/pages/UserPage.tsx | 79 ++++++------ client/styles/main.scss | 228 +++++++++++++++++++++-------------- server/routes.ts | 22 +++- 7 files changed, 255 insertions(+), 156 deletions(-) diff --git a/client/components/Header.tsx b/client/components/Header.tsx index 5f6cf34..1ee05f7 100644 --- a/client/components/Header.tsx +++ b/client/components/Header.tsx @@ -1,13 +1,13 @@ import { Link } from 'preact-router/match' -import { isAdministrator, UserRole } from '../../shared/model' +import { isAdministrator } from '../../shared/model' import { prependBaseUrl } from '../api' import { useCurrentUser } from '../hooks/useCurrentUser' -const ROLE_LABEL: Record = { - ['admin']: 'Admin', - ['moderator']: 'Moderatore', - ['student']: 'Studente', -} +// const ROLE_LABEL: Record = { +// ['admin']: 'Admin', +// ['moderator']: 'Moderatore', +// ['student']: 'Studente', +// } export const Header = ({}) => { const [user] = useCurrentUser() diff --git a/client/pages/AdminPage.tsx b/client/pages/AdminPage.tsx index b0179a7..70441c1 100644 --- a/client/pages/AdminPage.tsx +++ b/client/pages/AdminPage.tsx @@ -1,6 +1,6 @@ import { route } from 'preact-router' import { useState } from 'preact/hooks' -import { isAdministrator, Solution as SolutionModel, SolutionId } from '../../shared/model' +import { isAdministrator, Solution as SolutionModel, SolutionId, User as UserModel } from '../../shared/model' import { sortByStringKey } from '../../shared/utils' import { prependBaseUrl, server } from '../api' import { Header } from '../components/Header' @@ -63,6 +63,18 @@ export const AdminPage = ({}) => { const [trackInteracted, setTrackedInteracted] = useState>(new Set()) const hasUntrackedPending = sortedSolutions.filter(s => s.status === 'pending' || trackInteracted.has(s.id)).length > 0 + const [users] = useListResource(`/api/users`) + const [adminSuUsername, setAdminSuUsername] = useState('') + + const handleAdminSu = async () => { + await server.post(`/api/admin/su`, { + username: adminSuUsername, + }) + + route(prependBaseUrl('/u/' + adminSuUsername)) + location.reload() + } + return ( <>
@@ -108,24 +120,37 @@ export const AdminPage = ({}) => { {user.role === 'admin' && ( <>
-
Utenti
-
-
Nome Utente
+
Controlli speciali solo per Admin
+
Username Ateneo
+
Nome Utente
Ruolo
-
Ultimo Login
-
User 1
-
user1
-
admin
-
2/6/2075 20:32
-
User 2
-
user2
-
moderator
-
5/12/2102 20:61
-
User 3
-
user3
-
studente
-
11/5/2066 20:49
+
Azioni
+ + {users.map(user => ( + <> +
{user.id}
+
{user.fullName}
+
{user.role}
+
+ + +
+ + ))} +
+
+
su $USER
+

Gli admin per fare prove possono immedesimarsi come un altro utente

+ + setAdminSuUsername(e.target instanceof HTMLInputElement ? e.target.value : '')} + /> +
+ +
)} diff --git a/client/pages/ProfilePage.tsx b/client/pages/ProfilePage.tsx index 0f5ed7e..351ee63 100644 --- a/client/pages/ProfilePage.tsx +++ b/client/pages/ProfilePage.tsx @@ -31,14 +31,17 @@ export const ProfilePage = ({}) => { <>
-
Profilo
-
Le tue soluzioni
{sortedSolutions.map(solution => ( ))}
+
Profilo
+ + Vai alla tua pagina utente + +
) diff --git a/client/pages/ScoresPage.tsx b/client/pages/ScoresPage.tsx index 72e6dc8..f3ffb81 100644 --- a/client/pages/ScoresPage.tsx +++ b/client/pages/ScoresPage.tsx @@ -35,7 +35,7 @@ export const ScoresPage = () => { let orderedStats if (sortStateColumn === 'student') { - orderedStats = sortByStringKey(Object.entries(stats), ([user, s]) => user, sortStateOrder === 'ascending') + orderedStats = sortByStringKey(Object.entries(stats), ([user, s]) => user.toLowerCase(), sortStateOrder === 'ascending') } else { orderedStats = sortByNumericKey( Object.entries(stats), @@ -94,7 +94,7 @@ export const ScoresPage = () => { @{user}
{s.sentSolutionsCount}
-
{s.correctSolutionsCount}
+
{s.correctSolutionsCount}
))} diff --git a/client/pages/UserPage.tsx b/client/pages/UserPage.tsx index c0a96cb..d489a80 100644 --- a/client/pages/UserPage.tsx +++ b/client/pages/UserPage.tsx @@ -11,45 +11,48 @@ type RouteProps = { } export const UserPage = ({ uid }: RouteProps) => { - const [user] = useCurrentUser() - - const [stats] = useResource>(`/api/stats`, null) - if (stats) { - const userStats = stats[uid] - - const [solutions, refreshSolutions] = useResource(`/api/solutions?user=${uid}&public`, []) - const sortedSolutions = sortByStringKey(solutions, s => s.createdAt, false) + const [user, ready] = useCurrentUser() + if (!ready) { + return <> + } - return ( - <> -
-
-
- Profilo di @{uid} -
-
-
Statistiche
-
-
Soluzioni inviate
-
Soluzioni corrette
-
{userStats.sentSolutionsCount}
-
{userStats.correctSolutionsCount}
-
-
-
Soluzioni notevoli
-
- {sortedSolutions.map(solution => ( - - ))} -
-
- - ) - } else { + const [stats] = useResource>(`/api/stats`, null) + if (!stats) { return <> } + + const userStats = stats[uid] + + const [solutions, refreshSolutions] = useResource(`/api/solutions?user=${uid}&public`, []) + const sortedSolutions = sortByStringKey(solutions, s => s.createdAt, false) + + return ( + <> +
+
+
+ Profilo di {user?.fullName} +
+
+
Statistiche
+
+
Soluzioni inviate
+
Soluzioni corrette
+
{userStats?.sentSolutionsCount ?? 0}
+
{userStats?.correctSolutionsCount ?? 0}
+
+
+
Soluzioni notevoli
+
+ {sortedSolutions.map(solution => ( + + ))} +
+
+ + ) } diff --git a/client/styles/main.scss b/client/styles/main.scss index 6d9fcb2..bd60514 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -119,7 +119,15 @@ input[type='text'] { } } -button, .button { +button, +.button { + // reset link styles + text-decoration: none; + display: grid; + place-content: center; + + height: 38px; + cursor: pointer; font-family: 'Lato'; @@ -128,7 +136,7 @@ button, .button { color: #555; border: 1px solid #c8c8c8; - padding: 0.5rem 2rem; + padding: 0.5rem 1.5rem; box-shadow: -2px 2px 16px 0 #00000010; border-radius: 0.25rem; @@ -170,11 +178,15 @@ button, .button { font-size: 19px; } } + + .table .cell & { + padding: 0.3rem 1rem; + } } .link, -a, -a:visited { +a:not(.button), +a:not(.button):visited { cursor: pointer; color: var(--accent); @@ -189,6 +201,11 @@ a:visited { // Typography // +.mono { + font-family: monospace; + font-size: 90%; +} + .icon-text { display: flex; align-items: center; @@ -376,16 +393,7 @@ main.page-scores { padding: 1rem; .table { - display: grid; - - grid-template-columns: auto auto auto; - - box-shadow: -2px 4px 6px 1px #00000018, 0 0 4px 0px #00000010; - // border: 1px solid #ddd; - border-radius: 0.5rem; - background: #ffffff; - - user-select: none; + grid-template-columns: repeat(3, auto); .icon { cursor: pointer; @@ -393,100 +401,119 @@ main.page-scores { place-content: center; } - position: relative; - .cell { - padding: 1rem 2rem; - - gap: 0.5rem; - - border-left: 1px solid #ddd; - border-top: 1px solid #ddd; - - &:nth-child(3n + 3) { - border-right: 1px solid #ddd; - } - - &:nth-last-child(1), - &:nth-last-child(2), - &:nth-last-child(3) { - border-bottom: 1px solid #ddd; - } - - &:first-child { - border-top-left-radius: 0.5rem; - } - - &:nth-child(3) { - border-top-right-radius: 0.5rem; - } - - &:nth-last-child(3) { - border-bottom-left-radius: 0.5rem; - } - - &:last-child { - border-bottom-right-radius: 0.5rem; - } + padding: 1rem 2rem 1rem 1rem; &.header { font-weight: 400; font-size: 22px; - padding-right: 1rem; + padding-right: 0.5rem; background: #f0f0f0; display: flex; + gap: 1rem; + align-items: center; text-align: center; justify-content: space-between; } + } + } - &:not(.header) { - user-select: text; + // .table { + // display: grid; - &:nth-child(3n + 1) { - text-align: left; - } + // grid-template-columns: auto auto auto; - &:nth-child(3n + 2), - &:nth-child(3n + 3) { - text-align: center; - } + // box-shadow: -2px 4px 6px 1px #00000018, 0 0 4px 0px #00000010; + // // border: 1px solid #ddd; + // border-radius: 0.5rem; + // background: #ffffff; - &:nth-child(3n + 3) { - &::after { - content: ''; - position: absolute; + // user-select: none; - left: 0; - right: 0; - height: 3rem; + // position: relative; - transform: translate(0, -1rem); + // .cell { + // padding: 1rem 2rem; - pointer-events: none; - background: transparent; - } + // gap: 0.5rem; - &:hover::after { - background: #00000006; - } - } + // border-left: 1px solid #ddd; + // border-top: 1px solid #ddd; - &:hover { - &:nth-child(3n + 1) + .cell + .cell::after { - background: #00000006; - } + // &:nth-child(3n + 3) { + // border-right: 1px solid #ddd; + // } - &:nth-child(3n + 2) + .cell::after { - background: #00000006; - } - } - } - } - } + // &:nth-last-child(1), + // &:nth-last-child(2), + // &:nth-last-child(3) { + // border-bottom: 1px solid #ddd; + // } + + // &:first-child { + // border-top-left-radius: 0.5rem; + // } + + // &:nth-child(3) { + // border-top-right-radius: 0.5rem; + // } + + // &:nth-last-child(3) { + // border-bottom-left-radius: 0.5rem; + // } + + // &:last-child { + // border-bottom-right-radius: 0.5rem; + // } + + // &:not(.header) { + // user-select: text; + + // &:nth-child(3n + 1) { + // text-align: left; + // } + + // &:nth-child(3n + 2), + // &:nth-child(3n + 3) { + // text-align: center; + // } + + // &:nth-child(3n + 3) { + // &::after { + // content: ''; + // position: absolute; + + // left: 0; + // right: 0; + // height: 3rem; + + // transform: translate(0, -1rem); + + // pointer-events: none; + // background: transparent; + // } + + // &:hover::after { + // background: #00000006; + // } + // } + + // &:hover { + // &:nth-child(3n + 1) + .cell + .cell::after { + // background: #00000006; + // } + + // &:nth-child(3n + 2) + .cell::after { + // background: #00000006; + // } + // } + // } + // } + // } } } @@ -509,11 +536,17 @@ main.page-scores { overflow: hidden; .cell { - padding: 0.5rem 1rem; + padding: 0.75rem; border-right: 1px solid #ddd; border-bottom: 1px solid #ddd; + display: flex; + gap: 0.75rem; + + align-items: center; + justify-content: start; + &.last-col { border-right: none; } @@ -788,16 +821,16 @@ header { .form { min-width: 50ch; + max-width: 80ch; - background: #e0e0e0; + background: #f0f0f0; display: grid; grid-template-columns: auto 1fr; - border: 1px solid #c0c0c0; - border-radius: 1rem; - - // box-shadow: -2px 4px 16px 4px #00000018, -2px 4px 4px 1px #00000018; + box-shadow: -2px 4px 6px 1px #00000018, 0 0 4px 0px #00000010; + border: 1px solid #ddd; + border-radius: 0.5rem; padding: 1.5rem 2rem 1rem; gap: 1rem; @@ -864,6 +897,21 @@ header { } } +// +// Pages +// + +main.page-admin { + .users-table { + @extend .table; + + grid-template-columns: auto 1fr auto auto; + + width: 100%; + max-width: 80ch; + } +} + // // Mobile // diff --git a/server/routes.ts b/server/routes.ts index e3e0fe5..e91ba5b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -102,6 +102,26 @@ export async function createApiRouter() { // res.json({ status: 'ok' }) // }) + r.post('/api/admin/su', async (req, res) => { + const requestUser = await getRequestUser(req) + + if (!requestUser || requestUser.role !== 'admin') { + res.sendStatus(StatusCodes.UNAUTHORIZED) + return + } + + const username: string = req.body.username ?? '' + const newUser = await getUser(db, username) + if (!newUser) { + res.status(StatusCodes.NOT_FOUND) + res.send(`no user for id "${username}"`) + return + } + + res.cookie('sid', sessions.createSession(username as UserId), { maxAge: 1000 * 60 * 60 * 24 * 7 }) + res.send({ status: 'ok' }) + }) + r.post('/api/logout', (req, res) => { res.cookie('sid', '', { expires: new Date() }) res.json({ status: 'ok' }) @@ -109,7 +129,7 @@ export async function createApiRouter() { r.get('/api/users', async (req, res) => { const requestUser = await getRequestUser(req) - if (!requestUser || !isAdministrator(requestUser.role)) { + if (!requestUser || requestUser.role !== 'admin') { res.sendStatus(StatusCodes.UNAUTHORIZED) return }