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 toc: false
framed: false framed: false
# cover: # cover:
# link: # per i progetti
--- ---

View file

@ -5,6 +5,7 @@ date: 2024-03-16T18:00:00+02:00
author: "Admstaff" author: "Admstaff"
draft: false draft: false
toc: 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. 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" author: "Admstaff"
draft: false draft: false
toc: false toc: false
link: "https://git.students.cs.unibo.it"
cover: "img/git-forgejo.png" cover: "img/git-forgejo.png"
--- ---

View file

@ -4,6 +4,7 @@ date: 2025-11-25T18:00:00+02:00
author: "Admstaff" author: "Admstaff"
draft: false draft: false
toc: false toc: false
link: "https://sasso.students.cs.unibo.it"
cover: "https://sasso.students.cs.unibo.it/sasso.png" 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 <img
v-if="content?.frontmatter.cover" v-if="content?.frontmatter.cover"
:src="'/' + content.frontmatter.cover" :src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title" :alt="content.frontmatter.title"
class="w-full h-auto object-contain rounded-lg mb-6" 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 { useMeta } from "../composables/useMeta.js";
import { computed, watch } from "vue"; import { computed, watch } from "vue";
import { Icon } from "@iconify/vue"; import { Icon } from "@iconify/vue";
import { resolveAssetPath } from "../utils/assetPath.js";
const route = useRoute(); const route = useRoute();
const slug = computed(() => route.params.slug); const slug = computed(() => route.params.slug);
const resolveCover = (cover) => resolveAssetPath(cover);
// Carica l'evento specifico basandosi sullo slug nella URL // Carica l'evento specifico basandosi sullo slug nella URL
const { content, loading, error } = useContent(`events/${slug.value}`); const { content, loading, error } = useContent(`events/${slug.value}`);

View file

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

View file

@ -42,7 +42,7 @@
> >
<img <img
v-if="event.frontmatter.cover" v-if="event.frontmatter.cover"
:src="'/' + event.frontmatter.cover" :src="resolveCover(event.frontmatter.cover)"
:alt="event.frontmatter.title" :alt="event.frontmatter.title"
class="mb-4 w-full rounded-[10px]" 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" class="mb-4 aspect-[16/9] w-full overflow-hidden rounded-[10px] flex items-center justify-center"
> >
<img <img
:src="project.frontmatter.cover" :src="resolveCover(project.frontmatter.cover)"
:alt="project.frontmatter.title" :alt="project.frontmatter.title"
class="block h-auto w-full max-h-full object-contain object-center my-auto" 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 { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js"; import { useContentList } from "../composables/useContent.js";
import { computed, onMounted, nextTick } from "vue"; 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 // Carica eventi e progetti
const { items: allEvents, loading: eventsLoading } = const { items: allEvents, loading: eventsLoading } =
@ -140,4 +141,6 @@ function truncate(html, length) {
// Restituisci HTML semplice // Restituisci HTML semplice
return `<p>${finalText}...</p>`; return `<p>${finalText}...</p>`;
} }
const resolveCover = (cover) => resolveAssetPath(cover);
</script> </script>

View file

@ -19,7 +19,7 @@
<img <img
v-if="content?.frontmatter.cover" v-if="content?.frontmatter.cover"
:src="'/' + content.frontmatter.cover" :src="resolveCover(content.frontmatter.cover)"
:alt="content.frontmatter.title" :alt="content.frontmatter.title"
class="md:w-1/3 w-full h-auto object-contain rounded-lg" class="md:w-1/3 w-full h-auto object-contain rounded-lg"
/> />
@ -29,7 +29,10 @@
<script setup> <script setup>
import { useContent } from "../composables/useContent.js"; import { useContent } from "../composables/useContent.js";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica il file lab.md dalla directory content/ // Carica il file lab.md dalla directory content/
const { content, loading, error } = useContent("lab"); const { content, loading, error } = useContent("lab");
const resolveCover = (cover) => resolveAssetPath(cover);
</script> </script>

View file

@ -24,6 +24,13 @@
Back to Projects Back to Projects
</RouterLink> </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"> <h1 class="text-4xl font-bold text-primary mb-6">
{{ content?.frontmatter.title }}<span class="blinking-cursor"></span> {{ content?.frontmatter.title }}<span class="blinking-cursor"></span>
</h1> </h1>
@ -38,10 +45,13 @@
import { RouterLink, useRoute } from "vue-router"; import { RouterLink, useRoute } from "vue-router";
import { useContent } from "../composables/useContent.js"; import { useContent } from "../composables/useContent.js";
import { computed } from "vue"; import { computed } from "vue";
import { resolveAssetPath } from "../utils/assetPath.js";
const route = useRoute(); const route = useRoute();
const slug = computed(() => route.params.slug); const slug = computed(() => route.params.slug);
const resolveCover = (cover) => resolveAssetPath(cover);
// Carica il progetto specifico basandosi sullo slug nella URL // Carica il progetto specifico basandosi sullo slug nella URL
const { content, loading, error } = useContent(`projects/${slug.value}`); const { content, loading, error } = useContent(`projects/${slug.value}`);
</script> </script>

View file

@ -1,45 +1,238 @@
<template> <template>
<div class="max-w-6xl mx-auto"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
<h1 class="text-4xl text-primary mb-8"> <section class="mb-10 lg:mb-14 max-w-3xl">
Projects<span class="blinking-cursor"></span> <p class="text-xs uppercase tracking-[0.3em] text-secondary mb-3">
</h1> 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"> <div v-if="loading" class="space-y-8">
<span class="text-primary" <div class="grid gap-6 lg:grid-cols-[1.25fr_0.75fr]">
>Loading<span class="blinking-cursor"></span <div class="animate-pulse rounded-3xl border border-base-300 bg-base-200/70 p-6 lg:p-8 min-h-[22rem]">
></span> <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>
<div v-else-if="error" class="text-center py-12"> <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>
<div v-else class="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div v-else class="space-y-8 lg:space-y-10">
<RouterLink <article
v-for="project in items" v-if="featuredProject"
:key="project.path" 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"
:to="`/projects/${project.slug}`"
class="bg-base-200 p-6 rounded-2xl border border-base-300 hover:border-primary transition-all block"
> >
<img <div class="grid lg:grid-cols-[1.2fr_0.8fr] min-h-[20rem]">
v-if="project.frontmatter.cover" <div class="order-2 lg:order-1 p-6 sm:p-8 lg:p-10 flex flex-col justify-between gap-6">
:src="project.frontmatter.cover" <div>
:alt="project.frontmatter.title" <p class="text-xs uppercase tracking-[0.28em] text-secondary mb-4">
class="w-full h-48 object-cover rounded mb-4" Featured project
/> </p>
<h2 class="text-3xl font-bold mb-4 text-primary"> <h2 class="text-3xl sm:text-4xl font-bold text-primary mb-4 leading-tight group-hover:text-base-content transition-colors">
{{ project.frontmatter.title }} {{ featuredProject.frontmatter.title }}
</h2> </h2>
<div class="markdown-content line-clamp-3" v-html="project.html"></div> <div
</RouterLink> 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>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from "vue";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import { useContentList } from "../composables/useContent.js"; import { useContentList } from "../composables/useContent.js";
import { resolveAssetPath } from "../utils/assetPath.js";
// Carica tutti i progetti dalla directory content/projects/ // 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> </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>