diff --git a/.astro/types.d.ts b/.astro/types.d.ts index f964fe0..03d7cc4 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1 +1,2 @@ /// +/// \ No newline at end of file diff --git a/node_modules/.vite/deps/_metadata.json b/node_modules/.vite/deps/_metadata.json index b035c65..ee29d2a 100644 --- a/node_modules/.vite/deps/_metadata.json +++ b/node_modules/.vite/deps/_metadata.json @@ -1,25 +1,25 @@ { - "hash": "7113b21e", - "configHash": "2cd4a4ea", + "hash": "492ba4b0", + "configHash": "c5e12679", "lockfileHash": "53cd0e09", - "browserHash": "f4d46c12", + "browserHash": "c02b8ebb", "optimized": { "astro > cssesc": { "src": "../../.pnpm/cssesc@3.0.0/node_modules/cssesc/cssesc.js", "file": "astro___cssesc.js", - "fileHash": "dc615560", + "fileHash": "22e06d75", "needsInterop": true }, "astro > aria-query": { "src": "../../.pnpm/aria-query@5.3.2/node_modules/aria-query/lib/index.js", "file": "astro___aria-query.js", - "fileHash": "53d05d83", + "fileHash": "c870f0ee", "needsInterop": true }, "astro > axobject-query": { "src": "../../.pnpm/axobject-query@4.1.0/node_modules/axobject-query/lib/index.js", "file": "astro___axobject-query.js", - "fileHash": "35e7ec58", + "fileHash": "b44b4671", "needsInterop": true } }, diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/favicons/apple-touch-icon.png b/public/favicons/apple-touch-icon.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/favicons/apple-touch-icon.png differ diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/favicons/favicon-16x16.png differ diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/favicons/favicon-32x32.png differ diff --git a/public/favicons/favicon.png b/public/favicons/favicon.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/public/favicons/favicon.png differ diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..5352c2a --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Justin Deal", + "short_name": "Justin Deal", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#282828", + "background_color": "#ebdbb2", + "display": "standalone", + "start_url": "/" +} diff --git a/src/assets/images/michael-dam-unsplash.webp b/src/assets/images/michael-dam-unsplash.webp new file mode 100644 index 0000000..7f18027 Binary files /dev/null and b/src/assets/images/michael-dam-unsplash.webp differ diff --git a/src/assets/images/pixel_avatar.png b/src/assets/images/pixel_avatar.png new file mode 100644 index 0000000..02692ea Binary files /dev/null and b/src/assets/images/pixel_avatar.png differ diff --git a/src/assets/images/zaggonaut.png b/src/assets/images/zaggonaut.png new file mode 100644 index 0000000..76fc703 Binary files /dev/null and b/src/assets/images/zaggonaut.png differ diff --git a/src/components/SEO.astro b/src/components/SEO.astro new file mode 100644 index 0000000..58ac47c --- /dev/null +++ b/src/components/SEO.astro @@ -0,0 +1,72 @@ +--- +interface Props { + title?: string; + description?: string; + image?: string; + article?: boolean; + canonicalUrl?: string; + publishedTime?: string; + modifiedTime?: string; + tags?: string[]; +} + +import { GLOBAL } from "../lib/variables"; + +const { + title = GLOBAL.username, + description = GLOBAL.longDescription, + image = `${GLOBAL.rootUrl}/assets/images/${GLOBAL.profileImage}`, + article = false, + canonicalUrl = Astro.url.href, + publishedTime, + modifiedTime, + tags = [], +} = Astro.props; + +// Format the title with site name +const formattedTitle = title === GLOBAL.username + ? `${title} • ${GLOBAL.shortDescription}` + : `${title} • ${GLOBAL.username}`; +--- + + +{formattedTitle} + + + + + + + + + + + + + + + + + + + + + + +{article && publishedTime && ( + +)} +{article && modifiedTime && ( + +)} +{article && tags.length > 0 && ( + tags.map(tag => ) +)} + + + + + + + + diff --git a/src/components/SearchScript.astro b/src/components/SearchScript.astro index ad34a51..966ab1a 100644 --- a/src/components/SearchScript.astro +++ b/src/components/SearchScript.astro @@ -232,7 +232,7 @@ function initializeSearch(contentSelector = '.searchable-item', options = {}) { document.addEventListener('alpine:init', () => { // Homelab search window.Alpine.data('searchServices', () => { - return initializeSearch('.app-card', { + const baseSearch = initializeSearch('.app-card', { nameAttribute: 'data-app-name', tagsAttribute: 'data-app-tags', categoryAttribute: 'data-app-category', @@ -241,6 +241,112 @@ document.addEventListener('alpine:init', () => { resultCountMessage: (count) => `Found ${count} services`, itemLabel: 'services' }); + + // Add icon size slider functionality + return { + ...baseSearch, + iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large + iconSize: 'medium', // small, medium, large + viewMode: 'grid', // grid or list + debounceTimeout: null, // For debouncing slider changes + + init() { + baseSearch.init.call(this); + + // Apply initial icon size and view mode + this.applyIconSize(); + this.applyViewMode(); + }, + + // Icon size methods + setIconSize(size) { + if (typeof size === 'string') { + // Handle legacy string values (small, medium, large) + this.iconSize = size; + this.iconSizeValue = size === 'small' ? 1 : size === 'medium' ? 2 : 3; + } else { + // Handle slider numeric values + this.iconSizeValue = parseFloat(size); + + // Map slider value to size name + if (this.iconSizeValue <= 1.33) { + this.iconSize = 'small'; + } else if (this.iconSizeValue <= 2.33) { + this.iconSize = 'medium'; + } else { + this.iconSize = 'large'; + } + } + + this.applyIconSize(); + }, + + // Handle slider input with debounce + handleSliderChange(event) { + const value = event.target.value; + + // Clear any existing timeout + if (this.debounceTimeout) { + clearTimeout(this.debounceTimeout); + } + + // Set a new timeout + this.debounceTimeout = setTimeout(() => { + this.setIconSize(value); + }, 50); // 50ms debounce + }, + + applyIconSize() { + const appList = document.getElementById('app-list'); + if (!appList) return; + + // Remove existing size classes + appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large'); + + // Add the new size class + appList.classList.add(`icon-size-${this.iconSize}`); + + // Apply custom CSS variable for fine-grained control + appList.style.setProperty('--icon-scale', this.iconSizeValue); + }, + + // View mode methods + toggleViewMode() { + this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid'; + this.applyViewMode(); + }, + + setViewMode(mode) { + this.viewMode = mode; + this.applyViewMode(); + }, + + applyViewMode() { + const appList = document.getElementById('app-list'); + if (!appList) return; + + // Remove existing view mode classes + appList.classList.remove('view-mode-grid', 'view-mode-list'); + + // Add the new view mode class + appList.classList.add(`view-mode-${this.viewMode}`); + + // Update all category sections + document.querySelectorAll('.category-section').forEach(section => { + const gridContainer = section.querySelector('.grid'); + if (gridContainer) { + // Update grid classes based on view mode + if (this.viewMode === 'grid') { + gridContainer.classList.remove('grid-cols-1'); + gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4'); + } else { + gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4'); + gridContainer.classList.add('grid-cols-1'); + } + } + }); + } + }; }); // Blog search diff --git a/src/components/StructuredData.astro b/src/components/StructuredData.astro new file mode 100644 index 0000000..01c27a6 --- /dev/null +++ b/src/components/StructuredData.astro @@ -0,0 +1,88 @@ +--- +interface Props { + type: 'WebSite' | 'WebPage' | 'Article' | 'Person' | 'BreadcrumbList' | 'FAQPage'; + data: Record; +} + +const { type, data } = Astro.props; + +// Base structured data templates +const templates = { + WebSite: { + "@context": "https://schema.org", + "@type": "WebSite", + "name": "", + "url": "", + "description": "", + "potentialAction": { + "@type": "SearchAction", + "target": "{search_term_string}", + "query-input": "required name=search_term_string" + } + }, + WebPage: { + "@context": "https://schema.org", + "@type": "WebPage", + "name": "", + "description": "", + "url": "", + "isPartOf": { + "@type": "WebSite", + "name": "", + "url": "" + } + }, + Article: { + "@context": "https://schema.org", + "@type": "Article", + "headline": "", + "description": "", + "image": "", + "datePublished": "", + "dateModified": "", + "author": { + "@type": "Person", + "name": "", + "url": "" + }, + "publisher": { + "@type": "Person", + "name": "", + "url": "" + }, + "mainEntityOfPage": { + "@type": "WebPage", + "@id": "" + } + }, + Person: { + "@context": "https://schema.org", + "@type": "Person", + "name": "", + "url": "", + "jobTitle": "", + "sameAs": [] + }, + BreadcrumbList: { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [] + }, + FAQPage: { + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [] + } +}; + +// Merge template with provided data +const structuredData = { + ...templates[type], + ...data +}; + +// Stringify the data for output +const jsonLd = JSON.stringify(structuredData, null, 2); +--- + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/types.ts b/src/lib/types.ts index 1f9c898..126f161 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,3 +1,6 @@ +/** + * Represents the frontmatter for a project + */ export type ProjectFrontmatter = { /** * The title of the project @@ -43,6 +46,9 @@ export type ProjectFrontmatter = { filename: string; }; +/** + * Represents the frontmatter for an article + */ export type ArticleFrontmatter = { /** * The title of the article @@ -50,7 +56,7 @@ export type ArticleFrontmatter = { title: string; /** - * THe summary description of the article + * The summary description of the article */ description: string; @@ -81,3 +87,40 @@ export type ArticleFrontmatter = { */ filename: string; }; + +/** + * Represents a service in the homelab + */ +export type Service = { + /** + * The name of the service + */ + name: string; + + /** + * The URL to the service + */ + link: string; + + /** + * The URL to the service icon + */ + icon: string; + + /** + * Alternative text for the service icon + */ + alt: string; + + /** + * Tags associated with the service for filtering and categorization + */ + tags?: string[]; +}; + +/** + * Represents a category of services in the homelab + */ +export type ServiceCategory = { + [category: string]: Service[]; +}; diff --git a/src/lib/variables.ts b/src/lib/variables.ts index e2731f8..75c12a5 100644 --- a/src/lib/variables.ts +++ b/src/lib/variables.ts @@ -1,5 +1,28 @@ // Set any item to undefined to remove it from the site or to use the default value +/** + * Global variables used throughout the site + * @property {string} username - The username displayed on the site + * @property {string} rootUrl - The root URL of the site + * @property {string} shortDescription - A short description of the site + * @property {string} longDescription - A longer description of the site + * @property {string} githubProfile - The GitHub profile URL + * @property {string} giteaProfile - The Gitea profile URL + * @property {string} linkedinProfile - The LinkedIn profile URL + * @property {string} articlesName - The name used for articles + * @property {string} projectsName - The name used for projects + * @property {string} viewAll - The text used for "View All" links + * @property {string} noArticles - The text used when there are no articles + * @property {string} noProjects - The text used when there are no projects + * @property {string} blogTitle - The title of the blog section + * @property {string} blogShortDescription - A short description of the blog + * @property {string} blogLongDescription - A longer description of the blog + * @property {string} projectTitle - The title of the projects section + * @property {string} projectShortDescription - A short description of the projects + * @property {string} projectLongDescription - A longer description of the projects + * @property {string} profileImage - The profile image filename + * @property {Object} menu - The menu items + */ export const GLOBAL = { // Site metadata username: "Justin Deal", @@ -9,8 +32,8 @@ export const GLOBAL = { // Social media links githubProfile: "https://github.com/justindeal", - twitterProfile: "https://twitter.com/", - linkedinProfile: "https://www.linkedin.com/", + giteaProfile: "https://code.justin.deal/dealjus", + linkedinProfile: "https://www.linkedin.com/in/justin-deal/", // Common text names used throughout the site articlesName: "Articles", diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 2e13d5c..40eacce 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -7,7 +7,6 @@ import Section from "../../components/common/Section.astro"; import SearchBar from "../../components/common/SearchBar.astro"; import { articles } from "../../lib/list"; import { countTags } from "../../lib/utils"; -import { initializeSearch } from "../../components/common/searchUtils.js"; const tagCounts = countTags(articles.map((article) => article.tags).flat().filter((tag): tag is string => tag !== undefined)); @@ -39,7 +38,7 @@ const tagCounts = countTags(articles.map((article) => article.tags).flat().filte - +
diff --git a/src/pages/homelab/index.astro b/src/pages/homelab/index.astro index 7b4ca93..be002dd 100644 --- a/src/pages/homelab/index.astro +++ b/src/pages/homelab/index.astro @@ -3,42 +3,60 @@ 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 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"; -import { initializeSearch } from "../../components/common/searchUtils.js"; + +// 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 + } +}; --- - - {GLOBAL.username} • {GLOBAL.shortDescription} - - - - - - - - - - - - - + + - +
- +
-
+ +
+ + +
+ +
diff --git a/src/pages/homelab/services.ts b/src/pages/homelab/services.ts index d145bb8..4820fa6 100644 --- a/src/pages/homelab/services.ts +++ b/src/pages/homelab/services.ts @@ -1,4 +1,9 @@ -export const services = { +import { type Service, type ServiceCategory } from "../../lib/types"; + +/** + * Services available in the homelab, organized by category + */ +export const services: ServiceCategory = { Websites: [ { name: "https://justin.deal", diff --git a/src/pages/index.astro b/src/pages/index.astro index 3a78a74..6a0f138 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,39 +6,37 @@ import Hero from "../components/home/Hero.astro"; import Section from "../components/common/Section.astro"; import FeaturedProjects from "../components/home/FeaturedProjects.astro"; import FeaturedArticles from "../components/home/FeaturedArticles.astro"; +import SEO from "../components/SEO.astro"; +import StructuredData from "../components/StructuredData.astro"; + +// Structured data for the home page +const websiteData = { + name: `${GLOBAL.username} • ${GLOBAL.shortDescription}`, + url: GLOBAL.rootUrl, + description: GLOBAL.longDescription, + potentialAction: { + "@type": "SearchAction", + "target": `${GLOBAL.rootUrl}/search?q={search_term_string}`, + "query-input": "required name=search_term_string" + } +}; + +const personData = { + name: GLOBAL.username, + url: GLOBAL.rootUrl, + jobTitle: "Software Developer", + sameAs: [ + GLOBAL.githubProfile, + GLOBAL.linkedinProfile, + GLOBAL.giteaProfile + ] +}; --- - - {GLOBAL.username} • {GLOBAL.shortDescription} - - - - - - - - - - - - - + + +
diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index c282184..0147c1a 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -6,7 +6,6 @@ import ProjectSnippetSkeleton from "../../components/ProjectSnippetSkeleton.astr import SearchBar from "../../components/common/SearchBar.astro"; import Layout from "../../layouts/Layout.astro"; import { GLOBAL } from "../../lib/variables"; -import { initializeSearch } from "../../components/common/searchUtils.js"; --- @@ -35,7 +34,7 @@ import { initializeSearch } from "../../components/common/searchUtils.js"; - +
diff --git a/src/pages/projects/this-site.md b/src/pages/projects/this-site.md index ca0daeb..bf3c639 100644 --- a/src/pages/projects/this-site.md +++ b/src/pages/projects/this-site.md @@ -2,8 +2,8 @@ layout: ../../layouts/ProjectLayout.astro title: This Website 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 +tags: ["astro", "typescript", "tailwindcss", "alpinejs", "gruvbox-theme"] +githubUrl: https://code.justin.deal/dealjus/justin.deal timestamp: 2025-02-24T02:39:03+00:00 featured: true filename: this-site diff --git a/src/styles/global.css b/src/styles/global.css index cb5338e..3284daf 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -13,6 +13,19 @@ html.theme-loaded body { transition: background-color 0.3s ease, color 0.3s ease; } +/* Font loading states */ +html:not(.fonts-loaded) body { + /* Fallback font metrics that match your custom font */ + font-family: monospace; +} + +html.fonts-loaded body { + /* Your custom font */ + font-family: var(--font-mono); + /* Add a subtle transition for font changes */ + transition: font-family 0.1s ease-out; +} + /* Hide elements with x-cloak until Alpine.js is loaded */ [x-cloak] { display: none !important; @@ -24,10 +37,11 @@ html.theme-loaded body { outline-offset: 2px; } +/* Font declarations with optimized loading strategies */ @font-face { font-family: "Literata Variable"; font-style: normal; - font-display: swap; + font-display: swap; /* Use swap for text fonts */ font-weight: 200 900; src: url(https://cdn.jsdelivr.net/fontsource/fonts/literata:vf@latest/latin-opsz-normal.woff2) format("woff2-variations"); @@ -39,7 +53,7 @@ html.theme-loaded body { @font-face { font-family: "press-start-2p"; font-style: normal; - font-display: swap; + font-display: optional; /* Use optional for decorative fonts */ font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff2) format("woff2"), diff --git a/tsconfig.json b/tsconfig.json index 2e85c92..b9eced4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,16 @@ "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { - "allowImportingTsExtensions": true + "allowImportingTsExtensions": true, + "exactOptionalPropertyTypes": false, // Temporarily disabled to fix component issues + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } } }