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">
|
||||
<Markdown source={content} />
|
||||
</div>
|
||||
{solutionsCount && solutionsCount > 0 && (
|
||||
{solutionsCount !== undefined && solutionsCount > 0 && (
|
||||
<div class="problem-footer">
|
||||
{solutionsCount} soluzion{solutionsCount === 1 ? 'e' : 'i'}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { JSX } from 'preact/jsx-runtime'
|
||||
import { ProblemId, SolutionStatus, UserId } from '../../shared/model'
|
||||
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 = {
|
||||
sentBy?: UserId
|
||||
forProblem: ProblemId
|
||||
content: string
|
||||
status?: SolutionStatus
|
||||
adminControls: boolean
|
||||
}
|
||||
|
||||
export const Solution = ({ sentBy, forProblem, content }: Props) => {
|
||||
export const Solution = ({ sentBy, forProblem, content, status, adminControls }: Props) => {
|
||||
return (
|
||||
<div class="solution">
|
||||
<div class={['solution', status].join(' ')}>
|
||||
<div class="solution-header">
|
||||
<div>
|
||||
Soluzione
|
||||
@@ -31,6 +40,32 @@ export const Solution = ({ sentBy, forProblem, content }: Props) => {
|
||||
<div class="solution-content">
|
||||
<Markdown source={content} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 { Header } from '../components/Header'
|
||||
import { MarkdownEditor } from '../components/MarkdownEditor'
|
||||
@@ -44,7 +48,10 @@ export const ProblemPage = ({ id }: RouteProps) => {
|
||||
<summary>Soluzioni</summary>
|
||||
<div class="solution-list">
|
||||
{solutions.map(s => (
|
||||
<Solution {...s} />
|
||||
<Solution
|
||||
{...s}
|
||||
adminControls={user !== null && isAdministrator(user.role)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
+23
-13
@@ -1,19 +1,33 @@
|
||||
import { route } from 'preact-router'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { isAdministrator, Solution as SolutionModel, User } from '../../shared/model'
|
||||
import { server } from '../api'
|
||||
import { Header } from '../components/Header'
|
||||
import { Select } from '../components/Select'
|
||||
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 = ({}) => {
|
||||
const [solutions, setSolutions] = useState([])
|
||||
|
||||
const [user, logout] = useCurrentUser(user => {
|
||||
if (user) {
|
||||
server.get(`/api/solutions?user=${user.id}`).then(solutions => {
|
||||
setSolutions(solutions)
|
||||
})
|
||||
} else {
|
||||
if (!user) {
|
||||
route('/login', true)
|
||||
}
|
||||
})
|
||||
@@ -28,11 +42,7 @@ export const ProfilePage = ({}) => {
|
||||
<main class="page-profile">
|
||||
<Header {...{ user }} />
|
||||
<div class="subtitle">Le tue soluzioni</div>
|
||||
<div class="solution-list">
|
||||
{solutions.map(({ forProblem, content }) => (
|
||||
<Solution {...{ forProblem, content }} />
|
||||
))}
|
||||
</div>
|
||||
<SolutionList {...{ user }} />
|
||||
<div class="subtitle">Altro</div>
|
||||
<button onClick={handleLogout}>Logout</button>
|
||||
</main>
|
||||
|
||||
+112
-13
@@ -35,7 +35,7 @@ textarea {
|
||||
|
||||
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;
|
||||
background: #ffffff;
|
||||
}
|
||||
@@ -52,13 +52,13 @@ input[type='text'] {
|
||||
|
||||
padding: 0.5rem;
|
||||
|
||||
box-shadow: -2px 4px 4px 0 #00000020;
|
||||
box-shadow: -2px 4px 4px 0 #00000010;
|
||||
border-radius: 0.25rem;
|
||||
background: #ffffff;
|
||||
|
||||
font-family: 'Lato';
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ input[type='text'] {
|
||||
color: #555;
|
||||
|
||||
border: 1px solid #c8c8c8;
|
||||
padding: 0 0.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
box-shadow: -2px 2px 16px 0 #00000010;
|
||||
@@ -94,7 +94,9 @@ input[type='text'] {
|
||||
|
||||
appearance: none;
|
||||
|
||||
padding: 0.5rem 0;
|
||||
transform: translate(0, -0.0625rem);
|
||||
|
||||
padding: 0.35rem 0 0.35rem 0.35rem;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -105,6 +107,16 @@ input[type='text'] {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
|
||||
transform: translate(0, 0.0625rem);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -130,6 +142,34 @@ button {
|
||||
box-shadow: -2px 2px 20px 0 #00000010;
|
||||
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,
|
||||
@@ -234,6 +274,14 @@ main {
|
||||
.subtitle {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.fill-main {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
width: 100%;
|
||||
max-width: 80ch;
|
||||
}
|
||||
}
|
||||
|
||||
main.page-home {
|
||||
@@ -270,7 +318,14 @@ main.page-problem {
|
||||
// Components
|
||||
//
|
||||
|
||||
details {
|
||||
width: 100%;
|
||||
max-width: 80ch;
|
||||
}
|
||||
|
||||
.solution-list {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
@@ -305,7 +360,7 @@ header {
|
||||
align-items: center;
|
||||
|
||||
.nav-item {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
|
||||
a.active {
|
||||
@@ -325,12 +380,12 @@ header {
|
||||
|
||||
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;
|
||||
background: #ffffff;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.problem-header {
|
||||
@@ -356,16 +411,26 @@ header {
|
||||
.solution {
|
||||
padding: 1rem;
|
||||
|
||||
width: 100%;
|
||||
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;
|
||||
background: #ffffff;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
@@ -377,6 +442,40 @@ header {
|
||||
.solution-content {
|
||||
@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 {
|
||||
@@ -450,7 +549,7 @@ header {
|
||||
|
||||
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;
|
||||
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
|
||||
//
|
||||
|
||||
export type SolutionStatus = 'pending' | 'rejected' | 'accepted'
|
||||
export type SolutionStatus = 'pending' | 'correct' | 'wrong'
|
||||
|
||||
export type SolutionId = string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user