155 lines
3.7 KiB
Vue
155 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>
|