New Website in VueJS #6
11 changed files with 263 additions and 33 deletions
|
|
@ -5,4 +5,5 @@ author: "ADMStaff"
|
|||
toc: false
|
||||
framed: false
|
||||
# cover:
|
||||
# link: # per i progetti
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
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}`;
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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/");
|
||||
|
|
||||
|
||||
const resolveCover = (cover) => resolveAssetPath(cover);
|
||||
|
||||
// Formatta la data
|
||||
function formatDate(date) {
|
||||
return new Date(date).toLocaleDateString("it-IT", {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
samu
commented
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 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="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");
|
||||
|
samu
commented
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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue
We don't need the
/content/prefix