New Website in VueJS #6
30
.gitignore
vendored
|
|
@ -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
|
|
@ -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
|
|
||||||
|
|
|
||||||
71
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
---
|
---
|
||||||
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
|
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||||
|
|
|||||||
date: {{ .Date }}
|
date: { { .Date } }
|
||||||
author: 'ADMStaff'
|
author: "ADMStaff"
|
||||||
toc: false
|
toc: false
|
||||||
framed: false
|
framed: false
|
||||||
# cover:
|
# cover:
|
||||||
|
# link: # per i progetti
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
We’re looking for curious and motivated students. Your experience level doesn’t matter: what counts is the desire to learn and get involved. Drop by the lab or check out our [events](/events).
|
We’re looking for curious and motivated students. Your experience level doesn’t matter: what counts is the desire to learn and get involved. Drop by the lab or check out our [events](/events).
|
||||||
|
|
||||||
If you’re interested, write to us on Telegram: [t.me/admstaff_Chat](https://t.me/admstaff_Chat)
|
If you’re interested, write to us on Telegram: [t.me/admstaff_Chat](https://t.me/admstaff_Chat)
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
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:
|
||||||
|
|
@ -27,7 +27,6 @@ Anche per l'anno accademico 2025/26 organizziamo i laboratori fra pari: degli in
|
||||||
- **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).
|
||||||
|
|
||||||
Tutti gli incontri successivi si terranno di **giovedì** dalle **16:00 alle 18:00** in **Aula E2** (Dipartimento di Informatica, via Mura Anteo Zamboni 2B).
|
Tutti gli incontri successivi si terranno di **giovedì** dalle **16:00 alle 18:00** in **Aula E2** (Dipartimento di Informatica, via Mura Anteo Zamboni 2B).
|
||||||
|
|
|
||||||
|
|
@ -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 nell’installazione di Linux sul tuo PC. Sarà l’occasione 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 nell’installazione di Linux sul tuo PC. Sarà l’occasione 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
|
||||||
|
|
|
||||||
|
|
@ -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 nell’installazione di Linux sul tuo PC. Sarà l’occasione 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 nell’installazione di Linux sul tuo PC. Sarà l’occasione 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:
|
||||||

|

|
||||||

|

|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
Here’s a brief overview of his life and contributions:
|
Here’s 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**: Stallman’s 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 Computing’s Grace Murray Hopper Award.
|
- **Inspirations**: Stallman’s 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 Computing’s 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”.
|
||||||
|
|
|
||||||
|
|
@ -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**
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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).
|
||||||
87
hugo.toml
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
@ -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>
|
|
||||||
{{ 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 }}
|
|
||||||
|
|
@ -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 }}
|
|
||||||
|
|
@ -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>
|
|
||||||
{{ 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
2490
package-lock.json
generated
Normal file
32
package.json
Normal 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
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ignoredBuiltDependencies:
|
||||||
|
- canvas
|
||||||
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/img/laboratori-25-26.png
(Stored with Git LFS)
|
|
@ -1 +0,0 @@
|
||||||
{"Target":"css/style.css","MediaType":"text/css","Data":{}}
|
|
||||||
180
scripts/generate-og-images.js
Normal 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);
|
||||||
|
samu
commented
We could add 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) {
|
||||||
|
samu
commented
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() {
|
||||||
|
samu
commented
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");
|
||||||
|
samu
commented
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 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, "/");
|
||||||
|
samu
commented
It's longer but for path normalization should be better to use:
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) {
|
||||||
|
samu
commented
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:")) {
|
||||||
|
samu
commented
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
|
|
@ -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");
|
||||||
|
samu
commented
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
|
|
@ -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
20
src/components/BlinkingLogo.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
samu
commented
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
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
samu
commented
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>
|
||||||
|
samu
commented
For the copyright symbol you can use For the copyright symbol you can use `©`
|
|||||||
|
<p class="mt-2">{{ $t("footer.tagline") }}</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
samu
commented
We don't use this We don't use this
|
|||||||
|
</script>
|
||||||
138
src/components/Header.vue
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
fabio
commented
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
alice
commented
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>
|
||||||
|
samu
commented
I would also add 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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
samu
commented
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>© {{ 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>
|
||||||
179
src/composables/useContent.js
Normal 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/**"],
|
||||||
|
samu
commented
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 };
|
||||||
|
}
|
||||||
64
src/composables/useMeta.js
Normal 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
|
|
@ -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",
|
||||||
|
samu
commented
We can fall back to English, I think it's better. But we could also check the browser language (with 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
|
|
@ -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"
|
||||||
|
samu
commented
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
|
|
@ -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
|
|
@ -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
|
|
@ -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");
|
||||||
|
samu
commented
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
|
|
@ -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
|
|
@ -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) {
|
||||||
|
samu
commented
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) {
|
||||||
|
samu
commented
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
|
|
@ -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>
|
||||||
100
src/views/EventDetailView.vue
Normal 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
|
|
@ -0,0 +1,180 @@
|
||||||
|
<template>
|
||||||
|
samu
commented
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/");
|
||||||
|
samu
commented
We don't need the 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
|
|
@ -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";
|
||||||
|
samu
commented
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
|
||||||
|
samu
commented
Don't need the 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) {
|
||||||
|
samu
commented
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: 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
|
|
@ -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
|
|
@ -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>
|
||||||
57
src/views/ProjectDetailView.vue
Normal 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
|
|
@ -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 l’elenco 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"
|
||||||
|
>
|
||||||
|
samu
commented
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
|
|
@ -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
|
||||||
|
samu
commented
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"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</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,
|
||||||
|
);
|
||||||
|
|
||||||
|
samu
commented
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`,
|
||||||
|
samu
commented
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],
|
||||||
|
samu
commented
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, "<").replace(/>/g, ">");
|
||||||
|
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
|
|
@ -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>
|
||||||
|
samu
commented
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>
|
||||||
BIN
static/fonts/JetBrainsMono-Medium.ttf
Normal file
93
static/fonts/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||||
|
samu
commented
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.
|
||||||
Do we need the brackets? We are not parsing this as a jinja template...