New Website in VueJS #6

Open
alice wants to merge 11 commits from new-website into main
2 changed files with 340 additions and 179 deletions
Showing only changes of commit bdd26a3b31 - Show all commits

View file

@ -14,12 +14,12 @@
<span class="text-red-500">Error loading projects</span>
</div>
<div v-else class="space-y-6">
<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 border border-base-300 hover:border-primary transition-all block"
class="bg-base-200 p-6 rounded-2xl border border-base-300 hover:border-primary transition-all block"
>
<img
v-if="project.frontmatter.cover"

View file

@ -1,9 +1,61 @@
<template>
<div class="min-h-screen bg-base-100 text-base-content">
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-5 gap-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 class="lg:col-span-1 sticky top-4">
<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
class="btn btn-xs btn-ghost"
type="button"
@click="closeMobileSidebar"
>
X
Review

An icon would look better :)

An icon would look better :)
</button>
</div>
<nav class="rounded-lg">
<!-- Lista pagine dalla sidebar -->
<div class="space-y-2 text-sm">
@ -22,7 +74,7 @@
:target="item.isExternal ? '_blank' : null"
:rel="item.isExternal ? 'noopener noreferrer' : null"
@click="onSidebarLinkClick($event, item)"
class="block py-1 px-2 rounded-lg hover:bg-base-300/20 transition truncate"
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,
@ -39,16 +91,16 @@
<!-- TOC (Table of Contents) - Outline minimal terminal style -->
<nav
v-if="currentPage === item.slug && outline.length > 0"
class="sticky top-4"
class="mt-2"
>
<ul
class="space-y-0 text-xs font-mono leading-snug border-l border-base-300 pl-3"
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 py-0.5 hover:text-base-content rounded-lg transition truncate"
class="block rounded-md py-1.5 pr-2 transition hover:text-base-content"
:style="{
paddingLeft: `${(heading.level - 1) * 0.5}rem`,
}"
@ -70,7 +122,7 @@
</aside>
<!-- Contenuto principale -->
<main class="lg:col-span-4">
<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
@ -79,13 +131,13 @@
<article
v-else
class="bg-base-200 rounded-lg p-8 border border-base-300"
class="bg-base-200 rounded-lg border border-base-300 p-4 sm:p-6 lg:p-8"
>
<h1 class="text-4xl font-bold text-primary mb-6">
<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"
class="markdown-content prose prose-invert max-w-none text-sm md:text-base"
v-html="pageContent"
></div>
</article>
@ -93,7 +145,7 @@
</div>
</div>
<button
class="btn btn-circle btn-secondary bottom-4 right-4 fixed font-bold text-3xl"
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"
>
@ -103,13 +155,19 @@
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
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");
@ -118,147 +176,118 @@ const sidebarItems = ref([]);
const outline = ref([]);
const activeHeading = ref("");
const showScrollTopButton = ref(false);
const isMobileSidebarOpen = ref(false);
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
showScrollTopButton.value = false;
const sidebarPageCount = computed(
() =>
sidebarItems.value.filter(
(item) => item.type === "link" && !item.isExternal,
).length,
);
const mobileSidebarSummary = computed(() => {
const pages = sidebarPageCount.value;
const sections = outline.value.length;
return `${pages} pagine · ${sections} sezioni`;
});
const sidebarPanelClasses = computed(() => [
Review

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

We only use this once in the html, why use a compute?
"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 onSidebarLinkClick(event, item) {
if (item.isExternal) return;
event.preventDefault();
loadPage(item.slug);
function toWikiSlug(value) {
return value.replace(/^\.\//, "").replace(/\/$/, "").toLowerCase();
}
// Carica il contenuto della sidebar
async function loadSidebar() {
try {
const sidebarModule =
await import("/content/wiki/adm.wiki/_Sidebar.md?raw");
const sidebarContent = sidebarModule.default;
function toTitleCaseSlug(value) {
return value
.split("-")
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join("-");
}
// Parsa la sidebar markdown
const lines = sidebarContent.split("\n").filter((line) => line.trim());
const items = [];
function buildPossibleWikiPaths(slug) {
return [
`${WIKI_CONTENT_BASE_PATH}/${slug}.md`,
`${WIKI_CONTENT_BASE_PATH}/${slug.charAt(0).toUpperCase() + slug.slice(1)}.md`,
`${WIKI_CONTENT_BASE_PATH}/${toTitleCaseSlug(slug)}.md`,
];
}
for (const line of lines) {
// Ignora commenti
if (line.includes("<!-- NOT INCLUDE -->") || line.includes("<!--"))
continue;
// Heading (###)
if (line.startsWith("###") || line.startsWith("- ###")) {
items.push({
type: "heading",
text: line.replace(/^###\s*/, "").trim(),
});
}
// Link standard [text](url)
else if (line.includes("](")) {
const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (match) {
const text = match[1];
const url = match[2];
const isExternal = /^https?:\/\//i.test(url);
// Estrai lo slug dal URL
const slug = url
.replace(/^\.\//, "")
.replace(/\/$/, "")
.toLowerCase();
items.push({
type: "link",
text,
slug,
isExternal,
url,
});
}
}
// Link wiki-style [[text]]
else if (line.includes("[[")) {
const match = line.match(/\[\[([^\]]+)\]\]/);
if (match) {
const text = match[1];
items.push({
type: "link",
text,
slug: text.toLowerCase(),
});
}
}
}
sidebarItems.value = items;
} catch (error) {
console.error("Error loading sidebar:", error);
function parseSidebarLine(line) {
if (line.includes("<!-- NOT INCLUDE -->") || line.includes("<!--")) {
Review

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

What's the point of this? The exclusion should be server-side with the python script, not client side
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],
slug: toWikiSlug(match[1]),
isExternal: false,
url: "#",
};
}
return null;
}
// Carica una pagina wiki
async function loadPage(slug) {
loading.value = true;
currentPage.value = slug;
function createMarkdownRenderer() {
Review

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

All of this is very ugly tbh. Can we do it at compile time like the og photos?
const renderer = new marked.Renderer();
// Aggiorna URL senza ricaricare
router.push({ query: { page: slug } });
renderer.heading = function (text, level) {
const id = toHeadingId(text);
return `<h${level} id="${id}" href="#${id}" >${text}</h${level}>`;
};
try {
// Prova vari formati di filename
const possiblePaths = [
`/content/wiki/adm.wiki/${slug}.md`,
`/content/wiki/adm.wiki/${slug.charAt(0).toUpperCase() + slug.slice(1)}.md`,
`/content/wiki/adm.wiki/${slug
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("-")}.md`,
];
let content = null;
let foundPath = null;
for (const path of possiblePaths) {
try {
const module = await import(/* @vite-ignore */ path + "?raw");
content = module.default;
foundPath = path;
break;
} catch (err) {
// Continua a provare
}
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" />`;
};
if (!content) {
throw new Error(`Page not found: ${slug}`);
}
// Estrai titolo
const titleMatch = content.match(/^#\s+(.+)$/m);
pageTitle.value = titleMatch ? titleMatch[1] : slug.replace(/-/g, " ");
// Rimuovi solo il primo H1 iniziale (se presente)
const contentWithoutMainTitle = content.replace(/^\s*#\s+.+(?:\r?\n)+/, "");
// Converti markdown con custom renderers
const renderer = new marked.Renderer();
renderer.heading = function (text, level) {
const id = text.toLowerCase().replace(/[^\w]+/g, "-");
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 = `/content/wiki/adm.wiki/${href}`;
}
const titleAttr = title ? ` title="${title}"` : "";
return `<img src="${src}" alt="${text}"${titleAttr} class="max-w-full h-auto" />`;
};
renderer.code = function (code, language) {
const lang = language || "text";
const escapedCode = code.replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `<div class="code-block-wrapper">
renderer.code = function (code, language) {
const lang = language || "text";
const escapedCode = code.replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `<div class="code-block-wrapper">
<div class="code-block-header">
<span class="code-block-lang">${lang}</span>
<button class="code-block-copy" onclick="copyCodeToClipboard(this)" title="Copy code">
@ -270,15 +299,119 @@ async function loadPage(slug) {
</div>
<pre><code class="language-${lang}">${escapedCode}</code></pre>
</div>`;
};
};
// Render e outline coerenti, entrambi senza H1 iniziale
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({ query: { page: 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 });
// TODO: togliere parti di codice per elaborare l'outline
extractOutline(contentWithoutMainTitle);
} catch (error) {
console.error("Error loading page:", error);
pageContent.value = `<p class="text-red-500">Page not found: ${slug}</p>`;
pageContent.value = `<p class="text-red-500">Page not found: ${normalizedSlug}</p>`;
pageTitle.value = "Error";
outline.value = [];
} finally {
@ -296,7 +429,7 @@ function extractOutline(markdown) {
if (match) {
const level = match[1].length;
const text = match[2];
const id = text.toLowerCase().replace(/[^\w]+/g, "-");
const id = toHeadingId(text);
headings.push({ level, text, id });
}
@ -311,18 +444,29 @@ function scrollToHeading(id) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
closeMobileSidebar();
}
}
// Rileva heading attivo durante lo scroll
function handleScroll() {
if (outline.value.length === 0) return;
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 <= 100 && rect.bottom >= 100) {
if (rect.top <= threshold && rect.bottom >= threshold) {
activeHeading.value = heading.id;
break;
}
@ -331,6 +475,8 @@ function handleScroll() {
}
onMounted(async () => {
window.copyCodeToClipboard = copyCodeToClipboard;
await loadSidebar();
// Carica la pagina dall'URL o Home come default
@ -338,11 +484,31 @@ onMounted(async () => {
await loadPage(page);
window.addEventListener("scroll", handleScroll);
window.addEventListener("resize", updateBodyScrollLock);
handleScroll();
updateBodyScrollLock();
});
window.addEventListener("scroll", () => {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
showScrollTopButton.value = scrollTop > 300;
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", updateBodyScrollLock);
document.body.classList.remove("overflow-hidden");
delete window.copyCodeToClipboard;
});
watch(
() => route.query.page,
async (page) => {
const normalizedPage = typeof page === "string" ? page : "home";
if (normalizedPage !== currentPage.value) {
await loadPage(normalizedPage);
}
closeMobileSidebar();
},
);
watch(isMobileSidebarOpen, () => {
updateBodyScrollLock();
});
</script>
@ -350,7 +516,15 @@ onMounted(async () => {
.prose :deep(h2),
.prose :deep(h3),
.prose :deep(h4) {
scroll-margin-top: 5rem;
scroll-margin-top: 4rem;
}
@media (min-width: 768px) {
.prose :deep(h2),
.prose :deep(h3),
.prose :deep(h4) {
scroll-margin-top: 5rem;
}
}
/* Code blocks styling */
@ -385,7 +559,7 @@ onMounted(async () => {
border: none;
color: var(--adm-text-secondary);
cursor: pointer;
padding: 0.25rem;
padding: 0.5rem;
display: flex;
align-items: center;
border-radius: 4px;
@ -410,29 +584,16 @@ onMounted(async () => {
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>
<script>
// Funzione globale per copiare il codice
window.copyCodeToClipboard = function (button) {
const wrapper = button.closest(".code-block-wrapper");
const code = wrapper.querySelector("code").textContent;
navigator.clipboard
.writeText(code)
.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);
});
};
</script>