Migrate the Nuxt 3 + Vue 3 SSR frontend application for basicstack.org to our self-hosted Forgejo instance. This repository will serve as the source for all future development and deployment of the basicstack.org website. The application includes: - Nuxt 3 + Vue 3 SSR setup - Directus CMS integration - Tailwind CSS styling - Kubernetes deployment manifests - Docker containerization Co-Authored-By: Paperclip <noreply@paperclip.ing>
154 lines
3.7 KiB
Vue
154 lines
3.7 KiB
Vue
<template>
|
||
<div class="infra-page">
|
||
<div class="page-header">
|
||
<div class="container">
|
||
<h1>Infrastructure</h1>
|
||
<p>How BasicStack is hosted, secured, monitored, and maintained</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container page-body">
|
||
<div v-if="grouped && Object.keys(grouped).length > 0">
|
||
<section
|
||
v-for="(items, category) in grouped"
|
||
:key="category"
|
||
class="category-section"
|
||
>
|
||
<h2 class="category-title">{{ categoryLabels[category] ?? category }}</h2>
|
||
<div class="infra-grid">
|
||
<NuxtLink
|
||
v-for="item in items"
|
||
:key="item.id"
|
||
:to="item.slug ? `/infrastructure/${item.slug}` : '#'"
|
||
class="infra-card card"
|
||
>
|
||
<h3>{{ item.title }}</h3>
|
||
<p v-if="item.summary" class="item-summary">{{ item.summary }}</p>
|
||
<span class="read-more">Read more →</span>
|
||
</NuxtLink>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div v-else class="empty">
|
||
<p>Infrastructure documentation coming soon.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
const { getInfrastructure } = useDirectus()
|
||
const { data: infrastructure } = await useAsyncData('infrastructure', () => getInfrastructure())
|
||
|
||
const categoryLabels: Record<string, string> = {
|
||
hosting: 'Hosting',
|
||
security: 'Security & Encryption',
|
||
monitoring: 'Monitoring',
|
||
backup: 'Backup',
|
||
disaster_recovery: 'Disaster Recovery',
|
||
}
|
||
|
||
const categoryOrder = ['hosting', 'security', 'monitoring', 'backup', 'disaster_recovery']
|
||
|
||
const grouped = computed(() => {
|
||
if (!infrastructure.value) return {}
|
||
const groups: Record<string, typeof infrastructure.value> = {}
|
||
for (const item of infrastructure.value) {
|
||
if (!groups[item.category]) groups[item.category] = []
|
||
groups[item.category].push(item)
|
||
}
|
||
const ordered: Record<string, typeof infrastructure.value> = {}
|
||
for (const cat of categoryOrder) {
|
||
if (groups[cat]) ordered[cat] = groups[cat]
|
||
}
|
||
for (const cat of Object.keys(groups)) {
|
||
if (!ordered[cat]) ordered[cat] = groups[cat]
|
||
}
|
||
return ordered
|
||
})
|
||
|
||
useSeoMeta({
|
||
title: 'Infrastructure – BasicStack',
|
||
description: 'Learn about how BasicStack infrastructure is hosted, secured, and maintained on Hetzner Cloud.',
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
background: var(--color-bg-secondary);
|
||
border-bottom: 1px solid var(--color-border);
|
||
padding: var(--space-2xl) 0;
|
||
}
|
||
|
||
.page-header p {
|
||
color: var(--color-text-muted);
|
||
margin: var(--space-sm) 0 0;
|
||
}
|
||
|
||
.page-body {
|
||
padding: var(--space-2xl) var(--space-lg);
|
||
}
|
||
|
||
.category-section {
|
||
margin-bottom: var(--space-3xl);
|
||
}
|
||
|
||
.category-title {
|
||
font-size: 1.25rem;
|
||
margin-bottom: var(--space-lg);
|
||
color: var(--color-text-muted);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.infra-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: var(--space-lg);
|
||
}
|
||
|
||
.infra-card {
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: box-shadow 0.2s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-sm);
|
||
}
|
||
|
||
.infra-card:hover {
|
||
box-shadow: var(--shadow-md);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.infra-card h3 {
|
||
font-size: 1.0625rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.item-summary {
|
||
color: var(--color-text-muted);
|
||
font-size: 0.9375rem;
|
||
margin: 0;
|
||
flex: 1;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.read-more {
|
||
font-size: 0.875rem;
|
||
color: var(--color-primary);
|
||
margin-top: auto;
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
padding: var(--space-3xl) 0;
|
||
color: var(--color-text-muted);
|
||
}
|
||
</style>
|