Merge branch 'feat-domande-esami'
continuous-integration/drone/push Build is failing Details

main
Antonio De Lucreziis 1 month ago
commit 0b4bf289d4

@ -1,9 +0,0 @@
{
"printWidth": 110,
"singleQuote": true,
"quoteProps": "consistent",
"tabWidth": 4,
"useTabs": false,
"semi": false,
"arrowParens": "avoid"
}

@ -0,0 +1,26 @@
/** @type {import("prettier").Config} */
export default {
printWidth: 120,
singleQuote: true,
quoteProps: 'consistent',
tabWidth: 4,
useTabs: false,
semi: false,
arrowParens: 'avoid',
plugins: ['prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
{
files: '*.{yml,yaml,json}',
options: {
tabWidth: 2,
},
},
],
}

@ -1,3 +1,11 @@
{ {
"npm.packageManager": "bun" "npm.packageManager": "bun",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
} }

@ -18,6 +18,11 @@ export default defineConfig({
theme: 'github-light', theme: 'github-light',
}, },
}, },
integrations: [preact(), mdx()], integrations: [
preact({
compat: true,
}),
mdx(),
],
output: 'static', output: 'static',
}) })

Binary file not shown.

@ -21,12 +21,15 @@
"@fontsource/source-sans-pro": "^5.0.8", "@fontsource/source-sans-pro": "^5.0.8",
"@fontsource/space-mono": "^5.0.20", "@fontsource/space-mono": "^5.0.20",
"@phosphor-icons/core": "^2.1.1", "@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.7",
"@preact/signals": "^1.3.0", "@preact/signals": "^1.3.0",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"astro": "5.1.0", "astro": "5.1.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"katex": "^0.16.9", "katex": "^0.16.9",
"lucide-static": "^0.468.0", "lucide-static": "^0.468.0",
"marked": "^15.0.6",
"marked-extended-latex": "^1.1.0",
"preact": "^10.19.6", "preact": "^10.19.6",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
@ -37,6 +40,8 @@
"jsdom": "^24.1.1", "jsdom": "^24.1.1",
"linkedom": "^0.18.4", "linkedom": "^0.18.4",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.5.0",
"prettier-plugin-astro": "^0.14.1",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",

@ -0,0 +1,120 @@
import { useEffect, useState } from 'preact/hooks'
import { Funnel } from '@phosphor-icons/react'
import { marked } from 'marked'
// @ts-ignore
import extendedLatex from 'marked-extended-latex'
marked.use(
extendedLatex({
lazy: false,
render: (formula: string, display: boolean) => {
return display ? '$$' + formula + '$$' : '$' + formula + '$'
},
}),
)
import type { Database } from '@/data/domande-esami.yaml'
const useRemoteValue = <T,>(url: string): T | null => {
const [value, setValue] = useState<T | null>(null)
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(value => setValue(value))
.catch(error => console.error(error))
}, [url])
return value
}
type Props = {
course: string
}
export const DomandeEsamiCourse = ({ course }: Props) => {
const database = useRemoteValue<Database>(`/domande-esami/api/${course}.json`)
if (!database) {
return <>Loading...</>
}
if ('requestIdleCallback' in window) {
// @ts-ignore
requestIdleCallback(() => window.renderMath())
} else {
// @ts-ignore
setTimeout(() => window.renderMath(), 100)
}
const courseTags = [
...new Set(
database.questions.filter(question => question.course === course).flatMap(question => question.tags),
),
]
const [selectedTag, setSelectedTag] = useState<string | null>(null)
const filteredQuestions = database.questions
.filter(question => question.course === course)
.filter(question => (selectedTag ? question.tags.includes(selectedTag) : true))
return (
<>
<div class="grid-center">
<h3>
<a href="/domande-esami">Domande Esami</a>
</h3>
<h1>{database.names[course]}</h1>
</div>
{courseTags.length > 1 && (
<div class="card filter">
<div class="grid-h">
<Funnel />
<strong>Filtra Tag</strong>
</div>
<div class="flex-row-wrap">
{!selectedTag
? courseTags.map(tag => (
<div class="chip clickable" onClick={() => setSelectedTag(tag)}>
{tag}
</div>
))
: courseTags.map(tag => (
<div
class={tag === selectedTag ? 'chip clickable' : 'chip clickable disabled'}
onClick={() => setSelectedTag(tag === selectedTag ? null : tag)}
>
{tag}
</div>
))}
</div>
</div>
)}
<div class="wide-card-list" id="questions">
{filteredQuestions.length === 0 ? (
<div class="grid-center">
<em>No questions found</em>
</div>
) : (
filteredQuestions.map(question => (
<div class="card">
<div
class="text"
dangerouslySetInnerHTML={{
__html: marked(question.content, { async: false }),
}}
/>
<div class="metadata">
{question.tags.map(tag => (
<div class="chip small">{tag}</div>
))}
</div>
</div>
))
)}
</div>
</>
)
}

@ -0,0 +1,27 @@
const $debugConsole = document.createElement('div')
$debugConsole.style.position = 'fixed'
$debugConsole.style.bottom = '0'
$debugConsole.style.left = '0'
$debugConsole.style.width = '100%'
$debugConsole.style.height = '25vh'
$debugConsole.style.backgroundColor = 'black'
$debugConsole.style.color = 'white'
$debugConsole.style.overflow = 'auto'
$debugConsole.style.padding = '10px'
$debugConsole.style.boxSizing = 'border-box'
$debugConsole.style.fontFamily = 'monospace'
$debugConsole.style.zIndex = '9999'
$debugConsole.style.fontSize = '15px'
$debugConsole.style.opacity = '0.8'
document.body.appendChild($debugConsole)
function logDebugConsole(...args) {
$debugConsole.innerHTML += args.join(' ') + '<br>'
}
console.error = logDebugConsole
console.warn = logDebugConsole
console.log = logDebugConsole
console.debug = logDebugConsole

@ -5,6 +5,7 @@ const links = [
// { href: '/appunti', text: 'Appunti' }, // { href: '/appunti', text: 'Appunti' },
{ href: '/notizie', text: 'Notizie' }, { href: '/notizie', text: 'Notizie' },
{ href: '/guide', text: 'Guide' }, { href: '/guide', text: 'Guide' },
{ href: '/domande-esami', text: 'Domande Esami' },
{ href: '/storia', text: 'Storia' }, { href: '/storia', text: 'Storia' },
// { href: '/login', text: 'Login' }, // { href: '/login', text: 'Login' },
] ]

File diff suppressed because it is too large Load Diff

23
src/files.d.ts vendored

@ -19,3 +19,26 @@ declare module '@/data/macchinisti.yaml' {
const value: Macchinista[] const value: Macchinista[]
export default value export default value
} }
declare module '@/data/domande-esami.yaml' {
export type Question = {
course: string
content: string
tags: string[]
}
export type Group = {
id: string
name: string
items: Array<string>
}
export type Database = {
names: Record<string, string>
groups: Group[]
questions: Question[]
}
const value: Database
export default value
}

@ -44,7 +44,7 @@ const { title, description, thumbnail, pageTags } = Astro.props
<script> <script>
import renderMathInElement from 'katex/contrib/auto-render' import renderMathInElement from 'katex/contrib/auto-render'
document.addEventListener('DOMContentLoaded', function () { const renderMath = () => {
renderMathInElement(document.body, { renderMathInElement(document.body, {
delimiters: [ delimiters: [
{ left: '$$', right: '$$', display: true }, { left: '$$', right: '$$', display: true },
@ -54,7 +54,12 @@ const { title, description, thumbnail, pageTags } = Astro.props
], ],
throwOnError: false, throwOnError: false,
}) })
}) }
document.addEventListener('DOMContentLoaded', () => renderMath())
// @ts-ignore
window.renderMath = renderMath
</script> </script>
<script is:inline> <script is:inline>

@ -0,0 +1,28 @@
---
import type { GetStaticPaths } from 'astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro'
import { marked } from 'marked'
import database from '@/data/domande-esami.yaml'
import PhosphorIcon from '@/components/PhosphorIcon.astro'
import { DomandeEsamiCourse } from '@/client/DomandeEsamiCourse'
export const getStaticPaths = (() => {
return Object.keys(database.names).map(course => ({
params: { course },
}))
}) satisfies GetStaticPaths
const { course } = Astro.params
---
<BaseLayout title="Domande Esami | PHC" pageTags={'domande-esami'}>
<Header />
<main>
<DomandeEsamiCourse client:only="preact" course={course} />
</main>
<Footer />
</BaseLayout>

@ -0,0 +1,11 @@
import type { APIRoute } from 'astro'
import database from '@/data/domande-esami.yaml'
export const GET: APIRoute = ({}) => {
return new Response(JSON.stringify(database), {
headers: {
'content-type': 'application/json',
},
})
}

@ -0,0 +1,24 @@
import type { APIRoute, GetStaticPaths } from 'astro'
import database from '@/data/domande-esami.yaml'
export const getStaticPaths = (() => {
return Object.keys(database.names).map(course => ({
params: { course },
}))
}) satisfies GetStaticPaths
export const GET: APIRoute = ({ params: { course } }) => {
return new Response(
JSON.stringify({
groups: [],
names: Object.fromEntries(Object.entries(database.names).filter(([key]) => key === course)),
questions: database.questions.filter(question => question.course === course),
}),
{
headers: {
'content-type': 'application/json',
},
}
)
}

@ -0,0 +1,64 @@
---
import { PhosphorIcon } from '@/client/Icon'
import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
import database from '@/data/domande-esami.yaml'
const courseQuestionCounts = Object.fromEntries(
database.questions.reduce((acc, question) => {
acc.set(question.course, (acc.get(question.course) || 0) + 1)
return acc
}, new Map()),
)
---
<BaseLayout title="Domande Esami | PHC" pageTags={'domande-esami'}>
<Header />
<main>
<h1>Domande Esami</h1>
{
database.groups.map(group => (
<details open>
<summary>
<h2 id={group.id}>
<div class="details-closed">
<PhosphorIcon name="caret-down" />
</div>
<div class="details-openned">
<PhosphorIcon name="caret-up" />
</div>
{group.name}
</h2>
</summary>
<div class="wide-card-list">
{group.items
.filter(course => courseQuestionCounts[course] > 0)
.map(course => (
<a href={`/domande-esami/${course}`}>
<div class="card">
<h2>{database.names[course]}</h2>
<div class="text">
<p>{courseQuestionCounts[course] || 0} domande</p>
</div>
</div>
</a>
))}
</div>
</details>
))
}
<h3>Come Contribuire</h3>
<div class="card large">
<div class="text">
<p>
Se hai raccolto delle domande da un orale, puoi inviarcele per email all'indirizzo
<a href="mailto:macchinisti@lists.dm.unipi.it"> macchinisti@lists.dm.unipi.it</a>.
</p>
</div>
</div>
</main>
<Footer />
</BaseLayout>

@ -14,7 +14,7 @@
width: 22px; width: 22px;
height: 22px; height: 22px;
display: grid; display: grid inline;
place-content: center; place-content: center;
} }
@ -33,7 +33,11 @@
place-content: center; place-content: center;
font-size: 24px; font-size: 24px;
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24; font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24;
max-width: 32px; max-width: 32px;
} }
@ -79,29 +83,6 @@
} }
} }
.flex-column {
display: flex;
flex-direction: column;
gap: 1rem;
}
.flex-row {
display: flex;
flex-direction: row;
gap: 1rem;
}
// just to know for reference
.fake-masonry {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
grid-auto-flow: dense;
& > * {
grid-row: span var(--masonry-height);
}
}
.search-results { .search-results {
width: 100%; width: 100%;
@ -592,6 +573,13 @@
} }
} }
.metadata {
display: grid;
grid-auto-flow: column;
justify-content: start;
gap: 0.5rem;
}
@media screen and (max-width: $screen-desktop-min) { @media screen and (max-width: $screen-desktop-min) {
padding: 0.9rem; padding: 0.9rem;
@ -609,6 +597,38 @@
} }
} }
.chip {
user-select: none;
display: grid;
place-content: center;
place-items: center;
color: #111;
background: #0004;
padding: 0 0.25rem;
border-radius: 0.25rem;
line-height: 1.5;
font-size: 16px;
font-weight: 500;
&.small {
font-size: 13px;
font-weight: 600;
}
&.disabled {
color: #0004;
background: #0002;
}
}
a:has(> .card) {
display: contents;
}
// //
// Card List // Card List
// //
@ -898,4 +918,96 @@
} }
} }
} }
.wide-card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(auto, 28rem));
gap: 2rem;
padding: 2rem;
width: 100%;
justify-content: center;
// align-items: start;
.text > * {
max-width: none;
}
.card {
display: grid;
grid-template-rows: 1fr auto;
}
@media screen and (max-width: $screen-desktop-min) {
grid-template-columns: 1fr;
padding: 0;
gap: 1rem;
}
}
.filter {
min-width: 15rem;
}
.flex-column {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flex-row {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.flex-row-wrap {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
}
.grid-h {
display: grid;
grid-auto-flow: column;
justify-content: start;
align-items: center;
gap: 0.5rem;
}
.grid-v {
display: grid;
justify-items: start;
align-content: start;
grid-auto-flow: row;
gap: 0.5rem;
}
.grid-center {
display: grid;
place-content: center;
place-items: center;
gap: 0.5rem;
grid-auto-flow: row;
}
.clickable {
cursor: pointer;
}
// just to know for reference
.fake-masonry {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
grid-auto-flow: dense;
& > * {
grid-row: span var(--masonry-height);
}
}
} }

@ -129,4 +129,47 @@
justify-self: center; justify-self: center;
} }
} }
details {
width: 100%;
summary {
display: grid;
place-content: center;
place-items: center;
list-style: none;
&::-webkit-details-marker {
display: none;
}
}
.details-openned,
.details-closed {
display: contents;
}
&:not([open]) {
.details-openned {
display: none;
}
}
&[open] {
summary {
padding-bottom: 1rem;
}
.details-closed {
display: none;
}
}
@media screen and (max-width: $screen-desktop-min) {
summary {
place-content: stretch;
place-items: stretch;
}
}
}
} }

@ -122,6 +122,8 @@ body {
font-weight: 500; font-weight: 500;
letter-spacing: 1px; letter-spacing: 1px;
color: #333; color: #333;
padding: 0.25rem 1.325rem;
} }
@media screen and (max-width: $screen-desktop-min) { @media screen and (max-width: $screen-desktop-min) {
@ -208,6 +210,10 @@ body {
@media screen and (max-width: $screen-desktop-min) { @media screen and (max-width: $screen-desktop-min) {
padding: 3rem 1rem; padding: 3rem 1rem;
gap: 3rem; gap: 3rem;
.card {
width: 100%;
}
} }
} }

@ -437,6 +437,35 @@
} }
} }
.domande-esami {
background: hsl(180, 100%, 95%);
--card-base: hsl(180, 100%, 85%);
main {
justify-self: center;
display: flex;
flex-direction: column;
align-items: center;
padding: 4rem 0;
gap: 2rem;
.search {
max-width: 80ch;
}
button {
background: hsl(180, 100%, 72%);
}
.card a {
color: color-mix(in srgb, var(--card-base-internal), #000 80%);
}
}
}
// .login { // .login {
// background: #ddfaff; // background: #ddfaff;

@ -33,6 +33,18 @@
@layer typography { @layer typography {
@include geometric-headings; @include geometric-headings;
strong {
font-weight: 600;
}
em {
font-style: italic;
}
.text-center {
text-align: center;
}
.text { .text {
// text-align: justify; // text-align: justify;
// hyphens: auto; // hyphens: auto;
@ -204,6 +216,10 @@
} }
} }
li + li {
margin-top: 0.5rem;
}
a, a,
a:visited { a:visited {
color: var(--zone-color, #1e6733); color: var(--zone-color, #1e6733);

Loading…
Cancel
Save