basicstack.org/pages/packages/[slug].vue
Paperclip CTO a7bd527793 Initial commit: Move basicstack.org frontend to Forgejo
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>
2026-06-28 12:32:23 +00:00

264 lines
6.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="pkg" class="package-detail">
<div class="page-header">
<div class="container">
<NuxtLink to="/packages" class="breadcrumb"> All Packages</NuxtLink>
<div class="package-hero">
<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>
<div>
<h1>{{ pkg.name }}</h1>
<p class="short-desc">{{ pkg.short_description }}</p>
<div class="package-actions">
<a v-if="pkg.website_url" :href="pkg.website_url" target="_blank" rel="noopener" class="btn btn-primary">
Website
</a>
<a v-if="pkg.documentation_url" :href="pkg.documentation_url" target="_blank" rel="noopener" class="btn btn-outline">
Documentation
</a>
</div>
</div>
</div>
</div>
</div>
<div class="container page-body">
<div class="content-grid">
<div class="main-content">
<!-- Full description -->
<section v-if="pkg.full_description" class="content-section">
<h2>About {{ pkg.name }}</h2>
<div class="prose" v-html="pkg.full_description" />
</section>
<!-- Screenshots -->
<section v-if="pkg.screenshots && pkg.screenshots.length > 0" class="content-section">
<h2>Screenshots</h2>
<div class="screenshots-grid">
<figure
v-for="screenshot in sortedScreenshots"
:key="screenshot.id"
class="screenshot"
>
<img
:src="getAssetUrl(screenshot.image)"
:alt="screenshot.title ?? pkg.name + ' screenshot'"
class="screenshot-img"
/>
<figcaption v-if="screenshot.title || screenshot.caption" class="screenshot-caption">
<strong v-if="screenshot.title">{{ screenshot.title }}</strong>
<span v-if="screenshot.caption"> {{ screenshot.caption }}</span>
</figcaption>
</figure>
</div>
</section>
<!-- Deployment details -->
<section v-if="pkg.deployment_details && pkg.deployment_details.length > 0" class="content-section">
<h2>Deployment</h2>
<div
v-for="detail in sortedDeploymentDetails"
:key="detail.id"
class="detail-block"
>
<h3>{{ detail.title }}</h3>
<div class="prose" v-html="detail.content" />
</div>
</section>
<!-- Troubleshooting -->
<section v-if="pkg.troubleshooting && pkg.troubleshooting.length > 0" class="content-section">
<h2>Troubleshooting</h2>
<div
v-for="item in sortedTroubleshooting"
:key="item.id"
class="trouble-block card"
>
<h3>{{ item.title }}</h3>
<div v-if="item.problem">
<h4>Problem</h4>
<div class="prose" v-html="item.problem" />
</div>
<div v-if="item.solution">
<h4>Solution</h4>
<div class="prose" v-html="item.solution" />
</div>
</div>
</section>
</div>
</div>
</div>
</div>
<div v-else class="not-found container">
<h1>Package not found</h1>
<NuxtLink to="/packages" class="btn btn-primary">Back to Packages</NuxtLink>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { getPackage, getAssetUrl } = useDirectus()
const { data: pkg } = await useAsyncData(`package-${route.params.slug}`, () =>
getPackage(route.params.slug as string)
)
const sortedScreenshots = computed(() =>
[...(pkg.value?.screenshots ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
)
const sortedDeploymentDetails = computed(() =>
[...(pkg.value?.deployment_details ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
)
const sortedTroubleshooting = computed(() =>
[...(pkg.value?.troubleshooting ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
)
if (pkg.value) {
useSeoMeta({
title: `${pkg.value.name} BasicStack`,
description: pkg.value.short_description,
ogTitle: pkg.value.name,
ogDescription: pkg.value.short_description,
})
}
</script>
<style scoped>
.page-header {
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
padding: var(--space-2xl) 0;
}
.breadcrumb {
color: var(--color-text-muted);
font-size: 0.875rem;
display: block;
margin-bottom: var(--space-lg);
}
.package-hero {
display: flex;
align-items: flex-start;
gap: var(--space-xl);
}
.package-logo {
width: 96px;
height: 96px;
object-fit: contain;
flex-shrink: 0;
}
.package-logo-placeholder {
width: 96px;
height: 96px;
border-radius: var(--radius-lg);
background: var(--color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 700;
flex-shrink: 0;
}
.package-hero h1 {
margin-bottom: var(--space-sm);
}
.short-desc {
color: var(--color-text-muted);
margin-bottom: var(--space-lg);
}
.package-actions {
display: flex;
gap: var(--space-md);
flex-wrap: wrap;
}
.page-body {
padding: var(--space-2xl) var(--space-lg);
}
.content-section {
margin-bottom: var(--space-3xl);
}
.content-section h2 {
font-size: 1.5rem;
padding-bottom: var(--space-md);
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-xl);
}
.screenshots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-lg);
}
.screenshot {
margin: 0;
}
.screenshot-img {
width: 100%;
height: auto;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
.screenshot-caption {
font-size: 0.875rem;
color: var(--color-text-muted);
margin-top: var(--space-sm);
}
.detail-block {
margin-bottom: var(--space-2xl);
}
.detail-block h3 {
font-size: 1.125rem;
margin-bottom: var(--space-md);
}
.trouble-block {
margin-bottom: var(--space-lg);
}
.trouble-block h3 {
font-size: 1.125rem;
margin-bottom: var(--space-md);
}
.trouble-block h4 {
font-size: 0.9375rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}
.not-found {
padding: var(--space-3xl) var(--space-lg);
text-align: center;
}
@media (max-width: 640px) {
.package-hero {
flex-direction: column;
}
}
</style>