New Website in VueJS #6

Open
alice wants to merge 11 commits from new-website into main
127 changed files with 7502 additions and 1485 deletions

30
.gitignore vendored
View file

@ -1,2 +1,28 @@
public # Logs
resources logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Hugo/Vite output directory
public/
resources/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "content/wiki/adm.wiki"] [submodule "content/wiki/adm.wiki"]
path = content/wiki/adm.wiki path = content/wiki/adm.wiki
url = git@git.students.cs.unibo.it:adm/adm.wiki.git url = git@git.students.cs.unibo.it:adm/adm.wiki.git
[submodule "themes/website-theme"]
path = themes/website-theme
url = https://git.students.cs.unibo.it/adm/website-theme

View file

View file

@ -1,29 +1,72 @@
# ADMStaff website: https://students.cs.unibo.it # ADMStaff website
To contribute to the website, you can edit the markdown files in the `content` directory. https://students.cs.unibo.it
Firstly, you need to install [Hugo](https://gohugo.io/getting-started/installing/). This project uses Vue.js.
Next, once you have cloned the repo, you need to:
## 1. Install submodules ## Content
The site content lives in [content](content). Markdown files are used directly by the site, while [archetypes](archetypes) contains the Hugo-style templates used to create new content consistently.
Create a new page from the archetype with:
```bash ```bash
git submodule init && git submodule update pnpm new-content events/my_new_event
``` ```
## 2. Format the Wiki The command creates `content/events/my_new_event.md` and fills front matter defaults:
Use this command to compile the wiki into hugo format:
> Note: Do not push any changes on the adm-wiki submodule - `title`: derived from the file name.
- `date`: current local timestamp with timezone.
- `author`: `ADMStaff`.
Any kind of attachment (images, PDFs, etc.) can be placed in `static/img`, and referenced in markdown with `/img/filename.ext`.
Optional overrides:
```bash ```bash
python3 format_wiki.py pnpm new-content content/projects/my_tool.md --title "My Tool" --date "2026-04-14T10:00:00+02:00" --author "ADMStaff"
``` ```
## 3. Compile the Website ## Commands
```bash ```bash
hugo pnpm install
pnpm dev
pnpm build
pnpm preview
pnpm new-content events/my_new_event
pnpm format
``` ```
For local hosting - `pnpm dev`: starts the local development server.
- `pnpm build`: creates the production build and generates OG images.
- `pnpm preview`: previews the generated build locally.
- `pnpm new-content`: creates a new Markdown file in `content/` from `archetypes/default.md`.
- `pnpm format`: formats the project code and markdown files.
## Wiki
The wiki is a git submodule in `content/wiki/adm.wiki`.
To sync it:
```bash ```bash
hugo server git submodule update --init --recursive
cd content/wiki/adm.wiki
git pull
cd ../../..
python format_wiki.py
``` ```
The `format_wiki.py` script adapts the wiki files to the format used by the site.
## Deploy
Deployment starts from the local build:
```bash
pnpm build
```
The build generates the `dist` folder, which contains the static files to publish.

View file

@ -1,8 +1,9 @@
--- ---
title: '{{ replace .File.ContentBaseName "-" " " | title }}' title: '{{ replace .File.ContentBaseName "-" " " | title }}'
Review

Do we need the brackets? We are not parsing this as a jinja template...

Do we need the brackets? We are not parsing this as a jinja template...
date: {{ .Date }} date: { { .Date } }
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
# cover: # cover:
# link: # per i progetti
--- ---

View file

@ -1,11 +1,8 @@
--- ---
title: 'Chi siamo' title: "Chi siamo"
toc: false toc: false
# cover: 'img/ADMstaff_logo-modern-trasp.png'
--- ---
{{< figure src="/img/ADMstaff_logo-modern.png" title="ADMstaff logo" style="width: 40%;" >}}
ADMStaff è un gruppo di studenti del Dipartimento di Informatica dell'Università di Bologna, che sperimenta e approfondisce sistemi operativi e la gestione di infrastrutture IT. Dal 1994, guidati dal Prof. Renzo Davoli, portiamo avanti una tradizione: **servizi gestiti da studenti, per studenti**. Tutti i servizi che offriamo sono _open source_ e nati dalla passione e dall'impegno dei nostri membri. ADMStaff è un gruppo di studenti del Dipartimento di Informatica dell'Università di Bologna, che sperimenta e approfondisce sistemi operativi e la gestione di infrastrutture IT. Dal 1994, guidati dal Prof. Renzo Davoli, portiamo avanti una tradizione: **servizi gestiti da studenti, per studenti**. Tutti i servizi che offriamo sono _open source_ e nati dalla passione e dall'impegno dei nostri membri.
## Cosa facciamo ## Cosa facciamo
@ -15,20 +12,21 @@ Il nostro obiettivo è sperimentare e mettere in pratica ciò che ci appassiona:
L'università ci fornisce uno spazio fisico in via Mura Anteo Zamboni 7, IP pubblici e un dominio dedicato dove possiamo lavorare e sperimentare "senza limiti". L'università ci fornisce uno spazio fisico in via Mura Anteo Zamboni 7, IP pubblici e un dominio dedicato dove possiamo lavorare e sperimentare "senza limiti".
In particolare, offriamo: In particolare, offriamo:
- **Wiki ADM**: La nostra documentazione tecnica e le guide per utilizzare i nostri servizi. [Visita il Wiki](/wiki/adm.wiki/) - **Wiki ADM**: La nostra documentazione tecnica e le guide per utilizzare i nostri servizi. [Visita il Wiki](/wiki/adm.wiki/)
- **Servizi di hosting**: Offriamo hosting web, database e altri servizi per progetti studenteschi e personali. - **Servizi di hosting**: Offriamo hosting web, database e altri servizi per progetti studenteschi e personali.
- **sasso**: Un servizio di VPS per studenti e membri del dipartimento. [Scopri di più su sasso](https://sasso.students.cs.unibo.it) - **sasso**: Un servizio di VPS per studenti e membri del dipartimento. [Scopri di più su sasso](https://sasso.students.cs.unibo.it)
- **Laboratori fra pari**: Labortori aperti agli studenti in cui li guidiamo nell'apprendimenti di strumenti come: Shell, Vim, Git, GitHub, CI/CD. [Tutti i materiali usati sono disponibili qui](https://github.com/cartabinaria/lab) - **Laboratori fra pari**: Labortori aperti agli studenti in cui li guidiamo nell'apprendimenti di strumenti come: Shell, Vim, Git, GitHub, CI/CD. [Tutti i materiali usati sono disponibili qui](https://github.com/cartabinaria/lab)
- **Linux Installation Party**: Eventi periodici in cui aiutiamo gli studenti a installare Linux sui loro computer personali. - **Linux Installation Party**: Eventi periodici in cui aiutiamo gli studenti a installare Linux sui loro computer personali.
## Cosa usiamo per gestire la nostra infrastruttura ## Cosa usiamo per gestire la nostra infrastruttura
Utilizziamo tecnologie open source come Linux, Proxmox, Docker, Ansible, Git/Forgejo, PostgreSQL, Caddy, ZFS, TrueNAS, Netbox. Questi strumenti ci permettono di costruire e mantenere un'infrastruttura solida e scalabile, imparando continuamente nuove competenze. Utilizziamo tecnologie open source come Linux, Proxmox, Docker, Ansible, Git/Forgejo, PostgreSQL, Caddy, ZFS, TrueNAS, Netbox. Questi strumenti ci permettono di costruire e mantenere un'infrastruttura solida e scalabile, imparando continuamente nuove competenze.
## Unisciti a noi ## Unisciti a noi
Cerchiamo studenti curiosi e motivati. Non importa il livello di esperienza: conta la voglia di imparare e mettersi in gioco. Passa a trovarci in laboratorio o scopri i nostri [eventi](/events). Cerchiamo studenti curiosi e motivati. Non importa il livello di esperienza: conta la voglia di imparare e mettersi in gioco. Passa a trovarci in laboratorio o scopri i nostri [eventi](/events).
Se sei interessato scrivici su Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat) Se sei interessato scrivici su Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat)
![ADM 2024](/img/team2024.jpg)

View file

@ -1,12 +1,9 @@
--- ---
title: 'Who are we?' title: "Who are we?"
toc: false toc: false
# cover: 'img/ADMstaff_logo-modern-trasp.png' # cover: 'img/ADMstaff_logo-modern-trasp.png'
--- ---
{{< figure src="/img/ADMstaff_logo-modern.png" title="ADMstaff logo" style="width: 40%;" >}}
ADMStaff is a group of students from the Department of Computer Science at the University of Bologna. We experiment with and deepen our knowledge of operating systems and IT infrastructure management. Since 1994, under the guidance of Prof. Renzo Davoli, we have carried on a tradition: **services run by students, for students**. All the services we provide are _open source_ and are driven by the passion and commitment of our members. ADMStaff is a group of students from the Department of Computer Science at the University of Bologna. We experiment with and deepen our knowledge of operating systems and IT infrastructure management. Since 1994, under the guidance of Prof. Renzo Davoli, we have carried on a tradition: **services run by students, for students**. All the services we provide are _open source_ and are driven by the passion and commitment of our members.
## What we do ## What we do
@ -16,21 +13,21 @@ Our goal is to experiment with and put into practice what we love: system admini
The university provides us with a physical space at Via Mura Anteo Zamboni 7, public IPs, and a dedicated domain where we can work and experiment “without limits”. The university provides us with a physical space at Via Mura Anteo Zamboni 7, public IPs, and a dedicated domain where we can work and experiment “without limits”.
In particular, we provide: In particular, we provide:
- **ADM Wiki**: Our technical documentation and guides for using our services. [Visit the Wiki](/wiki/adm.wiki/) - **ADM Wiki**: Our technical documentation and guides for using our services. [Visit the Wiki](/wiki/adm.wiki/)
- **Hosting services**: We offer web hosting, databases, and other services for student and personal projects. - **Hosting services**: We offer web hosting, databases, and other services for student and personal projects.
- **sasso**: A VPS service for students and department members. [Learn more about sasso](https://sasso.students.cs.unibo.it) - **sasso**: A VPS service for students and department members. [Learn more about sasso](https://sasso.students.cs.unibo.it)
- **Peer-to-peer workshops**: Workshops open to students where we help them learn tools such as Shell, Vim, Git, GitHub, and CI/CD. [All the materials used are available here](https://github.com/cartabinaria/lab) - **Peer-to-peer workshops**: Workshops open to students where we help them learn tools such as Shell, Vim, Git, GitHub, and CI/CD. [All the materials used are available here](https://github.com/cartabinaria/lab)
- **Linux Installation Party**: Periodic events where we help students install Linux on their personal computers. - **Linux Installation Party**: Periodic events where we help students install Linux on their personal computers.
## What we use to run our infrastructure ## What we use to run our infrastructure
We use open-source technologies such as Linux, Proxmox, Docker, Ansible, Git/Forgejo, PostgreSQL, Caddy, ZFS, TrueNAS, and Netbox. These tools help us build and maintain a solid, scalable infrastructure while continuously learning new skills. We use open-source technologies such as Linux, Proxmox, Docker, Ansible, Git/Forgejo, PostgreSQL, Caddy, ZFS, TrueNAS, and Netbox. These tools help us build and maintain a solid, scalable infrastructure while continuously learning new skills.
## Join us ## Join us
Were looking for curious and motivated students. Your experience level doesnt matter: what counts is the desire to learn and get involved. Drop by the lab or check out our [events](/events). Were looking for curious and motivated students. Your experience level doesnt matter: what counts is the desire to learn and get involved. Drop by the lab or check out our [events](/events).
If youre interested, write to us on Telegram: [t.me/admstaff_Chat](https://t.me/admstaff_Chat) If youre interested, write to us on Telegram: [t.me/admstaff_Chat](https://t.me/admstaff_Chat)
![ADM 2024](/img/team2024.jpg)

View file

@ -1,8 +1,9 @@
--- ---
title: 'Laboratori fra pari' title: "Laboratori fra pari"
date: 2021-11-23T18:00:00+02:00 date: 2021-11-23T18:00:00+02:00
author: "Admstaff" author: "Admstaff"
framed: true framed: true
toc: false toc: false
--- ---
Per restare aggiornato entra nel [**gruppo telegram**](https://t.me/admstaff_Chat) Per restare aggiornato entra nel [**gruppo telegram**](https://t.me/admstaff_Chat)

View file

@ -1,14 +1,14 @@
--- ---
title: 'Giacomo Cavalieri @Unibo' title: "Giacomo Cavalieri @Unibo"
date: 2024-05-01T15:36:06+02:00 date: 2024-05-01T15:36:06+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/cavalieri.png cover: img/cavalieri.png
--- ---
| Date and Time | Location | | Date and Time | Location |
| --- | --- | | ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
| mercoledì 8 maggio 2024 dalle 18:00 alle 19:30 | Aula Tonelli (sesto piano), Dipartimento di Matematica, Piazza di Porta S. Donato 5, Bologna | | mercoledì 8 maggio 2024 dalle 18:00 alle 19:30 | Aula Tonelli (sesto piano), Dipartimento di Matematica, Piazza di Porta S. Donato 5, Bologna |
Te lo sei perso? Trovi la registrazione qui: https://media.students.cs.unibo.it/gleam.mp4 Te lo sei perso? Trovi la registrazione qui: https://media.students.cs.unibo.it/gleam.mp4
@ -20,4 +20,3 @@ Abbiamo invitato per tenere un talk Giacomo Cavalieri, ex studente e tutor unibo
Per altre info in più consigliamo: Per altre info in più consigliamo:
https://gleam.run/ https://gleam.run/
https://github.com/giacomocavalieri https://github.com/giacomocavalieri

View file

@ -7,6 +7,7 @@ draft: false
--- ---
Anche quest'anno con grande richiesta riproponiamo laboratori su: Anche quest'anno con grande richiesta riproponiamo laboratori su:
- **Terminale Base**: impariamo a muoverci nel terminale, manipolare file/directory, gestire i permessi... - **Terminale Base**: impariamo a muoverci nel terminale, manipolare file/directory, gestire i permessi...
- **Git 1 | comandi base**: impariamo a gestire i nostri progetti con Git, creare repository, fare commit... - **Git 1 | comandi base**: impariamo a gestire i nostri progetti con Git, creare repository, fare commit...
- **Git 2 | condivisione remota**: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente. - **Git 2 | condivisione remota**: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente.
@ -15,6 +16,7 @@ Le date e luogo di svolgimento le trovate nella locandina sottostante.
_**Disclaimer**_ _**Disclaimer**_
Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati. Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati.
> È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti. > È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti.
### Iscrizione richiesta per motivi organizzativi sul [typeform 'CSUnibo in Ascolto!'](https://lr533gb3hpt.typeform.com/to/m5fzNCsW) ### Iscrizione richiesta per motivi organizzativi sul [typeform 'CSUnibo in Ascolto!'](https://lr533gb3hpt.typeform.com/to/m5fzNCsW)

View file

@ -1,13 +1,14 @@
--- ---
title: 'Laboratori 2024' title: "Laboratori 2024"
date: 2024-10-01T09:48:38+02:00 date: 2024-10-01T09:48:38+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/laboratori-24.png cover: img/laboratori-24.png
--- ---
Anche questo semestre con grande richiesta riproponiamo laboratori su: Anche questo semestre con grande richiesta riproponiamo laboratori su:
- **Terminale base** _[15/10/2024]_: primi passi sulla shell - **Terminale base** _[15/10/2024]_: primi passi sulla shell
- **Git 1 | comandi base** _[22/10/2024]_: impariamo a gestire i nostri progetti con Git, creare repository, fare commit... - **Git 1 | comandi base** _[22/10/2024]_: impariamo a gestire i nostri progetti con Git, creare repository, fare commit...
- **Git 2 | condivisione remota** _[05/11/2024]_: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente. - **Git 2 | condivisione remota** _[05/11/2024]_: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente.
@ -20,7 +21,7 @@ Anche questo semestre con grande richiesta riproponiamo laboratori su:
- variabili - variabili
- gestione processi - gestione processi
- scripting - scripting
... ...
Gli incontri si terranno tutti dalle 17:00 alle 19:00 in Aula Bombelli (Dipartimento di Matematica, piazza Porta S. Donato 5). Gli incontri si terranno tutti dalle 17:00 alle 19:00 in Aula Bombelli (Dipartimento di Matematica, piazza Porta S. Donato 5).
@ -29,6 +30,7 @@ Posti disponibili per ogni incontro: 40
_**Disclaimer**_ _**Disclaimer**_
Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati. Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati.
> È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti. > È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti.
Per qualsiasi info o domande: [t.me/admstaff_Chat](https://t.me/admstaff_Chat) Per qualsiasi info o domande: [t.me/admstaff_Chat](https://t.me/admstaff_Chat)

View file

@ -1,14 +1,15 @@
--- ---
title: 'Laboratori 2025' title: "Laboratori 2025"
date: 2025-04-22T09:48:38+02:00 date: 2025-04-22T09:48:38+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/laboratori-25.png cover: img/laboratori-25.png
--- ---
Anche questo semestre con grande richiesta riproponiamo laboratori su: Anche questo semestre con grande richiesta riproponiamo laboratori su:
- **Git 1 | comandi base** _[08/05/2025]__: impariamo a gestire i nostri progetti con Git, creare repository, fare commit...
- **Git 1 | comandi base** \_[08/05/2025]\_\_: impariamo a gestire i nostri progetti con Git, creare repository, fare commit...
- **Git 2 | condivisione remota** _[15/05/2025]_: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente. - **Git 2 | condivisione remota** _[15/05/2025]_: carichiamo le nostre repository su GitHub, collaboriamo con altri utenti, gestiamo i conflitti, issue, pull request. Approfondiremo come fare su GitHub tutte le operazioni base che abbiamo imparato a fare da terminale l'incontro precedente.
- **GitHub | ci/cd** _[22/05/2025]_: impariamo a creare un workflow di CI/CD con GitHub Actions, per automatizzare i test e il deploy del nostro codice. - **GitHub | ci/cd** _[22/05/2025]_: impariamo a creare un workflow di CI/CD con GitHub Actions, per automatizzare i test e il deploy del nostro codice.
@ -18,6 +19,7 @@ Posti disponibili per ogni incontro: 40
_**Disclaimer**_ _**Disclaimer**_
Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati. Questi non sono corsi universitari, sono incontri/laboratori organizzati da studenti per studenti per imparare insieme e mettere mano da subito sugli argomenti trattati.
> È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti. > È quindi fondamentale **portare il proprio PC** per riuscire a seguire gli esercizi proposti.
Per qualsiasi info o domande: [t.me/admstaff_Chat](https://t.me/admstaff_Chat) Per qualsiasi info o domande: [t.me/admstaff_Chat](https://t.me/admstaff_Chat)

View file

@ -1,32 +1,31 @@
--- ---
title: 'Laboratori a.a. 2025/26' title: "Laboratori a.a. 2025/26"
date: 2025-10-16T09:48:38+02:00 date: 2025-10-16T09:48:38+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/laboratori-25-26.png cover: img/laboratori-25-26.jpg
--- ---
Anche per l'anno accademico 2025/26 organizziamo i laboratori fra pari: degli incontri gratuiti, aperti a tutti gli studenti. Di seguito il calendario e una breve descrizione degli incontri: Anche per l'anno accademico 2025/26 organizziamo i laboratori fra pari: degli incontri gratuiti, aperti a tutti gli studenti. Di seguito il calendario e una breve descrizione degli incontri:
- **Terminale 1 — comandi base** (04/11/2025) - **Terminale 1 — comandi base** (04/11/2025)
- Impariamo a muoverci nel filesystem, creare, spostare, copiare ed eliminare file e cartelle; uso dei comandi più comuni. - Impariamo a muoverci nel filesystem, creare, spostare, copiare ed eliminare file e cartelle; uso dei comandi più comuni.
- **Terminale 2 — redirezioni e pipe** (06/11/2025) - **Terminale 2 — redirezioni e pipe** (06/11/2025)
- Redirezioni per salvare l'output su file, concatenazione di comandi con pipe; introduzione a grep, find, xargs e affini. - Redirezioni per salvare l'output su file, concatenazione di comandi con pipe; introduzione a grep, find, xargs e affini.
- **Git 1 — comandi base** (13/11/2025) - **Git 1 — comandi base** (13/11/2025)
- Gestione di progetti con Git: inizializzare repository, commit, branch di base. - Gestione di progetti con Git: inizializzare repository, commit, branch di base.
- **Git 2 — condivisione remota** (20/11/2025) - **Git 2 — condivisione remota** (20/11/2025)
- Caricamento su GitHub, collaborazione remota e operazioni pratiche su GitHub rilevanti rispetto all'incontro precedente. - Caricamento su GitHub, collaborazione remota e operazioni pratiche su GitHub rilevanti rispetto all'incontro precedente.
- **GitHub 1 — issue, pull request, conflitti** (27/11/2025) - **GitHub 1 — issue, pull request, conflitti** (27/11/2025)
- Gestione di issue e pull request, risoluzione dei conflitti e strumenti di collaborazione. - Gestione di issue e pull request, risoluzione dei conflitti e strumenti di collaborazione.
- **GitHub 2 — CI/CD, Actions, Git LFS** (04/12/2025) - **GitHub 2 — CI/CD, Actions, Git LFS** (04/12/2025)
- Creazione di workflow CI/CD con GitHub Actions, test automatici, deploy e uso di Git LFS. - Creazione di workflow CI/CD con GitHub Actions, test automatici, deploy e uso di Git LFS.
Il primo incontro (Terminale 1) si terrà il 4 novembre 2025 dalle **17:00 alle 19:00** in **Aula Pincherle** (Dipartimento di Matematica, Piazza Porta S. Donato 5, piano 2). Il primo incontro (Terminale 1) si terrà il 4 novembre 2025 dalle **17:00 alle 19:00** in **Aula Pincherle** (Dipartimento di Matematica, Piazza Porta S. Donato 5, piano 2).

View file

@ -1,20 +1,22 @@
--- ---
title: 'Linux Installation Party' title: "Linux Installation Party"
date: 2024-09-20T16:20:31+02:00 date: 2024-09-20T16:20:31+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/linux-installation-party-2024.png cover: img/linux-installation-party-2024.png
--- ---
Ti piacerebbe avere una configurazione in dual-boot o provare Linux come macchina virtuale? Vieni al nostro Linux Installation Party, organizzato da ADMStaff! Ti piacerebbe avere una configurazione in dual-boot o provare Linux come macchina virtuale? Vieni al nostro Linux Installation Party, organizzato da ADMStaff!
Durante questo evento ti guideremo passo dopo passo nellinstallazione di Linux sul tuo PC. Sarà loccasione perfetta per entrare nel mondo del software libero, scoprire i vantaggi di un sistema operativo open-source, e ottenere supporto personalizzato durante tutto il processo. Durante questo evento ti guideremo passo dopo passo nellinstallazione di Linux sul tuo PC. Sarà loccasione perfetta per entrare nel mondo del software libero, scoprire i vantaggi di un sistema operativo open-source, e ottenere supporto personalizzato durante tutto il processo.
| Date and Time | Location | | Date and Time | Location |
| --- | --- | | ------------------------------------------------ | -------------------------------------------------- |
| 24 settembre 2024 dalle ore 17:00 alle ore 18:30 | Il giorno in ADMLab, mura Anteo Zamboni 7, Bologna | | 24 settembre 2024 dalle ore 17:00 alle ore 18:30 | Il giorno in ADMLab, mura Anteo Zamboni 7, Bologna |
## Cosa portare: ## Cosa portare:
- Il tuo computer - Il tuo computer
- Una chiavetta USB vuota - Una chiavetta USB vuota
- Almeno 30GB di spazio libero sul disco - Almeno 30GB di spazio libero sul disco

View file

@ -1,23 +1,25 @@
--- ---
title: 'Linux Installation Party' title: "Linux Installation Party"
date: 2025-10-01T16:20:31+02:00 date: 2025-10-01T16:20:31+02:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/linux-installation-party-2025-2.png cover: img/linux-installation-party-2025-2.png
--- ---
Ti piacerebbe avere una configurazione in dual-boot o provare Linux come macchina virtuale? Vieni al nostro Linux Installation Party, organizzato da ADMStaff! Ti piacerebbe avere una configurazione in dual-boot o provare Linux come macchina virtuale? Vieni al nostro Linux Installation Party, organizzato da ADMStaff!
Durante questo evento ti guideremo passo dopo passo nellinstallazione di Linux sul tuo PC. Sarà loccasione perfetta per entrare nel mondo del software libero, scoprire i vantaggi di un sistema operativo open-source, e ottenere supporto personalizzato durante tutto il processo. Durante questo evento ti guideremo passo dopo passo nellinstallazione di Linux sul tuo PC. Sarà loccasione perfetta per entrare nel mondo del software libero, scoprire i vantaggi di un sistema operativo open-source, e ottenere supporto personalizzato durante tutto il processo.
| Date and Time | Location | | Date and Time | Location |
| --- | --- | | ------------------------------------------------------------- | ---------------------------------------- |
|[concluso] 08 ottobre 2025 dalle ore 14:00 fino a chiusura | in ADMLab, mura Anteo Zamboni 7, Bologna | | [concluso] 08 ottobre 2025 dalle ore 14:00 fino a chiusura | in ADMLab, mura Anteo Zamboni 7, Bologna |
| 17 ottobre 2025 dalle 10:00 e dalle ore 14:00 fino a chiusura | in ADMLab, mura Anteo Zamboni 7, Bologna | | 17 ottobre 2025 dalle 10:00 e dalle ore 14:00 fino a chiusura | in ADMLab, mura Anteo Zamboni 7, Bologna |
Per accedere al laboratorio è necessario avere il badge, in caso di difficoltà all'accesso contattateci tramite il nostro gruppo Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat). Per accedere al laboratorio è necessario avere il badge, in caso di difficoltà all'accesso contattateci tramite il nostro gruppo Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat).
## Cosa portare: ## Cosa portare:
- Il tuo computer - Il tuo computer
- Una chiavetta USB vuota - Una chiavetta USB vuota
- Almeno 30GB di spazio libero sul disco - Almeno 30GB di spazio libero sul disco
@ -26,7 +28,6 @@ Per accedere al laboratorio è necessario avere il badge, in caso di difficoltà
> >
> Per restare aggiornato sull'evento, unisciti al nostro gruppo Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat). > Per restare aggiornato sull'evento, unisciti al nostro gruppo Telegram [t.me/admstaff_Chat](https://t.me/admstaff_Chat).
Locandine di quest'anno: Locandine di quest'anno:
![Linux Installation Party 2025 - primo incontro 08/10](img/linux-installation-party-2025-1.png) ![Linux Installation Party 2025 - primo incontro 08/10](img/linux-installation-party-2025-1.png)
![Linux Installation Party 2025 - secondo incontro 17/10](img/linux-installation-party-2025-2.png) ![Linux Installation Party 2025 - secondo incontro 17/10](img/linux-installation-party-2025-2.png)

View file

@ -7,8 +7,11 @@ toc: false
--- ---
## 29 novembre alle ore 10:00 in Via Ranzani 14, Laboratorio di Informatica, secondo piano. ## 29 novembre alle ore 10:00 in Via Ranzani 14, Laboratorio di Informatica, secondo piano.
### Chi siamo e cosa facciamo. ### Chi siamo e cosa facciamo.
Oltre a parlare di questo verranno anche introdotte alcuni concetti come: Oltre a parlare di questo verranno anche introdotte alcuni concetti come:
- Infrastructure as a Code - Infrastructure as a Code
- Come e' pensata e costruita in questo momento la nostra infrastruttura - Come e' pensata e costruita in questo momento la nostra infrastruttura
- le CTF Attacck/Defense. - le CTF Attacck/Defense.

View file

@ -1,14 +1,14 @@
--- ---
title: 'Richard Stallman @Unibo' title: "Richard Stallman @Unibo"
date: 2024-03-28T21:00:25+01:00 date: 2024-03-28T21:00:25+01:00
author: 'ADMStaff' author: "ADMStaff"
toc: false toc: false
framed: false framed: false
cover: img/stallman.jpg cover: img/stallman.jpg
--- ---
| Date and Time | Location | | Date and Time | Location |
| --- | --- | | ------------------------------------------------- | ---------------------------------------------------------------- |
| Il giorno 09 aprile 2024 dalle ore 15 alle ore 17 | in AULA D BERTI PICHAT, Viale Carlo Berti Pichat, 6-6/2, Bologna | | Il giorno 09 aprile 2024 dalle ore 15 alle ore 17 | in AULA D BERTI PICHAT, Viale Carlo Berti Pichat, 6-6/2, Bologna |
Richard Stallman terrà un seminario dal titolo: **"Software libero and freedom in the digital society"** Richard Stallman terrà un seminario dal titolo: **"Software libero and freedom in the digital society"**
@ -22,20 +22,23 @@ Se te lo sei perso lo abbiamo registrato: http://media.students.cs.unibo.it/rms-
> NOTA: per motivi di salute dell'ospite è obbligatorio l'uso della mascherina che copra naso e bocca. > NOTA: per motivi di salute dell'ospite è obbligatorio l'uso della mascherina che copra naso e bocca.
## Abstract: ## Abstract:
There are many threats to freedom in the digital society. They include massive surveillance, censorship, digital handcuffs, nonfree software that controls There are many threats to freedom in the digital society. They include massive surveillance, censorship, digital handcuffs, nonfree software that controls
users, and the War on Sharing. Computers for voting make election results untrustworthy. Other threats come from use of web services. Finally, we have users, and the War on Sharing. Computers for voting make election results untrustworthy. Other threats come from use of web services. Finally, we have
no assured right to make any particular use of the Internet; every activity is precarious, permitted only as long as companies are willing to cooperate with no assured right to make any particular use of the Internet; every activity is precarious, permitted only as long as companies are willing to cooperate with
our doing it. All of these threats originate more or less in the use of nonfree software. That is why free software is the first battle in the liberation of cyberspace. our doing it. All of these threats originate more or less in the use of nonfree software. That is why free software is the first battle in the liberation of cyberspace.
## Bio: ## Bio:
Richard Matthew Stallman (born March 16, 1953) is an American computer programmer and free-software advocate. He is widely known by his initials, rms. Stallman campaigns passionately for software to be distributed in a manner that grants users the freedom to use, study, distribute, and modify that software. Richard Matthew Stallman (born March 16, 1953) is an American computer programmer and free-software advocate. He is widely known by his initials, rms. Stallman campaigns passionately for software to be distributed in a manner that grants users the freedom to use, study, distribute, and modify that software.
Heres a brief overview of his life and contributions: Heres a brief overview of his life and contributions:
- **Education**: Stallman graduated from Harvard University in 1974 with a degree in physics. - **Education**: Stallman graduated from Harvard University in 1974 with a degree in physics.
MIT Years: From 1971 to 1984, he worked at the MIT Artificial Intelligence Lab, where he developed system software. Notably, he created the first extensible text editor called Emacs in 1976 and contributed to the AI technique of dependency-directed backtracking (also known as truth maintenance) in 1975. MIT Years: From 1971 to 1984, he worked at the MIT Artificial Intelligence Lab, where he developed system software. Notably, he created the first extensible text editor called Emacs in 1976 and contributed to the AI technique of dependency-directed backtracking (also known as truth maintenance) in 1975.
- **Free Software Movement**: In 1983, Stallman launched the Free Software Movement by announcing the project to develop the GNU operating system, which aimed to consist entirely of free (freedom-respecting) software. He began working on GNU in 1984 after resigning from MIT employment. - **Free Software Movement**: In 1983, Stallman launched the Free Software Movement by announcing the project to develop the GNU operating system, which aimed to consist entirely of free (freedom-respecting) software. He began working on GNU in 1984 after resigning from MIT employment.
- **GNU Project and Copyleft**: Stallman established the Free Software Foundation in October 1985. He invented the concept of copyleft, which allows users to modify and redistribute software while preserving its freedom. The GNU General Public License (GPL), co-written by Stallman, implements copyleft and has inspired initiatives like Creative Commons. - **GNU Project and Copyleft**: Stallman established the Free Software Foundation in October 1985. He invented the concept of copyleft, which allows users to modify and redistribute software while preserving its freedom. The GNU General Public License (GPL), co-written by Stallman, implements copyleft and has inspired initiatives like Creative Commons.
- **Software Contributions**: Stallman personally developed several widely used software components of the GNU system, including the GNU Compiler Collection, the GNU symbolic debugger (gdb), and GNU Emacs. The GNU/Linux system, which combines GNU software with the Linux kernel developed by Linus Torvalds, powers tens or hundreds of millions of computers worldwide. - **Software Contributions**: Stallman personally developed several widely used software components of the GNU system, including the GNU Compiler Collection, the GNU symbolic debugger (gdb), and GNU Emacs. The GNU/Linux system, which combines GNU software with the Linux kernel developed by Linus Torvalds, powers tens or hundreds of millions of computers worldwide.
- **Advocacy and Travel**: Nowadays, Stallman focuses on political advocacy for free software and its ethical principles. He travels extensively, speaking on topics such as “Free Software And Your Freedom,” “Copyright vs Community in the Age of Computer Networks,” and “A Free Digital Society.” - **Advocacy and Travel**: Nowadays, Stallman focuses on political advocacy for free software and its ethical principles. He travels extensively, speaking on topics such as “Free Software And Your Freedom,” “Copyright vs Community in the Age of Computer Networks,” and “A Free Digital Society.”
- **Inspirations**: Stallmans call for a free online encyclopedia helped inspire the creation of Wikipedia. His contributions have been recognized with awards, including the MacArthur Foundation Fellowship and the Association for Computings Grace Murray Hopper Award. - **Inspirations**: Stallmans call for a free online encyclopedia helped inspire the creation of Wikipedia. His contributions have been recognized with awards, including the MacArthur Foundation Fellowship and the Association for Computings Grace Murray Hopper Award.
For further insights, you can explore his book of essays titled “Free Software, Free Society” and his semiautobiography “Free as in Freedom”. For further insights, you can explore his book of essays titled “Free Software, Free Society” and his semiautobiography “Free as in Freedom”.

View file

@ -1,21 +1,23 @@
--- ---
title: 'Laboratorio | ADMstanzetta' title: "Laboratorio | ADMstanzetta"
date: 2023-10-28T00:00:00Z date: 2023-10-28T00:00:00Z
author: 'ADMStaff' author: "ADMStaff"
draft: false draft: false
framed: false framed: false
toc: false toc: false
cover: 'img/ADMlab.jpg' cover: "img/ADMlab.jpg"
--- ---
Da settembre 2023 ci troviamo in via Mura Anteo Zamboni 7, nel piano seminterrato, Aula 02. Da settembre 2023 ci troviamo in via Mura Anteo Zamboni 7, nel piano seminterrato, Aula 02.
## Perché venire in laboratorio? ## Perché venire in laboratorio?
- Spazi dedicati allo studio - Spazi dedicati allo studio
- Postazioni con autenticazioneADM di libero accesso - Postazioni con autenticazioneADM di libero accesso
- [Connessione ethernet](#come-mi-connetto-con-i-cavi) senza limitazioni di porte - [Connessione ethernet](#come-mi-connetto-con-i-cavi) senza limitazioni di porte
## Come mi connetto con i cavi? ## Come mi connetto con i cavi?
1. Collega il cavo ethernet al tuo computer 1. Collega il cavo ethernet al tuo computer
2. Apri un terminale: `ssh auth@130.136.201.254` 2. Apri un terminale: `ssh auth@130.136.201.254`
3. Segui le istruzioni da terminale usando le **tue credenziali unibo** 3. Segui le istruzioni da terminale usando le **tue credenziali unibo**

View file

@ -1,9 +1,10 @@
--- ---
title: 'ADMStaff Wallpapers' title: "ADMStaff Wallpapers"
toc: false toc: false
author: "ADMStaff" author: "ADMStaff"
date: "2024-04-19" date: "2024-04-19"
--- ---
Per scaricarli vai su https://media.students.cs.unibo.it/wallpapers o clicca sullo sfondo che preferisci. Per scaricarli vai su https://media.students.cs.unibo.it/wallpapers o clicca sullo sfondo che preferisci.
[{{< figure src="https://media.students.cs.unibo.it/wallpapers/ADMstaff_wallpaper-dark.png" title="ADMstaff wallpaper dark" style="width: 100%;" >}}](https://media.students.cs.unibo.it/wallpapers/ADMstaff_wallpaper-dark.png) [{{< figure src="https://media.students.cs.unibo.it/wallpapers/ADMstaff_wallpaper-dark.png" title="ADMstaff wallpaper dark" style="width: 100%;" >}}](https://media.students.cs.unibo.it/wallpapers/ADMstaff_wallpaper-dark.png)

View file

@ -1,10 +0,0 @@
---
title: "Ansible"
date: 2021-11-23T18:00:00+02:00
author: "Admstaff"
draft: false
toc: false
cover: 'img/ansible.png'
---
Ansible è un software open-source che automatizza la configurazione, la gestione e l'installazione di software su computer. È un software di gestione della configurazione, che funziona su molti sistemi operativi, tra cui i sistemi basati su Unix e quelli basati su Microsoft Windows. Ansible è stato progettato per essere semplice da usare e leggero, e non richiede l'installazione di software client su nodi gestiti.

15
content/projects/dumbo.md Normal file
View file

@ -0,0 +1,15 @@
---
title: "Dumbo"
cover: "img/dumbo.svg"
date: 2024-03-16T18:00:00+02:00
author: "Admstaff"
draft: false
toc: false
link: "https://dumbo.students.cs.unibo.it"
---
Dumbo è un servizio che permette di monitorare l'attività delle macchine del dipartimento DISI. Per ogni macchina vengono raccolti dati come l'utilizzo della CPU, della RAM, del disco e della rete, e vengono visualizzati in tempo reale su un'interfaccia web.
Potete accedere a Dumbo all'indirizzo [https://dumbo.students.cs.unibo.it/](https://dumbo.students.cs.unibo.it/)
Dumbo è stato sviluppato per essere utilizzato dagli studenti del dipartimento DISI, ma è accessibile a chiunque voglia monitorare le macchine del dipartimento.

View file

@ -4,7 +4,10 @@ date: 2021-11-23T18:00:00+02:00
author: "Admstaff" author: "Admstaff"
draft: false draft: false
toc: false toc: false
cover: 'img/git-forgejo.png' link: "https://git.students.cs.unibo.it"
cover: "img/git-forgejo.png"
--- ---
Git è un sistema di controllo di versione distribuito, gratuito e open source progettato per gestire tutto, dai piccoli ai grandi progetti con velocità ed efficienza. Git è un sistema di controllo di versione distribuito, gratuito e open source progettato per gestire tutto, dai piccoli ai grandi progetti con velocità ed efficienza.
Abbiamo una nostra istanza di GitLab, chiamata Forgejo, dove ospitiamo i nostri progetti e collaboriamo per la gestione dell'infrastruttura e dei servizi. Se sei interessato a contribuire o vuoi saperne di più sui nostri progetti, visita la nostra [pagina git.students](https://git.students.cs.unibo.it).

View file

@ -1,7 +0,0 @@
---
title: 'Linux'
cover: 'img/linux.png'
toc: false
---
Linux è un kernel di sistema operativo Unix-like creato da Linus Torvalds. Il kernel Linux è stato rilasciato per la prima volta il 17 settembre 1991 da Linus Torvalds. Linux è stato progettato per essere un sistema operativo open source e gratuito. Il kernel Linux è stato progettato per funzionare su molti tipi di hardware, tra cui computer desktop, server, dispositivi mobili e dispositivi embedded.

View file

@ -4,7 +4,11 @@ date: 2021-11-23T18:00:00+02:00
author: "Admstaff" author: "Admstaff"
draft: false draft: false
toc: false toc: false
cover: 'img/proxmox.png' cover: "img/proxmox.png"
--- ---
Proxmox è una piattaforma open source di virtualizzazione che permette di gestire macchine virtuali e container. Proxmox VE è basato su Debian GNU/Linux e utilizza il kernel Linux. Proxmox VE è in grado di gestire macchine virtuali e container, storage, reti e cluster di macchine. Proxmox VE include un web interface e un API REST per la gestione. Proxmox è una piattaforma open source di virtualizzazione che permette di gestire macchine virtuali e container. Proxmox VE è basato su Debian GNU/Linux e utilizza il kernel Linux. Proxmox VE è in grado di gestire macchine virtuali e container, storage, reti e cluster di macchine. Proxmox VE include un web interface e un API REST per la gestione.
Lo hostiamo sulla nostra infrastruttura e lo utilizziamo per ospitare i nostri servizi.
Un servizio utile alla gestione di Proxmox è il tool per avere ID consistenti delle macchine virtuali, che è disponibile su [vm.students.cs.unibo.it/](https://vm.students.cs.unibo.it/).

32
content/projects/sasso.md Normal file
View file

@ -0,0 +1,32 @@
---
title: "sasso"
date: 2025-11-25T18:00:00+02:00
author: "Admstaff"
draft: false
toc: false
link: "https://sasso.students.cs.unibo.it"
cover: "https://sasso.students.cs.unibo.it/sasso.png"
---
Sasso is a VPS built on top of Proxmox. It allows users to create and manage
virtual machines in a controlled and secure environment, without giving them
direct access to the Proxmox cluster.
This service should be used when multiple users need to create and manage their
own virtual machines, but they don't have direct access to the Proxmox cluster
where the VMs are hosted.
Sasso provides resource management for each user through a web interface.
It creates every virtual machine in a separate Proxmox VNet using a VXLAN Zone
to keep the VMs from differente users isolated from each other.
> [!WARNING]
> At the moment Sasso is in an early stage of development. Expect bugs, missing
> features and breaking changes.
This service was developed to be used by [ADMStaff](https://students.cs.unibo.it).
Please refer to the [Wiki](https://github.com/samuelemusiani/sasso/wiki) for more
information about the architecture, how to deploy and how to use Sasso.
The source code is available on [GitHub](https://github.com/samuelemusiani/sasso).

11
content/projects/wifi.md Normal file
View file

@ -0,0 +1,11 @@
---
title: "Wifi"
date: 2025-04-16T10:27:34+02:00
author: "ADMStaff"
toc: false
# framed: false
cover: "/img/wifi-pass.png"
link: "https://auth.lab.students.cs.unibo.it"
---
In ADM Lab è presente una rete Wi-Fi dedicata agli studenti, che consente di accedere a Internet e alle risorse del laboratorio. Per connettersi alla rete Wi-Fi, è necessario conoscere le credenziali per connettersi all'Access Point. Una volta connessi si aprirà il captive portal, dove potete registarvi e creare le vostre credenziali ADM. Queste possono essere usate per accedere sì alla rete, ma anche a tutti i servizi che abbiamo realizzato e richiedono un'autenticazione, come [sasso](projects/sasso).

View file

@ -1,87 +0,0 @@
baseURL = 'https://students.cs.unibo.it/'
languageCode = 'it-it'
title = 'ADMStaff'
theme = 'website-theme'
# Default language
defaultContentLanguage = "it"
[params]
# if you set this to 0, only submenu trigger will be visible
showMenuItems = 5
# dir name of your blog content (default is `content/posts`).
# the list of set content will show up on your index page (baseurl).
# contentTypeName = "events"
# Show table of contents at the top of your posts (defaults to false)
# Alternatively, add this param to post front matter for specific posts
toc = true
[params.logo]
logoText = "ADMStaff"
logoHomeLink = "/"
[languages]
[languages.it]
languageCode = "it-it"
languageName = "Italiano"
weight = 1
contentDir = "content"
[[languages.it.menu.main]]
identifier = "about"
name = "About Us"
url = "/about"
[[languages.it.menu.main]]
identifier = "wiki"
name = "Wiki"
url = "/wiki/adm.wiki"
[[languages.it.menu.main]]
identifier = "lab"
name = "Laboratorio"
url = "/lab"
[[languages.it.menu.main]]
identifier = "events"
name = "Eventi"
url = "/events"
[[languages.it.menu.main]]
identifier = "media"
name = "Media"
url = "/media"
[languages.en]
languageCode = "en-us"
languageName = "English"
weight = 2
contentDir = "content/en"
[[languages.en.menu.main]]
identifier = "about"
name = "About Us"
url = "/en/about"
[[languages.en.menu.main]]
identifier = "wiki"
name = "Wiki"
url = "/wiki/adm.wiki"
[[languages.en.menu.main]]
identifier = "lab"
name = "Laboratory"
url = "/lab"
[[languages.en.menu.main]]
identifier = "events"
name = "Events"
url = "/events"
[[languages.en.menu.main]]
identifier = "media"
name = "Media"
url = "/media"

17
index.html Normal file
View file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en" data-theme="admtheme">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/png"
href="/src/assets/ADMstaff_logo-favicon.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ADMStaff</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -1,63 +0,0 @@
{{ define "main" }}
{{ if .Content }}
<div class="index-content {{ if .Params.framed -}}framed{{- end -}}">
<h1>{{ .Title }}</h1>
{{ .Content }}
</div>
{{ end }}
<div class="posts">
{{ range .Pages }}
<article class="post on-list">
<h1 class="post-title"><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<div class="post-meta">
{{ if .Date }}
<time class="post-date">
{{ .Date.Format ($.Site.Params.DateFormatList | default "2006-01-02") }}
</time>
{{ if $.Site.Params.LastModDisplay }}
{{ partial "lastmod.html" . }}
{{ end }}
{{ end }}
{{ with .Params.Author }}
<span class="post-author">— {{ $.Site.Params.WrittenBy | default "Written by" }} {{ . }}</span>{{ end }}
{{ if $.Site.Params.ShowReadingTime }}
<span class="post-read-time">— {{ .ReadingTime }} {{ $.Site.Params.MinuteReadingTime | default "min read"
}}</span>
{{ end }}
</div>
{{ if .Params.tags }}
<span class="post-tags">
{{ range .Params.tags }}
<a href="{{ (urlize (printf " tags/%s" . )) | absLangURL }}/">#{{ . }}</a>&nbsp;
{{ end }}
</span>
{{ end }}
{{ if .Params.Cover }}
{{ partial "postcover.html" . }}
{{ end }}
<div class="post-content">
{{ with .Description }}
{{ . | markdownify }}
{{ else }}
{{ if .Truncated }}
{{ .Summary }}
{{ end }}
{{ end }}
</div>
{{ if not .Params.hideReadMore }}
<div>
<a class="read-more button" href="{{ .RelPermalink }}">{{ $.Site.Params.ReadMore | default "Read more" }}
</a>
</div>
{{ end }}
</article>
{{ end }}
{{ partial "pagination.html" . }}
</div>
{{ end }}

View file

@ -1,53 +0,0 @@
{{ define "main" }}
<div class="about">
<h4 id="about-text">ADMstaff nasce come gruppo che fornisce servizi da studenti per studenti.<br><br>
Il nostro obiettivo è sperimentare e fare pratica su ciò che ci appassiona e ci
incuriosisce. <br>Tutto
ciò che stuzzica la curiosità è alla nostra portata</h4>
<p class="logo__cursor" id="about-cursor"></p>
</div>
<br />
<h1>Ultimi Eventi</h1>
<div class="card-container">
{{ $events := site.GetPage "/events" }}
{{ range first 3 $events.Pages.ByDate.Reverse }}
<div class="card" id="events">
<a href="{{ .Permalink }}">
<h2>{{ .Title | markdownify }}</h2>
{{ if .Params.Cover }}
<img src="{{ .Params.Cover }}" alt="{{ .Title }}" class="card-img">
{{ end }}
<div class="card-body">
{{ .Summary | truncate 100 }}
</div>
</a>
</div>
{{ end }}
</div>
<br />
<h1>I nostri strumenti</h1>
<div class="card-container">
{{ $projects := site.GetPage "/projects" }}
{{ range $projects.Pages.ByTitle }}
<div class="card">
<a href="{{ .Permalink }}">
<h2>{{ .Title | markdownify }}</h2>
{{ if .Params.Cover }}
<img src="{{ .Params.Cover }}" alt="{{ .Title }}" class="card-img">
{{ end }}
<div class="card-body">
{{ .Summary | truncate 100 }}
</div>
{{ if .Params.tags }}
<div class="card-tags">
{{ range .Params.tags }}
<a href="{{ (urlize (printf " tags/%s" . )) | absLangURL }}/">#{{ . }}</a>
{{ end }}
</div>
{{ end }}
</a>
</div>
{{ end }}
</div>
{{ end }}

View file

@ -1,63 +0,0 @@
{{ define "main" }}
{{ if .Content }}
<div class="index-content titolo-wiki {{ if .Params.framed -}}framed{{- end -}}">
{{ .Content }}
</div>
{{ end }}
<div class="posts">
{{ range .Pages.ByTitle }}
<article class="post on-list">
<h1 class="post-title"><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<div class="post-meta">
{{ if .Date }}
<time class="post-date">
{{ .Date.Format ($.Site.Params.DateFormatList | default "2006-01-02") }}
</time>
{{ if $.Site.Params.LastModDisplay }}
{{ partial "lastmod.html" . }}
{{ end }}
{{ end }}
{{ with .Params.Author }}
<span class="post-author">— {{ $.Site.Params.WrittenBy | default "Written by" }} {{ . }}</span>{{ end }}
{{ if $.Site.Params.ShowReadingTime }}
<span class="post-read-time">— {{ .ReadingTime }} {{ $.Site.Params.MinuteReadingTime | default "min read"
}}</span>
{{ end }}
</div>
{{ if .Params.tags }}
<span class="post-tags">
{{ range .Params.tags }}
<a href="{{ (urlize (printf " tags/%s" . )) | absLangURL }}/">#{{ . }}</a>&nbsp;
{{ end }}
</span>
{{ end }}
{{ if .Params.Cover }}
{{ partial "postcover.html" . }}
{{ end }}
<div class="post-content">
{{ with .Description }}
{{ . | markdownify }}
{{ else }}
{{ if .Truncated }}
{{ .Summary | truncate 100 }}
{{ end }}
{{ end }}
</div>
<!-- {{ if not .Params.hideReadMore }}
<div>
<a class="read-more button" href="{{ .RelPermalink }}"
>{{ $.Site.Params.ReadMore | default "Read more" }} →</a
>
</div>
{{ end }} -->
</article>
{{ end }}
{{ partial "pagination.html" . }}
</div>
{{ end }}

BIN
og_base.png (Stored with Git LFS) Normal file

Binary file not shown.

2490
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "admstaff-website",
"private": true,
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "pnpm generate-og && vite build",
"preview": "vite preview",
"generate-og": "node scripts/generate-og-images.js",
"new-content": "node scripts/new-content.js",
"format": "prettier --write \"{src,content,layouts,scripts,archetypes}/**/*.{js,vue,css,md,html,json}\" \"*.{js,json,md,html,toml}\""
},
"dependencies": {
"@vueuse/head": "^2.0.0",
"vue": "^3.5.25",
"vue-i18n": "^11.2.8",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@iconify/vue": "^5.0.0",
"@napi-rs/canvas": "^0.1.92",
"@vitejs/plugin-vue": "^6.0.2",
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.14",
"marked": "^12.0.0",
"postcss": "^8.4.49",
"prettier": "^3.8.2",
"tailwindcss": "^3.4.17",
"vite": "^7.3.1"
}
}

1752
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
ignoredBuiltDependencies:
- canvas

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/img/laboratori-25-26.png (Stored with Git LFS)

Binary file not shown.

View file

@ -1 +0,0 @@
{"Target":"css/style.css","MediaType":"text/css","Data":{}}

View file

@ -0,0 +1,180 @@
import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Review

We could add ../ at _dirname as we add it on every part of the code on this page

We could add `../` at _dirname as we add it on every part of the code on this page
// Registra il font JetBrains Mono
const fontPath = path.join(
__dirname,
"../static/fonts/JetBrainsMono-Medium.ttf",
);
GlobalFonts.registerFromPath(fontPath, "JetBrains Mono");
// Configurazione (come in Hugo)
const config = {
x: 141,
y: 300,
fontSize: 55,
color: "#ffffff",
lineSpacing: 1.2,
maxWidth: 1080, // larghezza massima per il testo
basePngPath: path.join(__dirname, "../og_base.png"),
};
/**
* Genera un'immagine OpenGraph con il titolo sovrapposto
* @param {string} title - Il titolo da scrivere
* @param {string} outputPath - Percorso dove salvare l'immagine
*/
export async function generateOGImage(title, outputPath) {
Review

Do we need to export this func? We don't use it anywhere outside this file

Do we need to export this func? We don't use it anywhere outside this file
try {
// Carica l'immagine base
const baseImage = await loadImage(config.basePngPath);
// Crea canvas con le stesse dimensioni dell'immagine base
const canvas = createCanvas(baseImage.width, baseImage.height);
const ctx = canvas.getContext("2d");
// Disegna l'immagine base
ctx.drawImage(baseImage, 0, 0);
// Configura il testo
ctx.font = `${config.fontSize}px "JetBrains Mono"`;
ctx.fillStyle = config.color;
ctx.textBaseline = "top";
// Gestisci il text wrapping
const lines = wrapText(ctx, title, config.maxWidth);
// Disegna ogni linea
lines.forEach((line, index) => {
const y = config.y + index * config.fontSize * config.lineSpacing;
ctx.fillText(line, config.x, y);
});
// Salva l'immagine
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const buffer = canvas.toBuffer("image/png");
fs.writeFileSync(outputPath, buffer);
console.log(`✓ Generated OG image: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`✗ Error generating OG image for "${title}":`, error);
throw error;
}
}
/**
* Divide il testo in più righe se necessario
* @param {CanvasRenderingContext2D} ctx
* @param {string} text
* @param {number} maxWidth
* @returns {string[]}
*/
function wrapText(ctx, text, maxWidth) {
const words = text.split(" ");
const lines = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const width = ctx.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
/**
* Genera immagini OG per tutti i contenuti markdown
*/
export async function generateAllOGImages() {
Review

Do we need to export this func? We don't use it anywhere outside this file

Do we need to export this func? We don't use it anywhere outside this file
const contentDir = path.join(__dirname, "../content");
const publicDir = path.join(__dirname, "../public/og");
Review

The OG images are placed in /public/og, but after that no ones read them or interact with them. They are also not present in the build files after pnpm run build, so they don't work :)

The OG images are placed in /public/og, but after that no ones read them or interact with them. They are also not present in the build files after `pnpm run build`, so they don't work :)
// Leggi tutti i file markdown
const files = getAllMarkdownFiles(contentDir);
console.log(`\nGenerating OpenGraph images for ${files.length} files...\n`);
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const title = extractTitle(content);
if (title) {
const relativePath = path.relative(contentDir, file);
const slug = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
Review

It's longer but for path normalization should be better to use:

const slug = path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep)).replace(/\.md$/, "");
It's longer but for path normalization should be better to use: ```js const slug = path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep)).replace(/\.md$/, ""); ```
const outputPath = path.join(publicDir, `${slug}.png`);
await generateOGImage(title, outputPath);
}
}
console.log(`\nAll OpenGraph images generated!\n`);
}
/**
* Trova tutti i file markdown ricorsivamente
* @param {string} dir
* @returns {string[]}
*/
function getAllMarkdownFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
getAllMarkdownFiles(filePath, fileList);
} else if (file.endsWith(".md") && !file.startsWith("_")) {
fileList.push(filePath);
}
});
return fileList;
}
/**
* Estrae il titolo dal frontmatter o dal primo heading
* @param {string} content
* @returns {string|null}
*/
function extractTitle(content) {
Review

Also here we don't need a custom parser, we could use https://www.npmjs.com/package/gray-matter

Also here we don't need a custom parser, we could use https://www.npmjs.com/package/gray-matter
// Cerca nel frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const titleMatch = frontmatterMatch[1].match(/title:\s*['"](.*?)['"]/);
if (titleMatch) return titleMatch[1];
}
// Cerca nel primo heading
const headingMatch = content.match(/^#\s+(.+)$/m);
if (headingMatch) return headingMatch[1];
return null;
}
// Esegui lo script se chiamato direttamente
if (import.meta.url.startsWith("file:")) {
Review

Do we really need this trick to call the script itself? Could we just use a simple main()?

Do we really need this trick to call the script itself? Could we just use a simple main()?
const modulePath = fileURLToPath(import.meta.url);
const scriptPath = process.argv[1];
if (modulePath === scriptPath) {
generateAllOGImages().catch(console.error);
}
}

185
scripts/new-content.js Normal file
View file

@ -0,0 +1,185 @@
import fs from "node:fs/promises";
import path from "node:path";
function printUsage() {
console.log(
"Usage: pnpm new-content <target> [--title <title>] [--date <date>] [--author <author>] [--force]",
);
console.log("Examples:");
console.log(" pnpm new-content events/new-event");
console.log(
' pnpm new-content content/projects/new-tool.md --title "New Tool"',
);
}
function parseArgs(argv) {
const args = argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printUsage();
process.exit(0);
}
const options = {
target: null,
title: null,
date: null,
author: null,
force: false,
};
let i = 0;
while (i < args.length) {
const current = args[i];
if (!current.startsWith("--") && options.target === null) {
options.target = current;
i += 1;
continue;
}
if (current === "--force") {
options.force = true;
i += 1;
continue;
}
if (
current === "--title" ||
current === "--date" ||
current === "--author"
) {
const value = args[i + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Missing value for ${current}`);
}
if (current === "--title") options.title = value;
if (current === "--date") options.date = value;
if (current === "--author") options.author = value;
i += 2;
continue;
}
throw new Error(`Unknown argument: ${current}`);
}
if (!options.target) {
throw new Error("Missing target path");
}
return options;
}
function toLocalIsoWithTimezone(date = new Date()) {
const pad = (n) => String(n).padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
const tzMinutes = -date.getTimezoneOffset();
const sign = tzMinutes >= 0 ? "+" : "-";
const tzAbs = Math.abs(tzMinutes);
const tzHours = pad(Math.floor(tzAbs / 60));
const tzMins = pad(tzAbs % 60);
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${tzHours}:${tzMins}`;
}
function slugToTitle(filePath) {
const slug = path.basename(filePath, ".md");
return slug
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (ch) => ch.toUpperCase());
}
function escapeYamlString(value) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
function resolveTargetPath(targetArg, contentRoot) {
let normalized = targetArg.replace(/\\/g, "/").replace(/^\.\//, "");
if (normalized.startsWith("content/")) {
normalized = normalized.slice("content/".length);
}
if (!normalized || normalized.endsWith("/")) {
throw new Error(
"Target must be a markdown file path, for example: events/my-event.md",
);
}
if (!normalized.toLowerCase().endsWith(".md")) {
normalized = `${normalized}.md`;
}
const absolute = path.resolve(contentRoot, normalized);
const contentPrefix = contentRoot.endsWith(path.sep)
? contentRoot
: `${contentRoot}${path.sep}`;
if (absolute !== contentRoot && !absolute.startsWith(contentPrefix)) {
throw new Error("Target path must stay inside content/");
}
return absolute;
}
async function main() {
const { target, title, date, author, force } = parseArgs(process.argv);
const repoRoot = process.cwd();
const contentRoot = path.resolve(repoRoot, "content");
const archetypePath = path.resolve(repoRoot, "archetypes", "default.md");
Review

Does it make sense to have the template as a file? Because we only read it from here

Does it make sense to have the template as a file? Because we only read it from here
const targetPath = resolveTargetPath(target, contentRoot);
const resolvedTitle = title ?? slugToTitle(targetPath);
const resolvedDate = date ?? toLocalIsoWithTimezone(new Date());
const resolvedAuthor = author ?? "ADMStaff";
const [template, existing] = await Promise.all([
fs.readFile(archetypePath, "utf8"),
fs
.access(targetPath)
.then(() => true)
.catch(() => false),
]);
if (existing && !force) {
throw new Error(
`File already exists: ${path.relative(repoRoot, targetPath)} (use --force to overwrite)`,
);
}
let output = template;
output = output.replace(
/^title:\s*.*$/m,
`title: "${escapeYamlString(resolvedTitle)}"`,
);
output = output.replace(/^date:\s*.*$/m, `date: ${resolvedDate}`);
output = output.replace(
/^author:\s*.*$/m,
`author: "${escapeYamlString(resolvedAuthor)}"`,
);
if (!output.endsWith("\n")) {
output += "\n";
}
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, output, "utf8");
console.log(`Created: ${path.relative(repoRoot, targetPath)}`);
}
main().catch((error) => {
console.error(`Error: ${error.message}`);
process.exit(1);
});

18
src/App.vue Normal file
View file

@ -0,0 +1,18 @@
<template>
<div
data-theme="admtheme"
class="min-h-screen bg-base-100 text-base-content font-mono"
>
<Header />
<main class="container mx-auto px-4 py-8">
<RouterView />
</main>
<Footer />
</div>
</template>
<script setup>
import { RouterView } from "vue-router";
import Header from "./components/Header.vue";
import Footer from "./components/Footer.vue";
</script>

BIN
src/assets/ADMstaff_logo-favicon.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,20 @@
<template>
Review

This file is never used

This file is never used
<div class="flex items-center justify-center">
<img :src="src" :alt="alt" :class="imgClass" class="max-w-full h-auto" />
<span class="blinking-cursor text-4xl text-primary ml-2"></span>
</div>
</template>
<script setup>
defineProps({
src: {
type: String,
default: "/img/ADMstaff_logo-modern.png",
},
alt: {
type: String,
default: "ADMstaff logo",
},
imgClass: String,
});
</script>

14
src/components/Footer.vue Normal file
View file

@ -0,0 +1,14 @@
<template>
Review

In shorter pages foot is in the middle of the page. I would force it at the end, always

In shorter pages foot is in the middle of the page. I would force it at the end, always
<footer class="bg-neutral border-t border-base-300 mt-16 py-6">
<div class="container mx-auto px-4 text-center text-sm text-secondary">
<p>© {{ new Date().getFullYear() }} {{ $t("footer.copyright") }}</p>
Review

For the copyright symbol you can use &#xA9

For the copyright symbol you can use `&#xA9`
<p class="mt-2">{{ $t("footer.tagline") }}</p>
</div>
</footer>
</template>
<script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
Review

We don't use this

We don't use this
</script>

138
src/components/Header.vue Normal file
View file

@ -0,0 +1,138 @@
<template>
Review

secondo me è un po piccolino, preferirei il testo più grande o anche più grande tutto

secondo me è un po piccolino, preferirei il testo più grande o anche più grande tutto
Review

ma dici le scritte dentro all'header, il loghetto o proprio tutto?

ma dici le scritte dentro all'header, il loghetto o proprio tutto?
<header class="bg-neutral border-b border-base-300">
<nav class="container mx-auto px-4">
<div class="navbar min-h-16 px-0">
<div class="navbar-start">
<RouterLink
to="/"
class="group inline-flex shrink-0 items-center font-bold no-underline"
>
<span class="mr-1 inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5 text-base-content/70"
viewBox="0 0 44 44"
>
<path
fill="none"
class="stroke-current stroke-[6]"
d="M15 8l14.729 14.382L15 35.367"
/>
</svg>
</span>
<span
class="inline text-lg text-base-content/70 transition-colors group-hover:text-primary"
>ADMStaff</span
>
<span class="blinking-cursor ml-1"></span>
</RouterLink>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1 gap-1">
<li v-for="link in links" :key="link.path">
<RouterLink
:to="link.path"
class="text-base-content hover:text-primary"
>
{{ $t(link.nameKey) }}
</RouterLink>
</li>
</ul>
</div>
<div class="navbar-end gap-2">
<div class="join hidden lg:flex">
<button
v-for="lang in languages"
:key="`desktop-${lang.code}`"
@click="setLocale(lang.code)"
:class="languageBtnClass(lang.code)"
>
{{ lang.label }}
</button>
</div>
<div class="dropdown dropdown-end lg:hidden">
<label
tabindex="0"
class="btn btn-ghost btn-square"
aria-label="Open navigation menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-5 w-5 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</label>
<ul
tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] w-56 rounded-box border border-base-300 bg-base-100 p-2 shadow"
>
<li v-for="link in links" :key="`mobile-${link.path}`">
<RouterLink :to="link.path">{{ $t(link.nameKey) }}</RouterLink>
</li>
<li class="menu-title"><span>Language</span></li>
<li>
<div class="join justify-start">
<button
v-for="lang in languages"
:key="`mobile-${lang.code}`"
@click="setLocale(lang.code)"
:class="languageBtnClass(lang.code, true)"
>
{{ lang.label }}
</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</nav>
</header>
</template>
Review

I would also add Home, even though you could reach it by clicking the logo

I would also add `Home`, even though you could reach it by clicking the logo
<script setup>
import { RouterLink } from "vue-router";
import { useI18n } from "vue-i18n";
const { locale } = useI18n();
const links = [
{ path: "/about", nameKey: "nav.about" },
{ path: "/wiki", nameKey: "nav.wiki" },
{ path: "/events", nameKey: "nav.events" },
{ path: "/projects", nameKey: "nav.projects" },
{ path: "/media", nameKey: "nav.media" },
{ path: "/lab", nameKey: "nav.lab" },
Outdated
Review

isMobile is never used :)

But could we just duplicate the classes in the two buttons and avoid using this function?

isMobile is never used :) But could we just duplicate the classes in the two buttons and avoid using this function?
];
const languages = [
{ code: "it", label: "IT" },
{ code: "en", label: "EN" },
];
function languageBtnClass(code, isMobile = false) {
if (locale.value === code) {
return "btn btn-sm join-item btn-primary text-white";
}
return isMobile
? "btn btn-sm join-item btn-ghost"
: "btn btn-sm join-item btn-ghost text-base-content hover:text-primary";
}
function setLocale(lang) {
locale.value = lang;
localStorage.setItem("locale", lang);
}
</script>

81
src/components/Layout.vue Normal file
View file

@ -0,0 +1,81 @@
<template>
Review

This file is never used

This file is never used
<div class="min-h-screen bg-base-100">
<!-- Header -->
<header class="border-b border-base-300 bg-neutral">
<nav class="container mx-auto px-4 py-4">
<div class="flex justify-between items-center">
<router-link
to="/"
class="text-2xl font-bold text-primary hover:opacity-80"
>
ADMStaff<span class="blinking-cursor">_</span>
</router-link>
<div class="flex gap-6">
<router-link
to="/about"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
About
</router-link>
<router-link
to="/wiki"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
Wiki
</router-link>
<router-link
to="/events"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
Events
</router-link>
<router-link
to="/projects"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
Projects
</router-link>
<router-link
to="/media"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
Media
</router-link>
<router-link
to="/lab"
class="hover:text-primary transition-colors"
active-class="text-primary font-semibold"
>
Lab
</router-link>
</div>
</div>
</nav>
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<slot />
</main>
<!-- Footer -->
<footer class="border-t border-base-300 bg-neutral mt-16">
<div
class="container mx-auto px-4 py-6 text-center text-sm text-secondary"
>
<p>&copy; {{ new Date().getFullYear() }} ADMStaff</p>
<p class="mt-2">Services run by students, for students since 1994</p>
</div>
</footer>
</div>
</template>
<script setup>
import { RouterLink } from "vue-router";
</script>

View file

@ -0,0 +1,179 @@
import { ref, onMounted, watch } from "vue";
import { parseMarkdown } from "../utils/markdownParser.js";
import { useI18n } from "vue-i18n";
/**
* Composable per caricare contenuti markdown dalla directory content/
* Usa import.meta.glob di Vite per importare i file direttamente
*/
// Importa tutti i file markdown dalla directory content (più specifico)
const contentFiles = import.meta.glob(
["/content/**/*.md", "!**/node_modules/**", "!**/public/**"],
Review

We don't need the last two matching strings, only the first one

We don't need the last two matching strings, only the first one
{ query: "?raw", import: "default", eager: false },
);
export function useContent(path) {
const { locale } = useI18n();
const content = ref(null);
const loading = ref(true);
const error = ref(null);
const loadContent = async () => {
try {
loading.value = true;
error.value = null;
const candidates =
locale.value === "en"
? [`/content/en/${path}.md`, `/content/${path}.md`]
: [`/content/${path}.md`];
const resolvedPath = candidates.find(
(candidate) => !!contentFiles[candidate],
);
if (!resolvedPath) {
throw new Error(`File not found (tried): ${candidates.join(", ")}`);
}
const rawContent = await contentFiles[resolvedPath]();
content.value = parseMarkdown(rawContent);
loading.value = false;
} catch (e) {
console.error("Error loading content:", e);
error.value = e;
loading.value = false;
}
};
onMounted(loadContent);
watch(locale, loadContent);
return { content, loading, error, reload: loadContent };
}
/**
* Composable per caricare lista di contenuti (es. tutti gli eventi)
*/
export function useContentList(pattern) {
const { locale } = useI18n();
const items = ref([]);
const loading = ref(true);
const error = ref(null);
const loadList = async () => {
try {
loading.value = true;
error.value = null;
// console.log("=== DEBUG useContentList ===");
// console.log("Pattern received:", pattern);
// console.log("All available content files:", Object.keys(contentFiles));
let basePath = pattern;
if (basePath.startsWith("/content/")) {
basePath = basePath.slice("/content/".length);
}
basePath = basePath.replace(/^\/+|\/+$/g, "");
const defaultDir = `/content/${basePath}`;
const localizedDir =
locale.value === "en" ? `/content/en/${basePath}` : defaultDir;
// console.log("Search path:", localizedDir);
const filesInDir = (dir) =>
Object.entries(contentFiles).filter(([fullPath]) => {
const pathDir = fullPath.substring(0, fullPath.lastIndexOf("/"));
const matches = pathDir === dir && !fullPath.endsWith("_index.md");
// if (matches) console.log("✓ Matched file:", fullPath);
return matches;
});
const localizedEntries = filesInDir(localizedDir);
let matchingFiles = localizedEntries;
if (locale.value === "en") {
const fallbackEntries = filesInDir(defaultDir);
const localizedSlugs = new Set(
localizedEntries.map(([fullPath]) =>
fullPath.split("/").pop().replace(".md", ""),
),
);
const fallbackOnlyEntries = fallbackEntries.filter(([fullPath]) => {
const slug = fullPath.split("/").pop().replace(".md", "");
return !localizedSlugs.has(slug);
});
matchingFiles = [...localizedEntries, ...fallbackOnlyEntries];
}
// console.log("Total matching files:", matchingFiles.length);
const contents = await Promise.all(
matchingFiles.map(async ([fullPath, loader]) => {
try {
// console.log("Loading file:", fullPath);
const raw = await loader();
const parsed = parseMarkdown(raw);
// console.log("Parsed:", {
// title: parsed.frontmatter.title,
// date: parsed.frontmatter.date,
// });
// Estrai slug dal path
const pathParts = fullPath.split("/");
const fileName = pathParts[pathParts.length - 1].replace(".md", "");
return {
path: fullPath,
slug: fileName,
...parsed,
};
} catch (e) {
console.error(`Error loading ${fullPath}:`, e);
return null;
}
}),
);
// Filtra i null e ordina per data
const validContents = contents.filter((c) => c !== null);
// console.log(
// "Valid contents:",
// validContents.length,
// validContents.map((c) => ({
// title: c.frontmatter.title,
// date: c.frontmatter.date,
// })),
// );
items.value = validContents.sort((a, b) => {
// Ordina dal più recente al meno recente
if (a.frontmatter?.date && b.frontmatter?.date) {
const dateA = new Date(a.frontmatter.date);
const dateB = new Date(b.frontmatter.date);
return dateB - dateA; // Ordine decrescente (più recente prima)
}
// Se manca la data, metti alla fine
if (!a.frontmatter?.date) return 1;
if (!b.frontmatter?.date) return -1;
return 0;
});
// console.log("Final items:", items.value.length);
loading.value = false;
} catch (e) {
console.error("Error loading content list:", e);
error.value = e;
loading.value = false;
}
};
onMounted(loadList);
watch(locale, loadList);
return { items, loading, error, reload: loadList };
}

View file

@ -0,0 +1,64 @@
import { useHead } from "@vueuse/head";
import { computed } from "vue";
/**
* Composable per gestire i meta tag OpenGraph
* @param {Object} options - Opzioni per i meta tag
* @param {string} options.title - Titolo della pagina
* @param {string} options.description - Descrizione della pagina
* @param {string} options.image - Path relativo dell'immagine OG (es: 'events/cavalieri')
* @param {string} options.url - URL completo della pagina
* @param {string} [options.type='website'] - Tipo di contenuto OG
*/
export function useMeta(options) {
const {
title,
description = "ADMStaff - gruppo di studenti di Informatica dell'Università di Bologna",
image,
url,
type = "website",
} = options;
// Genera il percorso completo dell'immagine OG
const ogImageUrl = computed(() => {
if (!image) return null;
// Se è già un URL completo, usalo così com'è
if (image.startsWith("http")) return image;
// Altrimenti costruisci il path dall'immagine generata
const baseUrl = window.location.origin;
const imagePath = image.startsWith("/") ? image : `/og/${image}.png`;
return `${baseUrl}${imagePath}`;
});
// Configura i meta tag
useHead({
title: computed(() => `${title} | ADMStaff`),
meta: [
// Description
{ name: "description", content: description },
// OpenGraph
{ property: "og:title", content: computed(() => `${title} | ADMStaff`) },
{ property: "og:description", content: description },
{ property: "og:type", content: type },
{ property: "og:url", content: url },
...(ogImageUrl.value
? [
{ property: "og:image", content: ogImageUrl.value },
{ property: "og:image:width", content: "1200" },
{ property: "og:image:height", content: "630" },
]
: []),
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: computed(() => `${title} | ADMStaff`) },
{ name: "twitter:description", content: description },
...(ogImageUrl.value
? [{ name: "twitter:image", content: ogImageUrl.value }]
: []),
],
});
}

15
src/i18n/index.js Normal file
View file

@ -0,0 +1,15 @@
import { createI18n } from "vue-i18n";
import it from "./locales/it.json";
import en from "./locales/en.json";
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem("locale") || "it",
fallbackLocale: "it",
Review

We can fall back to English, I think it's better. But we could also check the browser language (with navigator.language i think) and use that if it falls in the [it, en] set.

We can fall back to English, I think it's better. But we could also check the browser language (with `navigator.language` i think) and use that if it falls in the [it, en] set.
messages: {
it,
en,
},
});
export default i18n;

15
src/i18n/locales/en.json Normal file
View file

@ -0,0 +1,15 @@
{
"nav": {
"home": "Home",
"about": "About",
"projects": "Projects",
"events": "Events",
"wiki": "Wiki",
"media": "Media",
"lab": "Lab"
},
"footer": {
"copyright": "ADMStaff",
"tagline": "Services run by students, for students since 1994"
Review

Tbh i don't like the "since" part. ADM exist since 1994 (i don't remember this date), but it evolves every time and like this it seems that it almost stayed the same since 1994. But maybe it's just me.

Tbh i don't like the "_since_" part. ADM exist since 1994 (i don't remember this date), but it evolves every time and like this it seems that it almost stayed the same since 1994. But maybe it's just me.
}
}

15
src/i18n/locales/it.json Normal file
View file

@ -0,0 +1,15 @@
{
"nav": {
"home": "Home",
"about": "Chi Siamo",
"projects": "Progetti",
"events": "Eventi",
"wiki": "Wiki",
"media": "Media",
"lab": "Laboratorio"
},
"footer": {
"copyright": "ADMStaff",
"tagline": "Servizi da studenti, per studenti dal 1994"
}
}

36
src/main.js Normal file
View file

@ -0,0 +1,36 @@
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createHead } from "@vueuse/head";
import App from "./App.vue";
import i18n from "./i18n";
import "./style.css";
// Views
import HomeView from "./views/HomeView.vue";
import AboutView from "./views/AboutView.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: HomeView },
{ path: "/about", component: AboutView },
{ path: "/wiki", component: () => import("./views/WikiView.vue") },
{ path: "/wiki/:slug", component: () => import("./views/WikiView.vue") },
{ path: "/events", component: () => import("./views/EventsView.vue") },
{
path: "/events/:slug",
component: () => import("./views/EventDetailView.vue"),
},
{ path: "/projects", component: () => import("./views/ProjectsView.vue") },
{
path: "/projects/:slug",
component: () => import("./views/ProjectDetailView.vue"),
},
{ path: "/media", component: () => import("./views/MediaView.vue") },
{ path: "/lab", component: () => import("./views/LabView.vue") },
],
});
const head = createHead();
createApp(App).use(router).use(i18n).use(head).mount("#app");

203
src/style.css Normal file
View file

@ -0,0 +1,203 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap");
Review

Why do we import the fonts if we already have them as a static content?

Why do we import the fonts if we already have them as a static content?
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
/* Semantic tokens backed by DaisyUI theme colors */
--adm-color-primary: oklch(var(--p));
--adm-color-primary-contrast: oklch(var(--pc));
--adm-color-surface: oklch(var(--b1));
--adm-color-surface-muted: oklch(var(--b2));
--adm-color-border: oklch(var(--b3));
--adm-color-text: oklch(var(--bc));
--adm-color-text-muted: oklch(var(--bc) / 0.72);
--adm-color-neutral: oklch(var(--n));
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
font-family: "JetBrains Mono", monospace;
font-size: 1rem;
font-weight: 400;
font-feature-settings: "liga", "tnum", "case", "calt", "zero", "ss01", "locl";
line-height: 1.54;
background-color: var(--adm-color-surface);
color: var(--adm-color-text);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
-webkit-text-size-adjust: 100%;
}
/* Cursore lampeggiante stile block */
.blinking-cursor {
display: inline-block;
width: 0.625em;
height: 1em;
background: var(--adm-color-primary);
margin-left: 0.3em;
border-radius: 1px;
animation: cursor 1s infinite;
vertical-align: baseline;
transform: translateY(0.05em);
}
@keyframes cursor {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Stile per markdown content */
.markdown-content {
line-height: 1.7;
}
/* Nascondi frontmatter se mai dovesse apparire */
.markdown-content pre:first-child:has(code) {
display: none;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
color: var(--adm-color-primary);
margin: 1.5rem 0 1rem;
font-weight: bold;
}
.markdown-content h1 {
font-size: 2.25rem;
}
.markdown-content h2 {
font-size: 1.875rem;
}
.markdown-content h3 {
font-size: 1.5rem;
}
.markdown-content p {
margin: 1rem 0;
}
.markdown-content a {
color: var(--adm-color-primary);
text-decoration: underline;
}
.markdown-content a:hover {
opacity: 0.8;
}
.markdown-content ul,
.markdown-content ol {
margin: 1rem 0;
padding-left: 2rem;
}
.markdown-content ul {
list-style: disc;
}
.markdown-content ol {
list-style: decimal;
}
.markdown-content li {
margin: 0.5rem 0;
}
.markdown-content code {
background: var(--adm-color-surface-muted);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: "JetBrains Mono", monospace;
}
.markdown-content pre {
background: var(--adm-color-surface-muted);
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-content pre code {
background: transparent;
padding: 0;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 5px;
margin: 1.5rem auto;
display: block;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
.markdown-content table th,
.markdown-content table td {
border: 1px solid var(--adm-color-border);
padding: 0.75rem;
text-align: left;
}
.markdown-content table th {
background: var(--adm-color-surface-muted);
color: var(--adm-color-primary);
font-weight: bold;
}
.markdown-content table tr:hover {
background: var(--adm-color-neutral);
}
.markdown-content blockquote {
border-left: 4px solid var(--adm-color-primary);
padding-left: 1rem;
margin: 1.5rem 0;
color: var(--adm-color-text-muted);
font-style: italic;
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--adm-color-border);
margin: 2rem 0;
}
/* Utility per line-clamp */
.line-clamp-3 {
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

11
src/utils/assetPath.js Normal file
View file

@ -0,0 +1,11 @@
export function resolveAssetPath(url) {
if (!url) {
return "";
}
if (/^(?:[a-z][a-z0-9+.-]*:|\/|#)/i.test(url)) {
return url;
}
return `/${url}`;
}

151
src/utils/markdownParser.js Normal file
View file

@ -0,0 +1,151 @@
import { marked } from "marked";
// Configura marked per un output più pulito
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
mangle: false,
});
/**
* Parser YAML semplice per il frontmatter
* Supporta stringhe (con o senza quotes), numeri, booleani, date
*/
function parseYAMLFrontmatter(yamlContent) {
Review

We don't need a custom parser. There are already made parsers for the frontmatter, like: https://www.npmjs.com/package/gray-matter

We don't need a custom parser. There are already made parsers for the frontmatter, like: https://www.npmjs.com/package/gray-matter
const frontmatter = {};
const lines = yamlContent.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const colonIndex = trimmed.indexOf(":");
if (colonIndex === -1) continue;
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
// Rimuovi quotes singole o doppie
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.substring(1, value.length - 1);
}
// Converti booleani
if (value === "true") value = true;
else if (value === "false") value = false;
// Non tentare mai di convertire a numero se la stringa contiene caratteri non numerici
else if (/^-?\d+\.?\d*$/.test(value)) value = Number(value);
if (key === "cover") {
// console.log("Cover value parsed:", value, "type:", typeof value);
}
frontmatter[key] = value;
}
return frontmatter;
}
/**
* Parser markdown che gestisce il frontmatter YAML come Hugo
* e converte gli shortcode Hugo in HTML
*/
export function parseMarkdown(content) {
try {
// Normalizza i line endings a \n
const normalizedContent = content
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.trim();
let frontmatter = {};
let markdown = normalizedContent;
// Estrai frontmatter se presente
if (normalizedContent.startsWith("---")) {
const endIndex = normalizedContent.indexOf("\n---", 3);
if (endIndex !== -1) {
const yamlContent = normalizedContent.substring(3, endIndex).trim();
frontmatter = parseYAMLFrontmatter(yamlContent);
markdown = normalizedContent.substring(endIndex + 4).trim();
}
}
// Processa shortcode Hugo prima di convertire in HTML
const processedMarkdown = processHugoShortcodes(markdown);
const html = marked(processedMarkdown);
return {
frontmatter,
body: markdown,
html,
};
} catch (error) {
console.error("Error parsing markdown:", error);
// Fallback: rimuovi frontmatter manualmente
const cleanContent = content
.replace(/^---\r?\n[\s\S]*?\n---\r?\n/, "")
.trim();
return {
frontmatter: {},
body: cleanContent,
html: marked(cleanContent),
};
}
}
/**
* Processa gli shortcode Hugo e li converte in HTML/markdown compatibile
*/
function processHugoShortcodes(content) {
Review

Not super important but this regex are a bit too much, we could use a simple formatting schema or use template-language parser like jinja (as these are jinja style templates)

Not super important but this regex are a bit too much, we could use a simple formatting schema or use template-language parser like jinja (as these are jinja style templates)
// Gestisci figure dentro link markdown: [{{< figure ... >}}](url)
content = content.replace(
/\[\{\{<\s*figure\s+src="([^"]+)"(?:\s+title="([^"]+)")?(?:\s+style="([^"]+)")?\s*>\}\}\]\(([^)]+)\)/g,
(match, src, title, style, linkUrl) => {
const styleAttr = style ? ` style="${style}"` : "";
const altText = title || "";
return `<a href="${linkUrl}" target="_blank"><img src="${src}" alt="${altText}"${styleAttr} class="mx-auto" /></a>`;
},
);
// Gestisci figure standalone: {{< figure src="/img/..." title="..." style="..." >}}
content = content.replace(
/\{\{<\s*figure\s+src="([^"]+)"(?:\s+title="([^"]+)")?(?:\s+style="([^"]+)")?\s*>\}\}/g,
(match, src, title, style) => {
const styleAttr = style ? ` style="${style}"` : "";
const altText = title || "";
const caption = title ? `\n\n*${title}*` : "";
return `<img src="${src}" alt="${altText}"${styleAttr} class="mx-auto" />${caption}`;
},
);
return content;
}
export async function loadMarkdownFile(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to load ${path}`);
}
const text = await response.text();
return parseMarkdown(text);
} catch (error) {
console.error("Error loading markdown:", error);
return null;
}
}
export async function loadMarkdownFiles(paths) {
const results = await Promise.all(
paths.map(async (path) => {
const data = await loadMarkdownFile(path);
return { path, data };
}),
);
return results.filter((r) => r.data !== null);
}

28
src/views/AboutView.vue Normal file
View file

@ -0,0 +1,28 @@
<template>
<div class="mx-auto flex flex-col md:flex-row gap-8 items-start">
<img
src="/img/ADMstaff_logo-modern.png"
alt="ADMstaff logo"
class="md:w-96 md:sticky md:top-4 rounded-2xl"
/>
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Error loading content</span>
</div>
<div v-else class="markdown-content" v-html="content?.html"></div>
</div>
</template>
<script setup>
import { useContent } from "../composables/useContent.js";
// Carica il file about.md dalla directory content/
const { content, loading, error } = useContent("about");
</script>

View file

@ -0,0 +1,100 @@
<template>
<div class="max-w-4xl mx-auto">
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Event not found</span>
<div class="mt-4">
<RouterLink to="/events" class="text-primary hover:underline">
Back to Events
</RouterLink>
</div>
</div>
<article v-else>
<div class="mb-8">
<RouterLink
to="/events"
class="text-primary hover:underline mb-4 inline-block"
>
Back to Events
</RouterLink>
<img
v-if="content?.frontmatter.cover"
:src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title"
class="w-full h-auto object-contain rounded-lg mb-6"
/>
<h1 class="text-4xl font-bold text-primary mb-2">
{{ content?.frontmatter.title }}<span class="blinking-cursor"></span>
</h1>
<div class="flex gap-4 text-secondary mb-6">
<span
v-if="content?.frontmatter.date"
class="flex items-center gap-1"
>
<Icon icon="mdi:calendar" class="w-4 h-4" />
{{
new Date(content.frontmatter.date).toLocaleDateString("it-IT", {
year: "numeric",
month: "long",
day: "numeric",
})
}}
</span>
<span
v-if="content?.frontmatter.author"
class="flex items-center gap-1"
>
<Icon icon="mdi:account-edit" class="w-4 h-4" />
{{ content.frontmatter.author }}
</span>
</div>
</div>
<div class="markdown-content" v-html="content?.html"></div>
</article>
</div>
</template>
<script setup>
import { RouterLink, useRoute } from "vue-router";
import { useContent } from "../composables/useContent.js";
import { useMeta } from "../composables/useMeta.js";
import { computed, watch } from "vue";
import { Icon } from "@iconify/vue";
import { resolveAssetPath } from "../utils/assetPath.js";
const route = useRoute();
const slug = computed(() => route.params.slug);
const resolveCover = (cover) => resolveAssetPath(cover);
// Carica l'evento specifico basandosi sullo slug nella URL
const { content, loading, error } = useContent(`events/${slug.value}`);
// Configura meta tag OpenGraph quando il contenuto è caricato
watch(
() => content.value,
(newContent) => {
if (newContent?.frontmatter) {
useMeta({
title: newContent.frontmatter.title,
description:
newContent.frontmatter.description || newContent.frontmatter.title,
image: `events/${slug.value}`,
url: window.location.href,
type: "article",
});
}
},
{ immediate: true },
);
</script>

180
src/views/EventsView.vue Normal file
View file

@ -0,0 +1,180 @@
<template>
Review

Some text is not translated

Some text is not translated
<div class="min-h-screen bg-base-100 text-base-content">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="mb-12">
<h1 class="text-5xl font-bold text-primary mb-4">
Eventi<span class="blinking-cursor"></span>
</h1>
<p class="text-xl text-secondary">
Workshop, talk e incontri della nostra community
</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Error loading events</span>
</div>
<!-- Eventi in stile blog -->
<div v-else class="space-y-8">
<!-- Evento in evidenza (primo) -->
<article
v-if="items.length > 0"
class="bg-base-200 rounded-lg overflow-hidden border border-base-300 hover:border-primary transition-all"
>
<RouterLink :to="`/events/${items[0].slug}`">
<div class="grid md:grid-cols-2 gap-6">
<img
v-if="items[0].frontmatter.cover"
:src="resolveCover(items[0].frontmatter.cover)"
:alt="items[0].frontmatter.title"
class="w-full h-full object-cover"
/>
<div
:class="
items[0].frontmatter.cover ? 'p-8' : 'p-8 md:col-span-2'
"
class="flex flex-col justify-center"
>
<div
class="uppercase text-xs text-primary font-bold mb-2 flex items-center gap-1"
>
<Icon icon="mdi:pin" class="w-4 h-4" /> In Evidenza
</div>
<h2
class="text-3xl font-bold mb-4 text-primary hover:text-base-content transition"
>
{{ items[0].frontmatter.title }}
</h2>
<div class="flex gap-4 text-sm text-secondary mb-4">
<span
v-if="items[0].frontmatter.date"
class="flex items-center gap-1"
>
<Icon icon="mdi:calendar" class="w-4 h-4" />
{{ formatDate(items[0].frontmatter.date) }}
</span>
<span
v-if="items[0].frontmatter.location"
class="flex items-center gap-1"
>
<Icon icon="mdi:map-marker" class="w-4 h-4" />
{{ items[0].frontmatter.location }}
</span>
</div>
<div
class="text-base-content markdown-content line-clamp-3"
v-html="items[0].html"
></div>
<div class="mt-4 text-primary font-bold hover:underline">
Leggi di più
</div>
</div>
</div>
</RouterLink>
</article>
<!-- Altri eventi -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="event in items.slice(1)"
:key="event.path"
class="bg-base-200 rounded-lg overflow-hidden border border-base-300 hover:border-primary transition-all group"
>
<RouterLink :to="`/events/${event.slug}`" class="block">
<div
v-if="event.frontmatter.cover"
class="relative overflow-hidden h-48"
>
<img
:src="resolveCover(event.frontmatter.cover)"
:alt="event.frontmatter.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-6">
<h2
class="text-xl font-bold mb-2 text-primary group-hover:text-base-content transition"
>
{{ event.frontmatter.title }}
</h2>
<div class="flex flex-col gap-1 text-xs text-secondary mb-3">
<span
v-if="event.frontmatter.date"
class="flex items-center gap-1"
>
<Icon icon="mdi:calendar" class="w-3 h-3" />
{{ formatDate(event.frontmatter.date) }}
</span>
<span
v-if="event.frontmatter.location"
class="flex items-center gap-1"
>
<Icon icon="mdi:map-marker" class="w-3 h-3" />
{{ event.frontmatter.location }}
</span>
</div>
<div
class="text-sm text-base-content markdown-content line-clamp-3"
v-html="event.html"
></div>
</div>
</RouterLink>
</article>
</div>
<!-- Empty State -->
<div v-if="items.length === 0" class="text-center py-20">
<Icon
icon="mdi:calendar-blank"
class="w-16 h-16 mx-auto mb-4 text-secondary"
/>
<h3 class="text-2xl font-bold text-base-content mb-2">
Nessun evento trovato
</h3>
<p class="text-secondary">
Torna più tardi per vedere i prossimi eventi!
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js";
import { Icon } from "@iconify/vue";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica tutti gli eventi dalla directory content/events/
const { items, loading, error } = useContentList("/content/events/");
Review

We don't need the /content/ prefix

We don't need the `/content/` prefix
const resolveCover = (cover) => resolveAssetPath(cover);
// Formatta la data
function formatDate(date) {
return new Date(date).toLocaleDateString("it-IT", {
day: "numeric",
month: "long",
year: "numeric",
});
}
</script>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

148
src/views/HomeView.vue Normal file
View file

@ -0,0 +1,148 @@
<template>
<div>
<!-- About Section con cursore -->
<div
class="flex flex-col md:flex-row md:items-center md:mx-[5%] mb-12 md:my-12 md:gap-5"
>
<img
src="@/assets/ADMstaff_logo-modern-trasp.png"
alt="ADMStaff Logo"
class="md:w-1/3"
/>
<h4 id="about-text" class="md:text-xl">
ADMstaff nasce come gruppo che fornisce servizi da studenti per
studenti.<br /><br />
Il nostro obiettivo è sperimentare e fare pratica su ciò che ci
appassiona e ci incuriosisce. <br />Tutto ciò che stuzzica la curiosità
è alla nostra portata<span class="blinking-cursor"></span>
</h4>
</div>
<br />
<!-- Ultimi Eventi -->
<h1 class="mx-[5%] my-4 text-3xl font-bold text-primary md:my-8">
Ultimi Eventi
</h1>
<div v-if="eventsLoading" class="text-center py-8">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div
v-else
class="mx-[5%] grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"
>
<div
v-for="event in latestEvents"
:key="event.path"
class="overflow-hidden rounded-[20px] border border-transparent bg-base-200 p-2 transition-all duration-300 ease-in-out hover:-translate-y-1 hover:border-primary hover:shadow-[0_5px_15px_rgba(0,0,0,0.2)]"
>
<RouterLink
:to="`/events/${event.slug}`"
class="block text-inherit no-underline"
>
<img
v-if="event.frontmatter.cover"
:src="resolveCover(event.frontmatter.cover)"
:alt="event.frontmatter.title"
class="mb-4 w-full rounded-[10px]"
/>
<h2 class="mb-4 text-primary font-bold">
{{ event.frontmatter.title }}
</h2>
<div
class="text-base-content"
v-html="truncate(event.html, 100)"
></div>
</RouterLink>
</div>
</div>
<br />
<!-- I nostri progetti -->
<h1 class="mx-[5%] my-4 text-3xl font-bold text-primary md:my-8">
I nostri progetti
</h1>
<div v-if="projectsLoading" class="text-center py-8">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div
v-else
class="mx-[5%] grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 md:gap-5"
>
<div
v-for="project in projects"
:key="project.path"
class="overflow-hidden rounded-[20px] border border-transparent bg-base-200 p-2 transition-all duration-300 ease-in-out hover:-translate-y-1 hover:border-primary hover:shadow-[0_5px_15px_rgba(0,0,0,0.2)]"
>
<RouterLink
:to="`/projects/${project.slug}`"
class="block text-inherit no-underline"
>
<div
v-if="project.frontmatter.cover"
class="mb-4 aspect-[16/9] w-full overflow-hidden rounded-[10px] flex items-center justify-center"
>
<img
:src="resolveCover(project.frontmatter.cover)"
:alt="project.frontmatter.title"
class="block h-auto w-full max-h-full object-contain object-center my-auto"
/>
</div>
<h2 class="mb-4 text-primary font-bold">
{{ project.frontmatter.title }}
</h2>
<div
class="text-base-content"
v-html="truncate(project.html, 100)"
></div>
</RouterLink>
</div>
</div>
</div>
</template>
<script setup>
import { RouterLink } from "vue-router";
Review

onMounted and nextTick are not used

onMounted and nextTick are not used
import { useContentList } from "../composables/useContent.js";
import { computed, onMounted, nextTick } from "vue";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica eventi e progetti
Review

Don't need the /content/ prefix

Don't need the `/content/` prefix
const { items: allEvents, loading: eventsLoading } =
useContentList("/content/events/");
const { items: projects, loading: projectsLoading } =
useContentList("/content/projects/");
// Prendi solo gli ultimi 3 eventi
const latestEvents = computed(() => {
return allEvents.value.slice(0, 3);
});
// Funzione per troncare HTML mantenendo la formattazione
function truncate(html, length) {
Review

This is ugly :(

All the content is from us so should be safe, but i would not bet on this. We should be able to only use a parser without creating a div:

const doc = new DOMParser().parseFromString(html, "text/html");

We could also think about a sanitize just to be sure?

This is ugly :( All the content is from us so should be safe, but i would not bet on this. We should be able to only use a parser without creating a div: ```js const doc = new DOMParser().parseFromString(html, "text/html"); ``` We could also think about a sanitize just to be sure?
if (!html) return "";
const div = document.createElement("div");
div.innerHTML = html;
const text = div.textContent || div.innerText || "";
if (text.length <= length) {
return html;
}
// Tronca il testo e trova l'ultimo spazio
const truncated = text.substring(0, length);
const lastSpace = truncated.lastIndexOf(" ");
const finalText =
lastSpace > 0 ? truncated.substring(0, lastSpace) : truncated;
// Restituisci HTML semplice
return `<p>${finalText}...</p>`;
}
const resolveCover = (cover) => resolveAssetPath(cover);
</script>

38
src/views/LabView.vue Normal file
View file

@ -0,0 +1,38 @@
<template>
<div class="max-w-6xl mx-auto">
<h1 class="text-4xl text-primary mb-8">
Lab<span class="blinking-cursor"></span>
</h1>
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Error loading content</span>
</div>
<div v-else class="flex flex-col md:flex-row gap-8">
<div class="flex-1 markdown-content" v-html="content?.html"></div>
<img
v-if="content?.frontmatter.cover"
:src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title"
class="md:w-1/3 w-full h-auto object-contain rounded-lg"
/>
</div>
</div>
</template>
<script setup>
import { useContent } from "../composables/useContent.js";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica il file lab.md dalla directory content/
const { content, loading, error } = useContent("lab");
const resolveCover = (cover) => resolveAssetPath(cover);
</script>

26
src/views/MediaView.vue Normal file
View file

@ -0,0 +1,26 @@
<template>
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl text-primary mb-8">
Media<span class="blinking-cursor"></span>
</h1>
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Error loading content</span>
</div>
<div v-else class="markdown-content" v-html="content?.html"></div>
</div>
</template>
<script setup>
import { useContent } from "../composables/useContent.js";
// Carica il file wallpapers.md dalla directory content/media/
const { content, loading, error } = useContent("media/wallpapers");
</script>

View file

@ -0,0 +1,57 @@
<template>
<div class="max-w-4xl mx-auto">
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<div v-else-if="error" class="text-center py-12">
<span class="text-red-500">Project not found</span>
<div class="mt-4">
<RouterLink to="/projects" class="text-primary hover:underline">
Back to Projects
</RouterLink>
</div>
</div>
<article v-else>
<div class="mb-8">
<RouterLink
to="/projects"
class="text-primary hover:underline mb-4 inline-block"
>
Back to Projects
</RouterLink>
<img
v-if="content?.frontmatter.cover"
:src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title"
class="w-full h-auto max-h-[55vh] md:max-h-[65vh] rounded-lg object-contain mx-auto mb-6"
/>
<h1 class="text-4xl font-bold text-primary mb-6">
{{ content?.frontmatter.title }}<span class="blinking-cursor"></span>
</h1>
</div>
<div class="markdown-content" v-html="content?.html"></div>
</article>
</div>
</template>
<script setup>
import { RouterLink, useRoute } from "vue-router";
import { useContent } from "../composables/useContent.js";
import { computed } from "vue";
import { resolveAssetPath } from "../utils/assetPath.js";
const route = useRoute();
const slug = computed(() => route.params.slug);
const resolveCover = (cover) => resolveAssetPath(cover);
// Carica il progetto specifico basandosi sullo slug nella URL
const { content, loading, error } = useContent(`projects/${slug.value}`);
</script>

285
src/views/ProjectsView.vue Normal file
View file

@ -0,0 +1,285 @@
<template>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
<section class="mb-10 lg:mb-14 max-w-3xl">
<p class="text-xs uppercase tracking-[0.3em] text-secondary mb-3">
Selected work
</p>
<h1
class="text-4xl sm:text-5xl font-bold text-primary mb-4 leading-tight"
>
Projects<span class="blinking-cursor"></span>
</h1>
</section>
<div v-if="loading" class="space-y-8">
<div class="grid gap-6 lg:grid-cols-[1.25fr_0.75fr]">
<div
class="animate-pulse rounded-3xl border border-base-300 bg-base-200/70 p-6 lg:p-8 min-h-[22rem]"
>
<div class="h-4 w-24 rounded bg-base-300 mb-6"></div>
<div class="h-10 w-3/4 rounded bg-base-300 mb-4"></div>
<div class="h-4 w-full rounded bg-base-300 mb-3"></div>
<div class="h-4 w-5/6 rounded bg-base-300 mb-8"></div>
<div class="mt-auto h-56 rounded-2xl bg-base-300"></div>
</div>
<div class="space-y-4">
<div
v-for="index in 3"
:key="index"
class="animate-pulse rounded-3xl border border-base-300 bg-base-200/70 p-4"
>
<div class="aspect-[16/9] rounded-2xl bg-base-300 mb-4"></div>
<div class="h-5 w-2/3 rounded bg-base-300 mb-3"></div>
<div class="h-4 w-full rounded bg-base-300 mb-2"></div>
<div class="h-4 w-5/6 rounded bg-base-300"></div>
</div>
</div>
</div>
</div>
<div v-else-if="error" class="text-center py-12">
<div
class="max-w-md mx-auto rounded-3xl border border-base-300 bg-base-200 p-8"
>
<span class="text-red-500 block mb-4">Error loading projects</span>
<p class="text-base-content/75 mb-6">
Non riesco a caricare lelenco in questo momento.
</p>
<button
type="button"
class="inline-flex items-center justify-center rounded-full border border-primary px-5 py-2 text-primary hover:bg-primary hover:text-primary-content transition-colors"
@click="reload"
>
Riprova
</button>
</div>
</div>
<div v-else class="space-y-8 lg:space-y-10">
<article
v-if="featuredProject"
class="group overflow-hidden rounded-[2rem] border border-base-300 bg-base-200/80 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:border-primary/40"
>
<div class="grid lg:grid-cols-[1.2fr_0.8fr] min-h-[20rem]">
<div
class="order-2 lg:order-1 p-6 sm:p-8 lg:p-10 flex flex-col justify-between gap-6"
>
<div>
<p
class="text-xs uppercase tracking-[0.28em] text-secondary mb-4"
>
Featured project
</p>
<h2
class="text-3xl sm:text-4xl font-bold text-primary mb-4 leading-tight group-hover:text-base-content transition-colors"
>
{{ featuredProject.frontmatter.title }}
</h2>
<div
class="max-w-2xl text-base-content/80 leading-relaxed line-clamp-4"
v-html="getExcerpt(featuredProject.html, 220)"
></div>
</div>
<div class="flex flex-wrap items-center gap-3 text-sm">
<RouterLink
:to="`/projects/${featuredProject.slug}`"
class="inline-flex items-center rounded-full border border-primary/50 px-4 py-2 text-primary hover:bg-primary hover:text-primary-content transition-colors"
>
Read more
</RouterLink>
<a
v-if="featuredServiceLink"
:href="featuredServiceLink"
:target="isExternalLink(featuredServiceLink) ? '_blank' : null"
:rel="
isExternalLink(featuredServiceLink)
? 'noopener noreferrer'
: null
"
class="inline-flex items-center rounded-full border border-base-300 bg-base-100/80 px-4 py-2 text-base-content/80 hover:border-primary/50 hover:text-primary transition-colors"
>
Vai al servizio
</a>
</div>
</div>
<div class="order-1 lg:order-2 bg-base-300/30">
<div
v-if="featuredProject.frontmatter.cover"
class="flex h-full min-h-[12rem] lg:min-h-[20rem] items-center justify-center overflow-hidden p-6 sm:p-8"
>
<img
:src="resolveCover(featuredProject.frontmatter.cover)"
:alt="featuredProject.frontmatter.title"
class="h-auto max-h-[10rem] w-full max-w-[20rem] object-contain transition-transform duration-500 group-hover:scale-[1.03]"
/>
</div>
<div
v-else
class="flex h-full min-h-[12rem] lg:min-h-[20rem] items-center justify-center p-8 text-secondary"
>
No cover available
</div>
</div>
</div>
</article>
<div
v-if="items.length > 1"
class="grid gap-5 md:grid-cols-2 xl:grid-cols-3"
>
<article
v-for="project in items.slice(1)"
:key="project.path"
class="group overflow-hidden rounded-[1.75rem] border border-base-300 bg-base-200/70 transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg"
>
<div class="aspect-[16/10] overflow-hidden bg-base-300/30 p-4">
<img
v-if="project.frontmatter.cover"
:src="resolveCover(project.frontmatter.cover)"
:alt="project.frontmatter.title"
class="h-full w-full object-contain transition-transform duration-500 group-hover:scale-[1.03]"
/>
<div
v-else
class="flex h-full items-center justify-center text-secondary"
>
No cover available
</div>
</div>
<div class="p-5 sm:p-6">
<h2
class="text-xl sm:text-2xl font-bold text-primary mb-3 leading-snug group-hover:text-base-content transition-colors"
>
{{ project.frontmatter.title }}
</h2>
<div
class="text-base-content/75 line-clamp-3"
v-html="getExcerpt(project.html, 140)"
></div>
<div class="mt-5 flex flex-wrap items-center gap-2 text-sm">
<RouterLink
:to="`/projects/${project.slug}`"
class="inline-flex items-center rounded-full border border-primary/50 px-3 py-1.5 font-medium text-primary hover:bg-primary hover:text-primary-content transition-colors"
>
Read more
</RouterLink>
<a
v-if="getProjectLink(project)"
:href="getProjectLink(project)"
:target="
isExternalLink(getProjectLink(project)) ? '_blank' : null
"
:rel="
isExternalLink(getProjectLink(project))
? 'noopener noreferrer'
: null
"
class="inline-flex items-center rounded-full border border-base-300 bg-base-100/80 px-3 py-1.5 font-medium text-base-content/80 hover:border-primary/50 hover:text-primary transition-colors"
>
Review

Same story of HomeView for the parser and sanitize

Same story of HomeView for the parser and sanitize
Vai al servizio
</a>
</div>
</div>
</article>
</div>
<div
v-else-if="items.length === 1"
class="rounded-3xl border border-dashed border-base-300 bg-base-200/50 p-8 text-secondary"
>
Solo un progetto disponibile per ora.
</div>
<div
v-else
class="rounded-3xl border border-dashed border-base-300 bg-base-200/50 p-8 text-secondary"
>
Nessun progetto trovato.
</div>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica tutti i progetti dalla directory content/projects/
const { items, loading, error, reload } = useContentList("/content/projects/");
const resolveCover = (cover) => resolveAssetPath(cover);
const featuredProject = computed(() => items.value[0] ?? null);
const featuredServiceLink = computed(() =>
getProjectLink(featuredProject.value),
);
function getExcerpt(html, length) {
if (!html) return "";
const div = document.createElement("div");
div.innerHTML = html;
const text = (div.textContent || div.innerText || "")
.replace(/\s+/g, " ")
.trim();
if (text.length <= length) {
return `<p>${text}</p>`;
}
const shortened = text.substring(0, length);
const lastSpace = shortened.lastIndexOf(" ");
const finalText =
lastSpace > 0 ? shortened.substring(0, lastSpace) : shortened;
return `<p>${finalText}...</p>`;
}
function getProjectLink(project) {
if (!project) return "";
const frontmatter = project.frontmatter || {};
const explicitLink =
frontmatter.serviceUrl ||
frontmatter.service_url ||
frontmatter.url ||
frontmatter.link ||
frontmatter.website ||
frontmatter.external;
if (typeof explicitLink === "string" && explicitLink.trim()) {
return explicitLink.trim();
}
const html = project.html || "";
const firstHref = html.match(/href="([^"]+)"/i);
return firstHref?.[1] || "";
}
function isExternalLink(link) {
return /^https?:\/\//i.test(link || "");
}
</script>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-4 {
display: -webkit-box;
line-clamp: 4;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

608
src/views/WikiView.vue Normal file
View file

@ -0,0 +1,608 @@
<template>
<div class="min-h-screen bg-base-100 text-base-content">
<div class="container mx-auto px-4 py-8">
<div class="mb-4 md:hidden">
<button
class="btn h-auto w-full justify-between border-base-300 bg-base-200 px-4 py-3 text-left normal-case shadow-sm hover:bg-base-300"
type="button"
@click="toggleMobileSidebar"
:aria-expanded="isMobileSidebarOpen"
aria-controls="wiki-sidebar-panel"
>
<span class="flex flex-col items-start">
<span class="text-sm font-semibold text-base-content"
>Sommario e Indice pagina</span
>
<span class="text-xs text-base-content/70">{{
mobileSidebarSummary
}}</span>
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
class="h-4 w-4 text-base-content/70 transition-transform"
:class="{ 'rotate-180': isMobileSidebarOpen }"
>
<path
d="M6 9l6 6 6-6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-12 md:gap-8">
<div
v-if="isMobileSidebarOpen"
class="fixed inset-0 z-40 bg-black/40 md:hidden"
@click="closeMobileSidebar"
></div>
<!-- Sidebar con navigazione pagine wiki -->
<aside
id="wiki-sidebar-panel"
:class="sidebarPanelClasses"
aria-label="Wiki sidebar"
>
<div class="mb-4 flex items-center justify-between md:hidden">
<span
class="text-sm font-semibold uppercase tracking-wide text-primary"
>Wiki menu</span
>
<button
Review

An icon would look better :)

An icon would look better :)
class="btn btn-xs btn-ghost"
type="button"
@click="closeMobileSidebar"
>
X
</button>
</div>
<nav class="rounded-lg">
<!-- Lista pagine dalla sidebar -->
<div class="space-y-2 text-sm">
<div v-for="(item, index) in sidebarItems" :key="index">
<!-- Heading di sezione -->
<h3
v-if="item.type === 'heading'"
class="text-xs font-bold text-secondary uppercase mt-4 mb-2"
>
{{ item.text }}
</h3>
<!-- Link a pagina -->
<a
v-else-if="item.type === 'link'"
:href="item.isExternal ? item.url : '#'"
:target="item.isExternal ? '_blank' : null"
:rel="item.isExternal ? 'noopener noreferrer' : null"
@click="onSidebarLinkClick($event, item)"
class="block min-h-11 rounded-lg px-3 py-2.5 transition hover:bg-base-300/20 md:min-h-0 md:py-1"
:class="{
'bg-base-300/20 text-primary font-bold':
!item.isExternal && currentPage === item.slug,
}"
:title="item.text"
>
{{ item.text
}}<span
v-if="item.isExternal"
class="ml-1 text-base-content/60"
></span
>
</a>
<!-- TOC (Table of Contents) - Outline minimal terminal style -->
<nav
v-if="currentPage === item.slug && outline.length > 0"
class="mt-2"
>
<ul
class="space-y-0 text-xs font-mono leading-snug border-l border-base-300 pl-2"
>
<li v-for="heading in outline" :key="heading.id">
<a
:href="`#${heading.id}`"
@click.prevent="scrollToHeading(heading.id)"
class="block rounded-md py-1.5 pr-2 transition hover:text-base-content"
:style="{
paddingLeft: `${(heading.level - 1) * 0.5}rem`,
}"
:class="{
'text-primary bg-base-300/20':
activeHeading === heading.id,
'text-base-content/60': activeHeading !== heading.id,
}"
:title="heading.text"
>
{{ heading.text }}
</a>
</li>
</ul>
</nav>
</div>
</div>
</nav>
</aside>
<!-- Contenuto principale -->
<main class="md:col-span-8 lg:col-span-9 xl:col-span-10">
<div v-if="loading" class="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
</div>
<article
v-else
class="bg-base-200 rounded-lg border border-base-300 p-4 sm:p-6 lg:p-8"
>
<h1
class="mb-5 text-2xl font-bold text-primary sm:text-3xl lg:mb-6 lg:text-4xl"
>
{{ pageTitle }}<span class="blinking-cursor"></span>
</h1>
<div
class="markdown-content prose prose-invert max-w-none text-sm md:text-base"
v-html="pageContent"
></div>
</article>
</main>
</div>
</div>
<button
class="btn btn-circle btn-secondary fixed bottom-4 right-4 z-30 text-3xl font-bold sm:bottom-6 sm:right-6"
:class="{ hidden: !showScrollTopButton }"
@click="scrollToTop"
Outdated
Review

All the logic in this file is a bit meh. But maybe with markdown render generation at compile time could be better

All the logic in this file is a bit meh. But maybe with markdown render generation at compile time could be better
>
</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { marked } from "marked";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const MOBILE_BREAKPOINT_QUERY = "(max-width: 767px)";
const MOBILE_HEADING_THRESHOLD = 84;
const DESKTOP_HEADING_THRESHOLD = 100;
const WIKI_CONTENT_BASE_PATH = "/content/wiki/adm.wiki";
const WIKI_SIDEBAR_PATH = `${WIKI_CONTENT_BASE_PATH}/_Sidebar.md?raw`;
const loading = ref(true);
const pageContent = ref("");
const pageTitle = ref("Wiki ADM");
const currentPage = ref("home");
const sidebarItems = ref([]);
const outline = ref([]);
const activeHeading = ref("");
const showScrollTopButton = ref(false);
const isMobileSidebarOpen = ref(false);
const sidebarPageCount = computed(
() =>
sidebarItems.value.filter(
(item) => item.type === "link" && !item.isExternal,
).length,
);
Review

We only use this once in the html, why use a compute?

We only use this once in the html, why use a compute?
const mobileSidebarSummary = computed(() => {
const pages = sidebarPageCount.value;
const sections = outline.value.length;
return `${pages} pagine · ${sections} sezioni`;
});
const sidebarPanelClasses = computed(() => [
"md:col-span-4 lg:col-span-3 xl:col-span-2 md:sticky md:top-4 md:self-start",
"fixed left-4 right-4 top-4 z-50 max-h-[calc(100vh-2rem)] overflow-y-auto rounded-lg border border-base-300 bg-base-100/95 p-4 shadow-lg backdrop-blur transition-transform duration-200 ease-out md:static md:z-auto md:max-h-none md:overflow-visible md:rounded-none md:border-0 md:bg-transparent md:p-0 md:shadow-none md:backdrop-blur-none",
isMobileSidebarOpen.value
? "translate-x-0"
: "-translate-x-[110%] md:translate-x-0",
]);
function toHeadingId(text) {
return text.toLowerCase().replace(/[^\w]+/g, "-");
}
function toWikiSlug(value) {
return value.replace(/^\.\//, "").replace(/\/$/, "").toLowerCase();
}
function toTitleCaseSlug(value) {
return value
.split("-")
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join("-");
}
function buildPossibleWikiPaths(slug) {
return [
`${WIKI_CONTENT_BASE_PATH}/${slug}.md`,
Review

What's the point of this? The exclusion should be server-side with the python script, not client side

What's the point of this? The exclusion should be server-side with the python script, not client side
`${WIKI_CONTENT_BASE_PATH}/${slug.charAt(0).toUpperCase() + slug.slice(1)}.md`,
`${WIKI_CONTENT_BASE_PATH}/${toTitleCaseSlug(slug)}.md`,
];
}
function parseSidebarLine(line) {
if (line.includes("<!-- NOT INCLUDE -->") || line.includes("<!--")) {
return null;
}
if (line.startsWith("###") || line.startsWith("- ###")) {
return {
type: "heading",
text: line
.replace(/^-\s*/, "")
.replace(/^###\s*/, "")
.trim(),
};
}
if (line.includes("](")) {
const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (!match) return null;
const text = match[1];
const url = match[2];
const isExternal = /^https?:\/\//i.test(url);
return {
type: "link",
text,
slug: toWikiSlug(url),
isExternal,
url,
};
}
if (line.includes("[[")) {
const match = line.match(/\[\[([^\]]+)\]\]/);
if (!match) return null;
return {
type: "link",
text: match[1],
Review

All of this is very ugly tbh. Can we do it at compile time like the og photos?

All of this is very ugly tbh. Can we do it at compile time like the og photos?
slug: toWikiSlug(match[1]),
isExternal: false,
url: "#",
};
}
return null;
}
function createMarkdownRenderer() {
const renderer = new marked.Renderer();
renderer.heading = function (text, level) {
const id = toHeadingId(text);
return `<h${level} id="${id}" href="#${id}" >${text}</h${level}>`;
};
renderer.image = function (href, title, text) {
let src = href;
if (!href.startsWith("http") && !href.startsWith("/")) {
src = `${WIKI_CONTENT_BASE_PATH}/${href}`;
}
const titleAttr = title ? ` title="${title}"` : "";
return `<img src="${src}" alt="${text}"${titleAttr} class="max-w-full h-auto" />`;
};
renderer.code = function (code, language) {
const lang = language || "text";
const escapedCode = code.replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `<div class="code-block-wrapper">
<div class="code-block-header">
<span class="code-block-lang">${lang}</span>
<button class="code-block-copy" onclick="copyCodeToClipboard(this)" title="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<pre><code class="language-${lang}">${escapedCode}</code></pre>
</div>`;
};
return renderer;
}
function copyCodeToClipboard(button) {
const wrapper = button?.closest(".code-block-wrapper");
const codeElement = wrapper?.querySelector("code");
if (!codeElement) return;
navigator.clipboard
.writeText(codeElement.textContent)
.then(() => {
const originalHTML = button.innerHTML;
button.innerHTML =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
button.style.color = "var(--adm-primary)";
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = "";
}, 2000);
})
.catch((err) => {
console.error("Failed to copy:", err);
});
}
function isMobileViewport() {
return window.matchMedia(MOBILE_BREAKPOINT_QUERY).matches;
}
function closeMobileSidebar() {
isMobileSidebarOpen.value = false;
}
function toggleMobileSidebar() {
isMobileSidebarOpen.value = !isMobileSidebarOpen.value;
}
function updateBodyScrollLock() {
if (!isMobileViewport()) {
document.body.classList.remove("overflow-hidden");
return;
}
document.body.classList.toggle("overflow-hidden", isMobileSidebarOpen.value);
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
showScrollTopButton.value = false;
}
function onSidebarLinkClick(event, item) {
if (item.isExternal) return;
event.preventDefault();
closeMobileSidebar();
loadPage(item.slug);
}
// Carica il contenuto della sidebar
async function loadSidebar() {
try {
const sidebarModule = await import(WIKI_SIDEBAR_PATH);
const sidebarContent = sidebarModule.default;
const lines = sidebarContent.split("\n").filter((line) => line.trim());
sidebarItems.value = lines.map(parseSidebarLine).filter(Boolean);
} catch (error) {
console.error("Error loading sidebar:", error);
}
}
// Carica una pagina wiki
async function loadPage(slug) {
const normalizedSlug = toWikiSlug(slug);
loading.value = true;
currentPage.value = normalizedSlug;
// Aggiorna URL senza ricaricare
router.push(`/wiki/${normalizedSlug}`);
try {
const possiblePaths = buildPossibleWikiPaths(normalizedSlug);
let content = null;
for (const path of possiblePaths) {
try {
const module = await import(/* @vite-ignore */ path + "?raw");
content = module.default;
break;
} catch (err) {
// Continua a provare
}
}
if (!content) {
throw new Error(`Page not found: ${normalizedSlug}`);
}
// Estrai titolo
const titleMatch = content.match(/^#\s+(.+)$/m);
pageTitle.value = titleMatch?.[1] || normalizedSlug.replace(/-/g, " ");
// Rimuovi solo il primo H1 iniziale (se presente)
const contentWithoutMainTitle = content.replace(/^\s*#\s+.+(?:\r?\n)+/, "");
const renderer = createMarkdownRenderer();
pageContent.value = marked(contentWithoutMainTitle, { renderer });
extractOutline(contentWithoutMainTitle);
} catch (error) {
console.error("Error loading page:", error);
pageContent.value = `<p class="text-red-500">Page not found: ${normalizedSlug}</p>`;
pageTitle.value = "Error";
outline.value = [];
} finally {
loading.value = false;
}
}
// Estrai i capitoli (headings) per l'outline
function extractOutline(markdown) {
const headings = [];
const lines = markdown.split("\n");
for (const line of lines) {
const match = line.match(/^(#{1,5})\s+(.+)$/);
if (match) {
const level = match[1].length;
const text = match[2];
const id = toHeadingId(text);
headings.push({ level, text, id });
}
}
outline.value = headings;
}
// Scroll a un heading
function scrollToHeading(id) {
activeHeading.value = id;
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
closeMobileSidebar();
}
}
// Rileva heading attivo durante lo scroll
function handleScroll() {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
showScrollTopButton.value = scrollTop > 300;
if (outline.value.length === 0) {
activeHeading.value = "";
return;
}
const threshold = isMobileViewport()
? MOBILE_HEADING_THRESHOLD
: DESKTOP_HEADING_THRESHOLD;
for (const heading of outline.value) {
const element = document.getElementById(heading.id);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= threshold && rect.bottom >= threshold) {
activeHeading.value = heading.id;
break;
}
}
}
}
onMounted(async () => {
window.copyCodeToClipboard = copyCodeToClipboard;
await loadSidebar();
// Carica la pagina dall'URL o Home come default
const slug = route.params.slug || "home";
await loadPage(slug);
window.addEventListener("scroll", handleScroll);
window.addEventListener("resize", updateBodyScrollLock);
handleScroll();
updateBodyScrollLock();
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", updateBodyScrollLock);
document.body.classList.remove("overflow-hidden");
delete window.copyCodeToClipboard;
});
watch(
() => route.params.slug,
async (slug) => {
const normalizedSlug = typeof slug === "string" ? slug : "home";
if (normalizedSlug !== currentPage.value) {
await loadPage(normalizedSlug);
}
closeMobileSidebar();
},
);
watch(isMobileSidebarOpen, () => {
updateBodyScrollLock();
});
</script>
<style scoped>
.prose :deep(h2),
.prose :deep(h3),
.prose :deep(h4) {
scroll-margin-top: 4rem;
}
@media (min-width: 768px) {
.prose :deep(h2),
.prose :deep(h3),
.prose :deep(h4) {
scroll-margin-top: 5rem;
}
}
/* Code blocks styling */
.markdown-content :deep(.code-block-wrapper) {
margin: 1.5rem 0;
border: 1px solid var(--adm-border);
border-radius: 8px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
}
.markdown-content :deep(.code-block-header) {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--adm-border);
font-size: 0.75rem;
}
.markdown-content :deep(.code-block-lang) {
color: var(--adm-text-secondary);
font-family: "JetBrains Mono", monospace;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.markdown-content :deep(.code-block-copy) {
background: transparent;
border: none;
color: var(--adm-text-secondary);
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
border-radius: 4px;
transition: all 0.2s;
}
.markdown-content :deep(.code-block-copy:hover) {
color: var(--adm-primary);
background: rgba(97, 131, 117, 0.1);
}
.markdown-content :deep(.code-block-wrapper pre) {
margin: 0;
padding: 1rem;
overflow-x: auto;
background: transparent;
}
.markdown-content :deep(.code-block-wrapper code) {
font-family: "JetBrains Mono", monospace;
font-size: 0.875rem;
line-height: 1.6;
color: #e0e0e0;
}
@media (max-width: 767px) {
.markdown-content :deep(.code-block-header) {
padding: 0.5rem 0.75rem;
}
.markdown-content :deep(table) {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
</style>

10
static/fonts/AUTHORS.txt Normal file
View file

@ -0,0 +1,10 @@
# This is the official list of project authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS.txt file.
# See the latter for an explanation.
#
# Names should be added to this file as:
# Name or Organization <email address>
Review

Do we need to fill this file?

Do we need to fill this file?
JetBrains <>
Philipp Nurullin <philipp.nurullin@jetbrains.com>
Konstantin Bulenkov <kb@jetbrains.com>

Binary file not shown.

93
static/fonts/OFL.txt Normal file
View file

@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
Review

Do we need the license in the repo?

Do we need the license in the repo?
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more