Tasti per modificare lo stato di una soluzione e dettagli estetici
This commit is contained in:
@@ -17,7 +17,7 @@ export const Problem = ({ id, content, solutionsCount }: Props) => {
|
|||||||
<div class="problem-content">
|
<div class="problem-content">
|
||||||
<Markdown source={content} />
|
<Markdown source={content} />
|
||||||
</div>
|
</div>
|
||||||
{solutionsCount && solutionsCount > 0 && (
|
{solutionsCount !== undefined && solutionsCount > 0 && (
|
||||||
<div class="problem-footer">
|
<div class="problem-footer">
|
||||||
{solutionsCount} soluzion{solutionsCount === 1 ? 'e' : 'i'}
|
{solutionsCount} soluzion{solutionsCount === 1 ? 'e' : 'i'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
|
import { JSX } from 'preact/jsx-runtime'
|
||||||
import { ProblemId, SolutionStatus, UserId } from '../../shared/model'
|
import { ProblemId, SolutionStatus, UserId } from '../../shared/model'
|
||||||
import { Markdown } from './Markdown'
|
import { Markdown } from './Markdown'
|
||||||
|
import { Select } from './Select'
|
||||||
|
|
||||||
|
const STATUS_SELECT_OPTIONS: Record<SolutionStatus, JSX.Element> = {
|
||||||
|
['pending']: <div class="pending">Soluzione in attesa di correzione</div>,
|
||||||
|
['correct']: <div class="correct">Soluzione corretta</div>,
|
||||||
|
['wrong']: <div class="wrong">Soluzione sbagliata</div>,
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sentBy?: UserId
|
sentBy?: UserId
|
||||||
forProblem: ProblemId
|
forProblem: ProblemId
|
||||||
content: string
|
content: string
|
||||||
status?: SolutionStatus
|
status?: SolutionStatus
|
||||||
|
adminControls: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Solution = ({ sentBy, forProblem, content }: Props) => {
|
export const Solution = ({ sentBy, forProblem, content, status, adminControls }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div class="solution">
|
<div class={['solution', status].join(' ')}>
|
||||||
<div class="solution-header">
|
<div class="solution-header">
|
||||||
<div>
|
<div>
|
||||||
Soluzione
|
Soluzione
|
||||||
@@ -31,6 +40,32 @@ export const Solution = ({ sentBy, forProblem, content }: Props) => {
|
|||||||
<div class="solution-content">
|
<div class="solution-content">
|
||||||
<Markdown source={content} />
|
<Markdown source={content} />
|
||||||
</div>
|
</div>
|
||||||
|
{status && (
|
||||||
|
<div class="solution-footer">
|
||||||
|
{/* <div class="row">
|
||||||
|
<div class="label">Stato</div>
|
||||||
|
<Select value={status} options={STATUS_SELECT_OPTIONS} />
|
||||||
|
</div> */}
|
||||||
|
<div class="row">
|
||||||
|
<div class="status-label">{STATUS_SELECT_OPTIONS[status]}</div>
|
||||||
|
{adminControls && (
|
||||||
|
<>
|
||||||
|
<button disabled={status === 'pending'} class="icon">
|
||||||
|
<span class="material-symbols-outlined">hourglass_empty</span>
|
||||||
|
</button>
|
||||||
|
<button disabled={status === 'correct'} class="icon">
|
||||||
|
<span class="material-symbols-outlined correct">
|
||||||
|
check_circle
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button disabled={status === 'wrong'} class="icon">
|
||||||
|
<span class="material-symbols-outlined wrong">cancel</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { useContext, useEffect, useRef, useState } from 'preact/hooks'
|
import { useContext, useEffect, useRef, useState } from 'preact/hooks'
|
||||||
import { Problem as ProblemModel, Solution as SolutionModel } from '../../shared/model'
|
import {
|
||||||
|
isAdministrator,
|
||||||
|
Problem as ProblemModel,
|
||||||
|
Solution as SolutionModel,
|
||||||
|
} from '../../shared/model'
|
||||||
import { server } from '../api'
|
import { server } from '../api'
|
||||||
import { Header } from '../components/Header'
|
import { Header } from '../components/Header'
|
||||||
import { MarkdownEditor } from '../components/MarkdownEditor'
|
import { MarkdownEditor } from '../components/MarkdownEditor'
|
||||||
@@ -44,7 +48,10 @@ export const ProblemPage = ({ id }: RouteProps) => {
|
|||||||
<summary>Soluzioni</summary>
|
<summary>Soluzioni</summary>
|
||||||
<div class="solution-list">
|
<div class="solution-list">
|
||||||
{solutions.map(s => (
|
{solutions.map(s => (
|
||||||
<Solution {...s} />
|
<Solution
|
||||||
|
{...s}
|
||||||
|
adminControls={user !== null && isAdministrator(user.role)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
+23
-13
@@ -1,19 +1,33 @@
|
|||||||
import { route } from 'preact-router'
|
import { route } from 'preact-router'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
|
import { isAdministrator, Solution as SolutionModel, User } from '../../shared/model'
|
||||||
import { server } from '../api'
|
import { server } from '../api'
|
||||||
import { Header } from '../components/Header'
|
import { Header } from '../components/Header'
|
||||||
|
import { Select } from '../components/Select'
|
||||||
import { Solution } from '../components/Solution'
|
import { Solution } from '../components/Solution'
|
||||||
import { useCurrentUser } from '../hooks'
|
import { useCurrentUser, useReadResource } from '../hooks'
|
||||||
|
|
||||||
|
const SolutionList = ({ user }: { user: User }) => {
|
||||||
|
const [solutions, refresh] = useReadResource<SolutionModel[]>(
|
||||||
|
`/api/solutions?user=${user.id}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="solution-list">
|
||||||
|
{solutions.map(({ forProblem, content, status }) => (
|
||||||
|
<Solution
|
||||||
|
{...{ forProblem, content, status }}
|
||||||
|
adminControls={isAdministrator(user.role)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const ProfilePage = ({}) => {
|
export const ProfilePage = ({}) => {
|
||||||
const [solutions, setSolutions] = useState([])
|
|
||||||
|
|
||||||
const [user, logout] = useCurrentUser(user => {
|
const [user, logout] = useCurrentUser(user => {
|
||||||
if (user) {
|
if (!user) {
|
||||||
server.get(`/api/solutions?user=${user.id}`).then(solutions => {
|
|
||||||
setSolutions(solutions)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
route('/login', true)
|
route('/login', true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -28,11 +42,7 @@ export const ProfilePage = ({}) => {
|
|||||||
<main class="page-profile">
|
<main class="page-profile">
|
||||||
<Header {...{ user }} />
|
<Header {...{ user }} />
|
||||||
<div class="subtitle">Le tue soluzioni</div>
|
<div class="subtitle">Le tue soluzioni</div>
|
||||||
<div class="solution-list">
|
<SolutionList {...{ user }} />
|
||||||
{solutions.map(({ forProblem, content }) => (
|
|
||||||
<Solution {...{ forProblem, content }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="subtitle">Altro</div>
|
<div class="subtitle">Altro</div>
|
||||||
<button onClick={handleLogout}>Logout</button>
|
<button onClick={handleLogout}>Logout</button>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+112
-13
@@ -35,7 +35,7 @@ textarea {
|
|||||||
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010;
|
box-shadow: -2px 4px 8px 1px #00000010, 0 0 4px 0px #00000010;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -52,13 +52,13 @@ input[type='text'] {
|
|||||||
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
box-shadow: -2px 4px 4px 0 #00000020;
|
box-shadow: -2px 4px 4px 0 #00000010;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ input[type='text'] {
|
|||||||
color: #555;
|
color: #555;
|
||||||
|
|
||||||
border: 1px solid #c8c8c8;
|
border: 1px solid #c8c8c8;
|
||||||
padding: 0 0.5rem;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
box-shadow: -2px 2px 16px 0 #00000010;
|
box-shadow: -2px 2px 16px 0 #00000010;
|
||||||
@@ -94,7 +94,9 @@ input[type='text'] {
|
|||||||
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
padding: 0.5rem 0;
|
transform: translate(0, -0.0625rem);
|
||||||
|
|
||||||
|
padding: 0.35rem 0 0.35rem 0.35rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -105,6 +107,16 @@ input[type='text'] {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
|
||||||
|
transform: translate(0, 0.0625rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -130,6 +142,34 @@ button {
|
|||||||
box-shadow: -2px 2px 20px 0 #00000010;
|
box-shadow: -2px 2px 20px 0 #00000010;
|
||||||
background: linear-gradient(180deg, #fff, #ededed 20%, #e8e8e8 90%, #c0c0c0);
|
background: linear-gradient(180deg, #fff, #ededed 20%, #e8e8e8 90%, #c0c0c0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding: 0.35rem;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
filter: grayscale(1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid #c8c8c8;
|
||||||
|
box-shadow: -2px 2px 16px 0 #00000010;
|
||||||
|
background: linear-gradient(180deg, #f0f0f0, #e8e8e8 20%, #e0e0e0 90%, #cdcdcd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link,
|
.link,
|
||||||
@@ -234,6 +274,14 @@ main {
|
|||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main.page-home {
|
main.page-home {
|
||||||
@@ -270,7 +318,14 @@ main.page-problem {
|
|||||||
// Components
|
// Components
|
||||||
//
|
//
|
||||||
|
|
||||||
|
details {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80ch;
|
||||||
|
}
|
||||||
|
|
||||||
.solution-list {
|
.solution-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -305,7 +360,7 @@ header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
||||||
a.active {
|
a.active {
|
||||||
@@ -325,12 +380,12 @@ header {
|
|||||||
|
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
|
|
||||||
box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010;
|
box-shadow: -2px 4px 8px 1px #00000010, 0 0 4px 0px #00000010;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: auto 1fr auto;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.problem-header {
|
.problem-header {
|
||||||
@@ -356,16 +411,26 @@ header {
|
|||||||
.solution {
|
.solution {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
|
|
||||||
box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010;
|
box-shadow: -2px 4px 8px 1px #00000010, 0 0 4px 0px #00000010;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: auto 1fr;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&.correct {
|
||||||
|
// background: hsl(120, 100%, 90%);
|
||||||
|
box-shadow: 0 0 12px 2px #00990030, 0 0 1px 1px #00330030;
|
||||||
|
}
|
||||||
|
&.wrong {
|
||||||
|
// background: hsl(0, 100%, 90%);
|
||||||
|
box-shadow: 0 0 12px 2px #99000030, 0 0 1px 1px #33000030;
|
||||||
|
}
|
||||||
|
|
||||||
.solution-header {
|
.solution-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
@@ -377,6 +442,40 @@ header {
|
|||||||
.solution-content {
|
.solution-content {
|
||||||
@extend .text-body;
|
@extend .text-body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.solution-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
color: #1653a3;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.correct {
|
||||||
|
color: green;
|
||||||
|
// font-weight: 400;
|
||||||
|
}
|
||||||
|
.wrong {
|
||||||
|
color: darkred;
|
||||||
|
// font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
@@ -450,7 +549,7 @@ header {
|
|||||||
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010;
|
box-shadow: -2px 4px 8px 1px #00000010, 0 0 4px 0px #00000010;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { ProblemId, SolutionId, UserId } from './model'
|
|
||||||
|
|
||||||
// export function refToUserId(ref: string): UserId {
|
|
||||||
// if (!ref.startsWith('user/')) {
|
|
||||||
// throw new Error(`"${ref}" is not a reference to a User`)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return ref.slice('user/'.length)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export function refToProblemId(ref: string): ProblemId {
|
|
||||||
// if (!ref.startsWith('problem/')) {
|
|
||||||
// throw new Error(`"${ref}" is not a reference to a Problem`)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return ref.slice('problem/'.length)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export function refToSolutionId(ref: string): SolutionId {
|
|
||||||
// if (!ref.startsWith('solution/')) {
|
|
||||||
// throw new Error(`"${ref}" is not a reference to a Solution`)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return ref.slice('solution/'.length)
|
|
||||||
// }
|
|
||||||
+1
-1
@@ -48,7 +48,7 @@ export type Problem = {
|
|||||||
// Solutions
|
// Solutions
|
||||||
//
|
//
|
||||||
|
|
||||||
export type SolutionStatus = 'pending' | 'rejected' | 'accepted'
|
export type SolutionStatus = 'pending' | 'correct' | 'wrong'
|
||||||
|
|
||||||
export type SolutionId = string
|
export type SolutionId = string
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user