Justin Deal bd8ed8bc9c
Some checks failed
Build and Deploy / build (push) Failing after 10s
Add more animations and attempt CICD fix
2025-05-03 15:31:48 -07:00

215 lines
6.6 KiB
Plaintext

---
import { GLOBAL } from "../../lib/variables";
import Layout from "../../layouts/Layout.astro";
import Section from "../../components/common/Section.astro";
import SearchBar from "../../components/common/SearchBar.astro";
import StyleControls from "../../components/common/StyleControls.astro";
import StyleControlsScript from "../../components/StyleControlsScript.astro";
import CategorySection from "../../components/homelab/CategorySection.astro";
import ServiceCardSkeleton from "../../components/common/ServiceCardSkeleton.astro";
import SkeletonLoader from "../../components/common/SkeletonLoader.astro";
import SEO from "../../components/SEO.astro";
import StructuredData from "../../components/StructuredData.astro";
import { services } from "./services.ts";
// Count total services
const totalServices = Object.values(services).reduce(
(count, serviceList) => count + serviceList.length,
0
);
// Structured data for the homelab page
const webpageData = {
name: "Homelab Dashboard",
description: `A collection of ${totalServices} self-hosted services and applications running on my personal homelab.`,
url: `${GLOBAL.rootUrl}/homelab`,
isPartOf: {
"@type": "WebSite",
"name": `${GLOBAL.username} • ${GLOBAL.shortDescription}`,
"url": GLOBAL.rootUrl
}
};
---
<Layout>
<SEO
slot="head"
title="Homelab Dashboard"
description={`A collection of ${totalServices} self-hosted services and applications running on my personal homelab.`}
canonicalUrl={`${GLOBAL.rootUrl}/homelab`}
/>
<StructuredData slot="head" type="WebPage" data={webpageData} />
<!-- Print-specific styles -->
<style is:global slot="head">
@media print {
/* Hide non-essential UI elements */
header, footer, .search-container, .style-controls, button[aria-controls] {
display: none !important;
}
/* Ensure all categories are expanded */
[x-show="open"] {
display: block !important;
}
/* Reset grid to single column for better print layout */
.grid {
display: block !important;
columns: 2 !important;
column-gap: 1.5rem !important;
}
/* Ensure proper page breaks */
.category-section {
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 1.5rem !important;
border-bottom: 1px solid #ddd;
padding-bottom: 1rem;
}
/* Category headings */
.category-section > button {
font-size: 1.5rem !important;
margin-bottom: 1rem !important;
border-bottom: 2px solid #000;
padding-bottom: 0.5rem;
}
/* Hide category toggle icons */
.category-section svg {
display: none !important;
}
/* Add page info */
@page {
margin: 1cm;
}
body::after {
content: "Printed from justin.deal/homelab on " attr(data-print-date);
display: block;
text-align: center;
font-size: 0.8rem;
margin-top: 2rem;
font-style: italic;
}
/* Ensure white background and black text */
body, html {
background: white !important;
color: black !important;
}
/* Add print header with title */
body::before {
content: "Homelab Services Directory";
display: block;
text-align: center;
font-size: 1.5rem;
font-weight: bold;
margin: 1rem 0 2rem;
border-bottom: 2px solid #000;
padding-bottom: 0.5rem;
}
/* Hide animations and transitions */
* {
animation: none !important;
transition: none !important;
}
}
</style>
<!-- Search functionality is provided by SearchScript.astro -->
<!-- Keyboard shortcuts for style controls -->
<StyleControlsScript />
<!-- Set current date for print view -->
<script is:inline>
document.addEventListener('DOMContentLoaded', () => {
const now = new Date();
const formattedDate = now.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
document.body.setAttribute('data-print-date', formattedDate);
});
</script>
<Section class="my-2">
<div x-data="searchServices" x-init="init()" x-cloak>
<!-- Search and controls container -->
<div class="mb-4 pt-0">
<!-- Style controls in a centered row above search -->
<div class="w-full flex justify-center mb-4">
<StyleControls />
</div>
<!-- Search bar below style controls -->
<div class="w-full">
<SearchBar
placeholder="Search services..."
ariaLabel="Search services"
/>
</div>
</div>
<!-- Page heading - now below search -->
<div class="flex items-center gap-4 pb-4 mt-2">
<h1 class="font-display text-3xl sm:text-4xl leading-loose">Homelab</h1>
</div>
<!-- No results message -->
<div
x-show="searchQuery !== '' && !hasResults"
x-transition
class="text-center py-8 my-4 border-2 border-dashed border-current zag-text rounded-lg"
>
<p class="text-xl font-semibold zag-text">No Results</p>
</div>
<!-- Loading skeleton -->
<div
x-show="loading"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
{Object.entries(services).map(([category, apps]) => (
<div class="mb-8">
<div class="text-xl font-semibold mb-4 w-full text-left flex items-center justify-between">
<SkeletonLoader type="title" width="40%" height="1.5rem" />
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: apps.length || 4 }).map((_, i) => (
<ServiceCardSkeleton />
))}
</div>
</div>
))}
</div>
<!-- Service categories (actual content) -->
<div
id="app-list"
x-show="!loading"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
>
{Object.entries(services).map(([category, apps]) => (
<CategorySection category={category} apps={apps} />
))}
</div>
</div>
</Section>
</Layout>