basicstack.org/pages/index.vue

247 lines
6.2 KiB
Vue
Raw Normal View History

<template>
<div>
<section class="hero">
<div class="container">
<h1 class="hero-title">Production-ready open source infrastructure</h1>
<p class="hero-sub">
BasicStack is a curated collection of self-hosted open source software, deployed and maintained on Hetzner Cloud infrastructure in Germany.
</p>
<div class="hero-actions">
<NuxtLink to="/packages" class="btn btn-primary">Browse Packages</NuxtLink>
<NuxtLink to="/infrastructure" class="btn btn-outline">Our Infrastructure</NuxtLink>
</div>
</div>
</section>
<section class="packages-section" v-if="packages && packages.length > 0">
<div class="container">
<h2 class="section-title">Software Packages</h2>
<p class="section-sub">Open source tools we run and maintain</p>
<div class="packages-grid">
<NuxtLink
v-for="pkg in packages.slice(0, 6)"
:key="pkg.id"
:to="`/packages/${pkg.slug}`"
class="package-card card"
>
<img
v-if="pkg.logo"
:src="getAssetUrl(pkg.logo)"
:alt="pkg.name + ' logo'"
class="package-logo"
/>
<div v-else class="package-logo-placeholder">
{{ pkg.name[0] }}
</div>
<h3 class="package-name">{{ pkg.name }}</h3>
<p class="package-desc">{{ pkg.short_description }}</p>
</NuxtLink>
</div>
<div class="section-cta" v-if="packages.length > 6">
<NuxtLink to="/packages" class="btn btn-outline">View all {{ packages.length }} packages</NuxtLink>
</div>
</div>
</section>
<section class="empty-state" v-else-if="packages !== null">
<div class="container">
<h2 class="section-title">Software Packages</h2>
<p class="section-sub">Content coming soon...</p>
</div>
</section>
<section class="infra-section">
<div class="container">
<h2 class="section-title">Infrastructure</h2>
<p class="section-sub">How we run BasicStack</p>
<div class="infra-grid" v-if="infrastructure && infrastructure.length > 0">
<NuxtLink
v-for="item in infrastructure.slice(0, 4)"
:key="item.id"
:to="item.slug ? `/infrastructure/${item.slug}` : '/infrastructure'"
class="infra-card card"
>
<span class="infra-category badge badge-primary">{{ formatCategory(item.category) }}</span>
<h3 class="infra-title">{{ item.title }}</h3>
<p class="infra-summary" v-if="item.summary">{{ item.summary }}</p>
</NuxtLink>
</div>
<div class="section-cta">
<NuxtLink to="/infrastructure" class="btn btn-outline">Learn more about our infrastructure</NuxtLink>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const { getPackages, getInfrastructure, getAssetUrl } = useDirectus()
const [{ data: packages }, { data: infrastructure }] = await Promise.all([
useAsyncData('home-packages', () => getPackages()),
useAsyncData('home-infrastructure', () => getInfrastructure()),
])
const categoryLabels: Record<string, string> = {
hosting: 'Hosting',
security: 'Security',
monitoring: 'Monitoring',
backup: 'Backup',
disaster_recovery: 'Disaster Recovery',
}
function formatCategory(cat: string) {
return categoryLabels[cat] ?? cat
}
useSeoMeta({
title: 'BasicStack Production-ready open source infrastructure',
description: 'A curated collection of self-hosted open source software on Hetzner Cloud infrastructure in Germany.',
ogTitle: 'BasicStack',
ogDescription: 'Production-ready open source infrastructure',
})
</script>
<style scoped>
.hero {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg) 100%);
padding: var(--space-3xl) 0;
text-align: center;
}
.hero-title {
font-size: clamp(2rem, 5vw, 3.5rem);
margin-bottom: var(--space-lg);
line-height: 1.15;
}
.hero-sub {
font-size: 1.125rem;
color: var(--color-text-muted);
max-width: 600px;
margin: 0 auto var(--space-xl);
}
.hero-actions {
display: flex;
gap: var(--space-md);
justify-content: center;
flex-wrap: wrap;
}
.packages-section,
.infra-section {
padding: var(--space-3xl) 0;
}
.infra-section {
background: var(--color-bg-secondary);
}
.section-title {
font-size: 1.875rem;
margin-bottom: var(--space-sm);
}
.section-sub {
color: var(--color-text-muted);
margin-bottom: var(--space-2xl);
}
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-lg);
}
.package-card {
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s, transform 0.2s;
}
.package-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
text-decoration: none;
}
.package-logo {
width: 64px;
height: 64px;
object-fit: contain;
margin-bottom: var(--space-md);
}
.package-logo-placeholder {
width: 64px;
height: 64px;
border-radius: var(--radius-md);
background: var(--color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-md);
}
.package-name {
font-size: 1.125rem;
margin-bottom: var(--space-sm);
}
.package-desc {
color: var(--color-text-muted);
font-size: 0.9375rem;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.section-cta {
text-align: center;
margin-top: var(--space-2xl);
}
.infra-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.infra-card {
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s;
}
.infra-card:hover {
box-shadow: var(--shadow-md);
text-decoration: none;
}
.infra-category {
margin-bottom: var(--space-md);
}
.infra-title {
font-size: 1.125rem;
margin: var(--space-sm) 0;
}
.infra-summary {
color: var(--color-text-muted);
font-size: 0.9375rem;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>