feat: Implement Phase 1 enhancements - components and animations
Enterprise AI Code Review / ai-review (pull_request) Successful in 19s
Enterprise AI Code Review / ai-review (pull_request) Successful in 19s
Components: - Extract ServiceItem component with hover effects - Extract SupportItem component with hover animations - Extract Card component with scroll animations - Extract Section component for consistent layout Enhancements: - Add Intersection Observer for scroll-triggered animations - Implement hover effects on cards, services, and support items - Add PWA manifest (site.webmanifest) - Create SVG OG image placeholder for social sharing - Add comprehensive favicon meta tags (16x16, 32x32, apple-touch-icon) - Respect prefers-reduced-motion for accessibility Files modified: - src/pages/index.astro - Refactored to use new components - src/layouts/BaseLayout.astro - Enhanced favicon and meta tags Files created: - src/components/Card.astro - src/components/Section.astro - src/components/ServiceItem.astro - src/components/SupportItem.astro - public/og-image.svg - public/site.webmanifest - public/FAVICON_INSTRUCTIONS.md Note: PNG favicon variants still need to be generated (see FAVICON_INSTRUCTIONS.md)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={["card", "scroll-animate", className]}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--color-bg-light);
|
||||
border-radius: 12px;
|
||||
padding: var(--space-lg);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(212, 165, 116, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.card :global(h2) {
|
||||
font-size: 2rem;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.card :global(p) {
|
||||
margin-bottom: var(--space-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.card :global(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Scroll animation states */
|
||||
.scroll-animate {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
}
|
||||
|
||||
.scroll-animate.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.scroll-animate {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card :global(h2) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
ariaLabelledby?: string;
|
||||
}
|
||||
|
||||
const { class: className, ariaLabelledby } = Astro.props;
|
||||
---
|
||||
|
||||
<section class:list={["section", className]} aria-labelledby={ariaLabelledby}>
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.section {
|
||||
padding: var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-md);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
url?: string;
|
||||
comingSoon?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, url, comingSoon = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="service-item" class:list={{ "coming-soon": comingSoon }} role="listitem">
|
||||
<h3>
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Visit ${title}`}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.service-item {
|
||||
background: var(--color-bg);
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--color-accent);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 8px rgba(212, 165, 116, 0.2);
|
||||
}
|
||||
|
||||
.service-item.coming-soon {
|
||||
border-left-color: var(--color-warm);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.service-item h3 {
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.service-item p {
|
||||
color: var(--color-text-dim);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.service-item {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
comingSoonNote?: string;
|
||||
}
|
||||
|
||||
const { title, description, comingSoonNote } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="support-item" role="listitem">
|
||||
<h3>{title}</h3>
|
||||
<p>
|
||||
{description}
|
||||
{comingSoonNote && (
|
||||
<span class="coming-soon-text">({comingSoonNote})</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.support-item {
|
||||
background: var(--color-bg);
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.support-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(212, 165, 116, 0.15);
|
||||
}
|
||||
|
||||
.support-item h3 {
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.support-item p {
|
||||
color: var(--color-text-dim);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.coming-soon-text {
|
||||
font-style: italic;
|
||||
color: var(--color-warm);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.support-item {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.support-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+170
-143
@@ -1,16 +1,16 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.",
|
||||
ogImage = "/og-image.png",
|
||||
canonicalURL = Astro.url.pathname
|
||||
title,
|
||||
description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.",
|
||||
ogImage = "/og-image.png",
|
||||
canonicalURL = Astro.url.pathname,
|
||||
} = Astro.props;
|
||||
|
||||
const fullCanonicalURL = new URL(canonicalURL, Astro.site).href;
|
||||
@@ -19,156 +19,183 @@ const fullOgImage = new URL(ogImage, Astro.site).href;
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={fullCanonicalURL} />
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={fullCanonicalURL} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={fullCanonicalURL} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={fullOgImage} />
|
||||
<meta property="og:site_name" content="Hidden Den Cafe" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={fullCanonicalURL} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={fullOgImage} />
|
||||
<meta property="og:site_name" content="Hidden Den Cafe" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={fullCanonicalURL} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={fullOgImage} />
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={fullCanonicalURL} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={fullOgImage} />
|
||||
|
||||
<!-- Additional Meta Tags -->
|
||||
<meta name="author" content="Latte" />
|
||||
<meta name="keywords" content="self-hosted, privacy, open-source, furry, developer, cozy, hidden den" />
|
||||
<meta name="theme-color" content="#d4a574" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<!-- Additional Meta Tags -->
|
||||
<meta name="author" content="Latte" />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="self-hosted, privacy, open-source, furry, developer, cozy, hidden den"
|
||||
/>
|
||||
<meta name="theme-color" content="#d4a574" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<!-- Preconnect for performance (if needed for future external resources) -->
|
||||
<!-- <link rel="preconnect" href="https://example.com" crossorigin /> -->
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
<!-- Preconnect for performance (if needed for future external resources) -->
|
||||
<!-- <link rel="preconnect" href="https://example.com" crossorigin /> -->
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
:root {
|
||||
/* Cozy Den Color Palette */
|
||||
--color-bg: #1a1410;
|
||||
--color-bg-light: #2a1f18;
|
||||
--color-text: #f4e9d8;
|
||||
--color-text-dim: #c4b5a0;
|
||||
--color-accent: #d4a574;
|
||||
--color-accent-bright: #e8bf8e;
|
||||
--color-warm: #8b6f47;
|
||||
:root {
|
||||
/* Cozy Den Color Palette */
|
||||
--color-bg: #1a1410;
|
||||
--color-bg-light: #2a1f18;
|
||||
--color-text: #f4e9d8;
|
||||
--color-text-dim: #c4b5a0;
|
||||
--color-accent: #d4a574;
|
||||
--color-accent-bright: #e8bf8e;
|
||||
--color-warm: #8b6f47;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 1rem;
|
||||
--space-md: 1.5rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 3rem;
|
||||
/* Spacing */
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 1rem;
|
||||
--space-md: 1.5rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 3rem;
|
||||
|
||||
/* Typography */
|
||||
--font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-body);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-accent-bright);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent-bright);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Smooth animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
/* Typography */
|
||||
--font-body:
|
||||
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
html {
|
||||
font-family: var(--font-body);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve focus visibility for keyboard navigation */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Screen reader only content */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--color-accent-bright);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent-bright);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Smooth animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve focus visibility for keyboard navigation */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Screen reader only content */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
+112
-208
@@ -1,5 +1,9 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import Card from "../components/Card.astro";
|
||||
import Section from "../components/Section.astro";
|
||||
import ServiceItem from "../components/ServiceItem.astro";
|
||||
import SupportItem from "../components/SupportItem.astro";
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -18,137 +22,95 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
</section>
|
||||
|
||||
<!-- About Hidden Den Section -->
|
||||
<section class="section about-den" aria-labelledby="about-den-heading">
|
||||
<div class="container">
|
||||
<div class="card fade-in">
|
||||
<h2 id="about-den-heading">About Hidden Den</h2>
|
||||
<p>
|
||||
Welcome to Hidden Den Cafe - a warm, self-hosted space
|
||||
where technology meets comfort. This is a personal
|
||||
corner of the internet built with love, care, and a
|
||||
strong belief in privacy and open-source values.
|
||||
</p>
|
||||
<p>
|
||||
Everything here runs on self-hosted infrastructure,
|
||||
giving complete control over data and services. No cloud
|
||||
dependencies, no tracking, just a cozy digital home.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Section class="about-den" ariaLabelledby="about-den-heading">
|
||||
<Card>
|
||||
<h2 id="about-den-heading">About Hidden Den</h2>
|
||||
<p>
|
||||
Welcome to Hidden Den Cafe - a warm, self-hosted space where
|
||||
technology meets comfort. This is a personal corner of the
|
||||
internet built with love, care, and a strong belief in
|
||||
privacy and open-source values.
|
||||
</p>
|
||||
<p>
|
||||
Everything here runs on self-hosted infrastructure, giving
|
||||
complete control over data and services. No cloud
|
||||
dependencies, no tracking, just a cozy digital home.
|
||||
</p>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- About Latte Section -->
|
||||
<section class="section about-me" aria-labelledby="about-me-heading">
|
||||
<div class="container">
|
||||
<div class="card fade-in">
|
||||
<h2 id="about-me-heading">About Me</h2>
|
||||
<p>
|
||||
Hey there! I'm Latte, a gay furry developer who loves
|
||||
building things and being part of the warm, welcoming
|
||||
furry community. I'm passionate about self-hosting,
|
||||
open-source software, and creating cozy spaces both
|
||||
online and off.
|
||||
</p>
|
||||
<p>
|
||||
I work primarily with Python and Flask, though I'm
|
||||
always learning new things (currently exploring
|
||||
JavaScript and the C stack). When I get those surges of
|
||||
creative energy, I love diving into new projects - from
|
||||
Reddit downloaders to Telegram sticker tools, and
|
||||
everything in between.
|
||||
</p>
|
||||
<p>
|
||||
Coffee enthusiast • Linux lover • Self-hosting advocate
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Section class="about-me" ariaLabelledby="about-me-heading">
|
||||
<Card>
|
||||
<h2 id="about-me-heading">About Me</h2>
|
||||
<p>
|
||||
Hey there! I'm Latte, a gay furry developer who loves
|
||||
building things and being part of the warm, welcoming furry
|
||||
community. I'm passionate about self-hosting, open-source
|
||||
software, and creating cozy spaces both online and off.
|
||||
</p>
|
||||
<p>
|
||||
I work primarily with Python and Flask, though I'm always
|
||||
learning new things (currently exploring JavaScript and the
|
||||
C stack). When I get those surges of creative energy, I love
|
||||
diving into new projects - from Reddit downloaders to
|
||||
Telegram sticker tools, and everything in between.
|
||||
</p>
|
||||
<p>Coffee enthusiast • Linux lover • Self-hosting advocate</p>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="section services" aria-labelledby="services-heading">
|
||||
<div class="container">
|
||||
<div class="card fade-in">
|
||||
<h2 id="services-heading">Services</h2>
|
||||
<p>Here are the services currently running in the den:</p>
|
||||
<Section class="services" ariaLabelledby="services-heading">
|
||||
<Card>
|
||||
<h2 id="services-heading">Services</h2>
|
||||
<p>Here are the services currently running in the den:</p>
|
||||
|
||||
<div class="service-list" role="list">
|
||||
<div class="service-item" role="listitem">
|
||||
<h3>
|
||||
<a
|
||||
href="https://git.hiddenden.cafe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Visit Gitea - Self-hosted Git service"
|
||||
>
|
||||
Gitea
|
||||
</a>
|
||||
</h3>
|
||||
<p>
|
||||
Self-hosted Git service for all my projects and
|
||||
code repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="service-item coming-soon" role="listitem">
|
||||
<h3>More Coming Soon</h3>
|
||||
<p>
|
||||
The den is always growing! More services will be
|
||||
added as they're developed and deployed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-list" role="list">
|
||||
<ServiceItem
|
||||
title="Gitea"
|
||||
description="Self-hosted Git service for all my projects and code repositories."
|
||||
url="https://git.hiddenden.cafe"
|
||||
/>
|
||||
<ServiceItem
|
||||
title="More Coming Soon"
|
||||
description="The den is always growing! More services will be added as they're developed and deployed."
|
||||
comingSoon={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- Support Section -->
|
||||
<section class="section support" aria-labelledby="support-heading">
|
||||
<div class="container">
|
||||
<div class="card fade-in">
|
||||
<h2 id="support-heading">How to Help Out</h2>
|
||||
<p>
|
||||
If you'd like to support the Hidden Den and help keep
|
||||
the lights on, here are some ways you can contribute:
|
||||
</p>
|
||||
<Section class="support" ariaLabelledby="support-heading">
|
||||
<Card>
|
||||
<h2 id="support-heading">How to Help Out</h2>
|
||||
<p>
|
||||
If you'd like to support the Hidden Den and help keep the
|
||||
lights on, here are some ways you can contribute:
|
||||
</p>
|
||||
|
||||
<div class="support-list" role="list">
|
||||
<div class="support-item" role="listitem">
|
||||
<h3>Share & Spread the Word</h3>
|
||||
<p>
|
||||
Tell others about the projects and services
|
||||
hosted here!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="support-item" role="listitem">
|
||||
<h3>Report Issues</h3>
|
||||
<p>
|
||||
Found a bug or have a suggestion? Let me know!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="support-item" role="listitem">
|
||||
<h3>Buy Me a Coffee</h3>
|
||||
<p>
|
||||
Donations help cover server costs and keep the
|
||||
den cozy.
|
||||
<span class="coming-soon-text"
|
||||
>(Payment links coming soon!)</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="support-item" role="listitem">
|
||||
<h3>Contribute</h3>
|
||||
<p>
|
||||
Check out the projects on Gitea - contributions
|
||||
are always welcome!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="support-list" role="list">
|
||||
<SupportItem
|
||||
title="Share & Spread the Word"
|
||||
description="Tell others about the projects and services hosted here!"
|
||||
/>
|
||||
<SupportItem
|
||||
title="Report Issues"
|
||||
description="Found a bug or have a suggestion? Let me know!"
|
||||
/>
|
||||
<SupportItem
|
||||
title="Buy Me a Coffee"
|
||||
description="Donations help cover server costs and keep the den cozy."
|
||||
comingSoonNote="Payment links coming soon!"
|
||||
/>
|
||||
<SupportItem
|
||||
title="Contribute"
|
||||
description="Check out the projects on Gitea - contributions are always welcome!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer" role="contentinfo">
|
||||
@@ -166,6 +128,32 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Scroll Animation Script -->
|
||||
<script>
|
||||
// Intersection Observer for scroll animations
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible");
|
||||
// Optionally unobserve after animation
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all elements with scroll-animate class
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const animateElements =
|
||||
document.querySelectorAll(".scroll-animate");
|
||||
animateElements.forEach((el) => observer.observe(el));
|
||||
});
|
||||
</script>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
@@ -204,91 +192,15 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
padding: var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--color-bg-light);
|
||||
border-radius: 12px;
|
||||
padding: var(--space-lg);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(212, 165, 116, 0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin-bottom: var(--space-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.card p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Services Section */
|
||||
.service-list {
|
||||
/* Service and Support Lists */
|
||||
.service-list,
|
||||
.support-list {
|
||||
margin-top: var(--space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.service-item {
|
||||
background: var(--color-bg);
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.service-item h3 {
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.service-item p {
|
||||
color: var(--color-text-dim);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.service-item.coming-soon {
|
||||
border-left-color: var(--color-warm);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Support Section */
|
||||
.support-list {
|
||||
margin-top: var(--space-md);
|
||||
display: grid;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.support-item {
|
||||
background: var(--color-bg);
|
||||
padding: var(--space-md);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.support-item h3 {
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.support-item p {
|
||||
color: var(--color-text-dim);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.coming-soon-text {
|
||||
font-style: italic;
|
||||
color: var(--color-warm);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
@@ -310,10 +222,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--color-warm);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
@@ -323,9 +231,5 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user