basicstack.org/pages/infrastructure/index.vue

155 lines
3.7 KiB
Vue
Raw Normal View History

<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>