New Website in VueJS #6

Open
alice wants to merge 11 commits from new-website into main
11 changed files with 263 additions and 33 deletions
Showing only changes of commit c57cafee28 - Show all commits

View file

@ -5,4 +5,5 @@ author: "ADMStaff"
toc: false
framed: false
# cover:
# link: # per i progetti
---

View file

@ -5,6 +5,7 @@ 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.

View file

@ -4,6 +4,7 @@ date: 2021-11-23T18:00:00+02:00
author: "Admstaff"
draft: false
toc: false
link: "https://git.students.cs.unibo.it"
cover: "img/git-forgejo.png"
---

View file

@ -4,6 +4,7 @@ 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"
---

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

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

View file

@ -26,7 +26,7 @@
<img
v-if="content?.frontmatter.cover"
:src="'/' + content.frontmatter.cover"
:src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title"
class="w-full h-auto object-contain rounded-lg mb-6"
/>
@ -70,10 +70,13 @@ 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}`);

View file

@ -34,7 +34,7 @@
<div class="grid md:grid-cols-2 gap-6">
<img
v-if="items[0].frontmatter.cover"
:src="'/' + items[0].frontmatter.cover"
:src="resolveCover(items[0].frontmatter.cover)"
:alt="items[0].frontmatter.title"
class="w-full h-full object-cover"
/>
@ -95,7 +95,7 @@
class="relative overflow-hidden h-48"
>
<img
:src="'/' + event.frontmatter.cover"
:src="resolveCover(event.frontmatter.cover)"
:alt="event.frontmatter.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
@ -153,10 +153,13 @@
import { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js";
import { Icon } from "@iconify/vue";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica tutti gli eventi dalla directory content/events/
const { items, loading, error } = useContentList("/content/events/");
Review

We don't need the /content/ prefix

We don't need the `/content/` prefix
const resolveCover = (cover) => resolveAssetPath(cover);
// Formatta la data
function formatDate(date) {
return new Date(date).toLocaleDateString("it-IT", {

View file

@ -42,7 +42,7 @@
>
<img
v-if="event.frontmatter.cover"
:src="'/' + event.frontmatter.cover"
:src="resolveCover(event.frontmatter.cover)"
:alt="event.frontmatter.title"
class="mb-4 w-full rounded-[10px]"
/>
@ -86,7 +86,7 @@
class="mb-4 aspect-[16/9] w-full overflow-hidden rounded-[10px] flex items-center justify-center"
>
<img
:src="project.frontmatter.cover"
:src="resolveCover(project.frontmatter.cover)"
:alt="project.frontmatter.title"
class="block h-auto w-full max-h-full object-contain object-center my-auto"
/>
@ -108,6 +108,7 @@
import { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js";
import { computed, onMounted, nextTick } from "vue";
Review

onMounted and nextTick are not used

onMounted and nextTick are not used
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica eventi e progetti
const { items: allEvents, loading: eventsLoading } =
@ -140,4 +141,6 @@ function truncate(html, length) {
// Restituisci HTML semplice
return `<p>${finalText}...</p>`;
}
const resolveCover = (cover) => resolveAssetPath(cover);
</script>

View file

@ -19,7 +19,7 @@
<img
v-if="content?.frontmatter.cover"
:src="'/' + 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"
/>
@ -29,7 +29,10 @@
<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>

View file

@ -24,6 +24,13 @@
Back to Projects
</RouterLink>
<img
v-if="content?.frontmatter.cover"
:src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title"
class="w-full h-auto rounded-lg mb-6 object-cover"
/>
<h1 class="text-4xl font-bold text-primary mb-6">
{{ content?.frontmatter.title }}<span class="blinking-cursor"></span>
</h1>
@ -38,10 +45,13 @@
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>

View file

@ -1,45 +1,238 @@
<template>
<div class="max-w-6xl mx-auto">
<h1 class="text-4xl text-primary mb-8">
Projects<span class="blinking-cursor"></span>
</h1>
<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="text-center py-12">
<span class="text-primary"
>Loading<span class="blinking-cursor"></span
></span>
<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">
<span class="text-red-500">Error loading projects</span>
<div class="max-w-md mx-auto rounded-3xl border border-base-300 bg-base-200 p-8">
<span class="text-red-500 block mb-4">Error loading projects</span>
<p class="text-base-content/75 mb-6">
Non riesco a caricare lelenco in questo momento.
</p>
<button
type="button"
class="inline-flex items-center justify-center rounded-full border border-primary px-5 py-2 text-primary hover:bg-primary hover:text-primary-content transition-colors"
@click="reload"
>
Riprova
</button>
</div>
</div>
<div v-else class="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<RouterLink
v-for="project in items"
:key="project.path"
:to="`/projects/${project.slug}`"
class="bg-base-200 p-6 rounded-2xl border border-base-300 hover:border-primary transition-all block"
<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"
>
<img
v-if="project.frontmatter.cover"
:src="project.frontmatter.cover"
:alt="project.frontmatter.title"
class="w-full h-48 object-cover rounded mb-4"
/>
<h2 class="text-3xl font-bold mb-4 text-primary">
{{ project.frontmatter.title }}
</h2>
<div class="markdown-content line-clamp-3" v-html="project.html"></div>
</RouterLink>
<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"
>
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 } = useContentList("/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");
Review

Same story of HomeView for the parser and sanitize

Same story of HomeView for the parser and sanitize
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>