diff --git a/src/components/ArticleSnippetSkeleton.astro b/src/components/ArticleSnippetSkeleton.astro new file mode 100644 index 0000000..b948b12 --- /dev/null +++ b/src/components/ArticleSnippetSkeleton.astro @@ -0,0 +1,33 @@ +--- +import SkeletonLoader from "./common/SkeletonLoader.astro"; + +interface Props { + className?: string; +} + +const { className = "" } = Astro.props; +--- + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
diff --git a/src/components/ProjectSnippetSkeleton.astro b/src/components/ProjectSnippetSkeleton.astro new file mode 100644 index 0000000..3fa1f80 --- /dev/null +++ b/src/components/ProjectSnippetSkeleton.astro @@ -0,0 +1,43 @@ +--- +import SkeletonLoader from "./common/SkeletonLoader.astro"; + +interface Props { + className?: string; +} + +const { className = "" } = Astro.props; +--- + +
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + + +
+
+
+
diff --git a/src/components/SearchScript.astro b/src/components/SearchScript.astro index 8e363bf..ad34a51 100644 --- a/src/components/SearchScript.astro +++ b/src/components/SearchScript.astro @@ -24,16 +24,23 @@ function initializeSearch(contentSelector = '.searchable-item', options = {}) { searchQuery: '', hasResults: true, visibleCount: 0, + loading: false, // Start with loading state false - the LoadingManager will control this init() { // Initialize the visible count this.visibleCount = document.querySelectorAll(contentSelector).length; this.setupWatchers(); this.setupKeyboardShortcuts(); + + // Handle theme changes + window.addEventListener('theme-changed', () => { + this.filterContent(this.searchQuery); + }); }, setupWatchers() { this.$watch('searchQuery', (query) => { + // Filter content immediately - no artificial delay this.filterContent(query); }); }, diff --git a/src/components/common/LoadingOverlay.astro b/src/components/common/LoadingOverlay.astro new file mode 100644 index 0000000..84addcc --- /dev/null +++ b/src/components/common/LoadingOverlay.astro @@ -0,0 +1,224 @@ +--- +interface Props { + message?: string; + subMessage?: string; + className?: string; +} + +const { + message = "This is taking longer than expected...", + subMessage = "Still working on loading your content", + className = "" +} = Astro.props; +--- + + + + + + diff --git a/src/components/common/ServiceCardSkeleton.astro b/src/components/common/ServiceCardSkeleton.astro new file mode 100644 index 0000000..4503cc6 --- /dev/null +++ b/src/components/common/ServiceCardSkeleton.astro @@ -0,0 +1,30 @@ +--- +import SkeletonLoader from "./SkeletonLoader.astro"; + +interface Props { + className?: string; +} + +const { className = "" } = Astro.props; +--- + +
+
+ +
+ +
+ + +
+ +
+
+
+ + diff --git a/src/components/common/SkeletonLoader.astro b/src/components/common/SkeletonLoader.astro new file mode 100644 index 0000000..b3d48dd --- /dev/null +++ b/src/components/common/SkeletonLoader.astro @@ -0,0 +1,134 @@ +--- +interface Props { + type?: 'card' | 'text' | 'image' | 'title' | 'paragraph' | 'custom'; + width?: string; + height?: string; + className?: string; + rounded?: boolean; + count?: number; + animate?: boolean; +} + +const { + type = 'card', + width, + height, + className = '', + rounded = false, + count = 1, + animate = true, +} = Astro.props; + +// Determine dimensions based on type +let defaultWidth = '100%'; +let defaultHeight = '100%'; + +switch (type) { + case 'card': + defaultWidth = '100%'; + defaultHeight = '200px'; + break; + case 'text': + defaultWidth = '100%'; + defaultHeight = '1rem'; + break; + case 'image': + defaultWidth = '100%'; + defaultHeight = '150px'; + break; + case 'title': + defaultWidth = '70%'; + defaultHeight = '1.5rem'; + break; + case 'paragraph': + defaultWidth = '100%'; + defaultHeight = '4rem'; + break; +} + +const finalWidth = width || defaultWidth; +const finalHeight = height || defaultHeight; +const roundedClass = rounded ? 'rounded-lg' : ''; +const animateClass = animate ? 'animate-pulse' : ''; + +// Generate multiple skeleton items if count > 1 +const items = Array.from({ length: count }, (_, i) => i); +--- + +{ + items.map((_, index) => ( + + )) +} + + diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index f0a20d7..d93875e 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -2,6 +2,7 @@ import Footer from "../components/Footer.astro"; import Header from "../components/Header.astro"; import SearchScript from "../components/SearchScript.astro"; +import LoadingOverlay from "../components/common/LoadingOverlay.astro"; import "../styles/global.css"; --- @@ -51,6 +52,9 @@ import "../styles/global.css"; + + +
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 7812018..2e13d5c 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -2,6 +2,7 @@ import { GLOBAL } from "../../lib/variables"; import Layout from "../../layouts/Layout.astro"; import ArticleSnippet from "../../components/ArticleSnippet.astro"; +import ArticleSnippetSkeleton from "../../components/ArticleSnippetSkeleton.astro"; import Section from "../../components/common/Section.astro"; import SearchBar from "../../components/common/SearchBar.astro"; import { articles } from "../../lib/list"; @@ -41,7 +42,7 @@ const tagCounts = countTags(articles.map((article) => article.tags).flat().filte
-
+
@@ -64,7 +65,29 @@ const tagCounts = countTags(articles.map((article) => article.tags).flat().filte >

No Articles Found

-
    + +
    + {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
    + + +
      { articles.map((article) => { const articleTags = article.tags ? article.tags.join(' ').toLowerCase() : ''; diff --git a/src/pages/homelab/index.astro b/src/pages/homelab/index.astro index 28fc7b3..7b4ca93 100644 --- a/src/pages/homelab/index.astro +++ b/src/pages/homelab/index.astro @@ -4,6 +4,8 @@ import Layout from "../../layouts/Layout.astro"; import Section from "../../components/common/Section.astro"; import SearchBar from "../../components/common/SearchBar.astro"; import CategorySection from "../../components/homelab/CategorySection.astro"; +import ServiceCardSkeleton from "../../components/common/ServiceCardSkeleton.astro"; +import SkeletonLoader from "../../components/common/SkeletonLoader.astro"; import { services } from "./services.ts"; import { initializeSearch } from "../../components/common/searchUtils.js"; --- @@ -28,7 +30,7 @@ import { initializeSearch } from "../../components/common/searchUtils.js";
      -
      +
      @@ -53,8 +55,38 @@ import { initializeSearch } from "../../components/common/searchUtils.js";

      No Results

      - -
      + +
      + {Object.entries(services).map(([category, apps]) => ( +
      +
      + +
      +
      + {Array.from({ length: apps.length || 4 }).map((_, i) => ( + + ))} +
      +
      + ))} +
      + + +
      {Object.entries(services).map(([category, apps]) => ( ))} diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index 9466b5f..c282184 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -2,6 +2,7 @@ import { projects } from "../../lib/list"; import Section from "../../components/common/Section.astro"; import ProjectSnippet from "../../components/ProjectSnippet.astro"; +import ProjectSnippetSkeleton from "../../components/ProjectSnippetSkeleton.astro"; import SearchBar from "../../components/common/SearchBar.astro"; import Layout from "../../layouts/Layout.astro"; import { GLOBAL } from "../../lib/variables"; @@ -37,7 +38,7 @@ import { initializeSearch } from "../../components/common/searchUtils.js";
      -
      +
      @@ -61,7 +62,29 @@ import { initializeSearch } from "../../components/common/searchUtils.js";

      No Projects Found

      -
        + +
        + {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
        + + +
          { projects.map((project) => { const projectTags = project.tags ? project.tags.join(' ').toLowerCase() : ''; diff --git a/src/pages/projects/zaggonaut.md b/src/pages/projects/zaggonaut.md index 2f1d1d0..009d076 100644 --- a/src/pages/projects/zaggonaut.md +++ b/src/pages/projects/zaggonaut.md @@ -1,8 +1,8 @@ --- layout: ../../layouts/ProjectLayout.astro title: This Website -description: My personal blog and portfolio, built using TypeScript, TailwindCSS, and Astro. -tags: ["astro", "typescript", "web-development"] +description: My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js featuring smart loading, advanced search, and accessibility features. +tags: ["astro", "typescript", "tailwindcss", "alpinejs", "responsive-design", "accessibility", "dark-mode", "gruvbox-theme"] githubUrl: https://code.justin.deal timestamp: 2025-02-24T02:39:03+00:00 featured: true @@ -11,17 +11,59 @@ filename: zaggonaut ## The Details -Zaggonaut is a retro-inspired theme for Astro, built using TypeScript, TailwindCSS, and Astro. Use this theme to power your own personal website, blog, or portfolio with flexibility and customization. +This website serves as my personal digital space, combining a blog, project portfolio, and homelab dashboard in one cohesive experience. Built on Astro for performance and flexibility, it leverages TypeScript for type safety, TailwindCSS for styling, and Alpine.js for interactive components. + +The architecture follows a component-based approach with a focus on reusability and maintainability. The site features a custom Gruvbox-inspired theme system that supports both dark and light modes while maintaining accessibility standards. The responsive design ensures a seamless experience across all device sizes. ## The Features -- Dark & light mode -- Customizable colors -- 100 / 100 Lighthouse score -- Fully accessible -- Fully responsive -- Type-safe +### Content & Navigation +- **Multi-purpose platform**: Blog, portfolio, and homelab dashboard integration +- **Dark & light mode** with smooth transitions and flash prevention +- **Gruvbox-inspired color scheme** with customizable theme variables +- **Fully responsive design** optimized for all screen sizes -## The Future +### Performance & User Experience +- **Smart loading system**: + - Connection-aware loading states that adapt to network speed + - Skeleton loaders that match content layout during loading + - Extended loading fallback UI for slow connections + - Navigation API integration for accurate loading states -Check out [the theme website](https://zaggonaut.dev) to see it in action! \ No newline at end of file +- **Advanced search functionality**: + - Real-time content filtering across all sections + - Keyboard navigation with arrow keys and shortcuts + - Category-aware filtering for structured content + - Accessible search results with screen reader support + +### Technical Implementation +- **Component architecture**: + - Reusable UI components with TypeScript interfaces + - Consistent styling patterns using CSS custom properties + - Modular structure for easy maintenance and extension + +- **Accessibility features**: + - ARIA attributes for screen reader compatibility + - Keyboard navigation throughout the site + - Reduced motion support for animations + - High contrast theme options + +- **Performance optimizations**: + - Optimized page transitions with minimal flicker + - Network-aware resource loading + - Efficient DOM updates using Alpine.js + - Astro's static site generation for fast initial loads + +### Homelab Integration +- **Self-hosted services dashboard** with categorized listings +- **Service search and filtering** by name, category, and tags +- **Visual indicators** for service status and information + +## The Technology Stack + +- **Framework**: Astro 5.6 +- **Languages**: TypeScript, JavaScript +- **Styling**: TailwindCSS 4.1, Custom CSS Variables +- **Interactivity**: Alpine.js +- **Build Tools**: Vite, npm/pnpm +- **Deployment**: Self-hosted diff --git a/src/styles/global.css b/src/styles/global.css index e73557b..cb5338e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -13,6 +13,17 @@ html.theme-loaded body { transition: background-color 0.3s ease, color 0.3s ease; } +/* Hide elements with x-cloak until Alpine.js is loaded */ +[x-cloak] { + display: none !important; +} + +/* Add keyboard focus styling for keyboard navigation */ +.keyboard-focus { + outline: 2px solid var(--color-zag-accent-dark); + outline-offset: 2px; +} + @font-face { font-family: "Literata Variable"; font-style: normal;