Make configs json and have light/dark images for socials
All checks were successful
Build and Deploy / build (push) Successful in 40s
All checks were successful
Build and Deploy / build (push) Successful in 40s
This commit is contained in:
parent
e56520c9e8
commit
a80b2a5e01
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@ -1,2 +1 @@
|
|||||||
/// <reference types="astro/client" />
|
/// <reference types="astro/client" />
|
||||||
/// <reference path="content.d.ts" />
|
|
56
config/README.md
Normal file
56
config/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Site Configuration
|
||||||
|
|
||||||
|
This directory contains configuration files for the website. Instead of hardcoding values in the TypeScript files, these JSON files are used to make the site more configurable.
|
||||||
|
|
||||||
|
## Available Configuration Files
|
||||||
|
|
||||||
|
- **site.json**: Contains global site metadata, menu structure, and text strings
|
||||||
|
- **services.json**: Contains homelab services organized by category
|
||||||
|
- **socials.json**: Contains social media profile configurations (the single source of truth for all social profiles)
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
To modify any configuration values, simply edit the appropriate JSON file. The changes will be reflected in the application without having to modify any TypeScript code.
|
||||||
|
|
||||||
|
### Example: Updating Menu Items
|
||||||
|
|
||||||
|
To add or remove a menu item, edit the `menu` section in `site.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"menu": {
|
||||||
|
"home": "/",
|
||||||
|
"about": "/about",
|
||||||
|
"blog": "/blog",
|
||||||
|
"projects": "/projects",
|
||||||
|
"homelab": "/homelab",
|
||||||
|
"code": "https://code.justin.deal",
|
||||||
|
"new-page": "/new-page"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Adding a New Service
|
||||||
|
|
||||||
|
To add a new service, find the appropriate category in `services.json` and add a new item:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Media": [
|
||||||
|
{
|
||||||
|
"name": "Jellyfin",
|
||||||
|
"link": "https://watch.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
|
||||||
|
"alt": "Jellyfin",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Plex",
|
||||||
|
"link": "https://plex.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/plex.svg",
|
||||||
|
"alt": "Plex Media Server",
|
||||||
|
"tags": ["media", "streaming"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
The configuration files are loaded from this directory using the `loadConfig` function in `src/lib/config.ts`. The function reads the JSON files, parses them, and caches the results to avoid reading the files multiple times.
|
153
config/services.json
Normal file
153
config/services.json
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"Websites": [
|
||||||
|
{
|
||||||
|
"name": "justin.deal",
|
||||||
|
"link": "https://justin.deal",
|
||||||
|
"icon": "/pixel_avatar.png",
|
||||||
|
"alt": "Personal Website"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Utilities": [
|
||||||
|
{
|
||||||
|
"name": "Silverbullet",
|
||||||
|
"link": "https://notes.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/silverbullet.png",
|
||||||
|
"alt": "Silverbullet",
|
||||||
|
"tags": ["notes", "markdown", "knowledge base"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vikunja",
|
||||||
|
"link": "https://todo.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vikunja.svg",
|
||||||
|
"alt": "Vikunja",
|
||||||
|
"tags": ["todo", "tasks", "productivity"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Actual",
|
||||||
|
"link": "https://budget.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/actual-budget.svg",
|
||||||
|
"alt": "Actual",
|
||||||
|
"tags": ["finance", "budget", "money"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Searxng",
|
||||||
|
"link": "https://search.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng.svg",
|
||||||
|
"alt": "Searxng",
|
||||||
|
"tags": ["search", "privacy", "metasearch"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BaiKal",
|
||||||
|
"link": "https://dav.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/baikal.png",
|
||||||
|
"alt": "BaiKal",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cryptpad",
|
||||||
|
"link": "https://docs.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/cryptpad.svg",
|
||||||
|
"alt": "Cryptpad",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Development": [
|
||||||
|
{
|
||||||
|
"name": "Gitea",
|
||||||
|
"link": "https://code.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg",
|
||||||
|
"alt": "Gitea",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OpenGist",
|
||||||
|
"link": "https://snippets.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist.svg",
|
||||||
|
"alt": "OpenGist",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IT-Tools",
|
||||||
|
"link": "https://tools.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools.svg",
|
||||||
|
"alt": "IT-Tools",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Media": [
|
||||||
|
{
|
||||||
|
"name": "Jellyfin",
|
||||||
|
"link": "https://watch.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
|
||||||
|
"alt": "Jellyfin",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Calibre-Web",
|
||||||
|
"link": "https://books.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web.svg",
|
||||||
|
"alt": "Calibre-Web",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Analytics": [
|
||||||
|
{
|
||||||
|
"name": "Uptime Kuma",
|
||||||
|
"link": "https://status.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma.svg",
|
||||||
|
"alt": "Uptime Kuma",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Umami",
|
||||||
|
"link": "https://analytics.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami.svg",
|
||||||
|
"alt": "Umami",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TeslaMate",
|
||||||
|
"link": "https://tesla.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate.svg",
|
||||||
|
"alt": "TeslaMate",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Infrastructure": [
|
||||||
|
{
|
||||||
|
"name": "Pi-hole",
|
||||||
|
"link": "https://pihole.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole.svg",
|
||||||
|
"alt": "Pi-hole",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ntfy",
|
||||||
|
"link": "https://ntfy.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy.svg",
|
||||||
|
"alt": "Ntfy",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vaultwarden",
|
||||||
|
"link": "https://passwords.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden.svg",
|
||||||
|
"alt": "Vaultwarden",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authentik",
|
||||||
|
"link": "https://auth.justin.deal",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg",
|
||||||
|
"alt": "Authentik",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Traefik",
|
||||||
|
"link": "https://proxy.justin.deal:8080",
|
||||||
|
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik.svg",
|
||||||
|
"alt": "Traefik",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
32
config/site.json
Normal file
32
config/site.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"username": "Justin Deal",
|
||||||
|
"rootUrl": "https://justin.deal",
|
||||||
|
"shortDescription": "My personal slice of the internet",
|
||||||
|
"longDescription": "My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js.",
|
||||||
|
|
||||||
|
"articlesName": "Articles",
|
||||||
|
"projectsName": "Projects",
|
||||||
|
"viewAll": "View All",
|
||||||
|
|
||||||
|
"noArticles": "No featured articles yet.",
|
||||||
|
"noProjects": "No featured projects yet.",
|
||||||
|
|
||||||
|
"blogTitle": "My Thoughts & Takes",
|
||||||
|
"blogShortDescription": "Practical wisdom, unfiltered thoughts, and hot takes.",
|
||||||
|
"blogLongDescription": "Web development, tech trends, and the occasional programming mishap.",
|
||||||
|
|
||||||
|
"projectTitle": "Projects and Code",
|
||||||
|
"projectShortDescription": "A list of my web development projects and developer tools.",
|
||||||
|
"projectLongDescription": "All of my projects, including both frontend and full-stack applications.",
|
||||||
|
|
||||||
|
"profileImage": "pixel_avatar.png",
|
||||||
|
|
||||||
|
"menu": {
|
||||||
|
"home": "/",
|
||||||
|
"about": "/about",
|
||||||
|
"blog": "/blog",
|
||||||
|
"projects": "/projects",
|
||||||
|
"homelab": "/homelab",
|
||||||
|
"code": "https://code.justin.deal"
|
||||||
|
}
|
||||||
|
}
|
29
config/socials.json
Normal file
29
config/socials.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Gitea",
|
||||||
|
"url": "https://code.justin.deal/dealjus",
|
||||||
|
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg",
|
||||||
|
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-light.svg",
|
||||||
|
"alt": "Gitea Profile",
|
||||||
|
"showInFooter": true,
|
||||||
|
"showInAbout": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LinkedIn",
|
||||||
|
"url": "https://www.linkedin.com/in/justin-deal/",
|
||||||
|
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-dark.svg",
|
||||||
|
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-light.svg",
|
||||||
|
"alt": "LinkedIn Profile",
|
||||||
|
"showInFooter": true,
|
||||||
|
"showInAbout": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"url": "https://github.com/justintdeal",
|
||||||
|
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-dark.svg",
|
||||||
|
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-light.svg",
|
||||||
|
"alt": "GitHub Profile",
|
||||||
|
"showInFooter": true,
|
||||||
|
"showInAbout": true
|
||||||
|
}
|
||||||
|
]
|
@ -20,7 +20,8 @@ const footerSocials = socials.filter(social => social.showInFooter);
|
|||||||
<SocialIcon
|
<SocialIcon
|
||||||
name={social.name}
|
name={social.name}
|
||||||
url={social.url}
|
url={social.url}
|
||||||
icon={social.icon}
|
iconDark={social.iconDark}
|
||||||
|
iconLight={social.iconLight}
|
||||||
alt={social.alt}
|
alt={social.alt}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -4,12 +4,18 @@ import Anchor from "./Anchor.astro";
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: string;
|
iconDark?: string;
|
||||||
|
iconLight?: string;
|
||||||
|
icon?: string; // For backward compatibility
|
||||||
alt: string;
|
alt: string;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, url, icon, alt, size = "md" } = Astro.props;
|
const { name, url, iconDark, iconLight, icon, alt, size = "md" } = Astro.props;
|
||||||
|
|
||||||
|
// Use provided icons or fallback to the legacy icon prop
|
||||||
|
const darkIcon = iconDark || icon;
|
||||||
|
const lightIcon = iconLight || icon;
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "w-6 h-6",
|
sm: "w-6 h-6",
|
||||||
@ -19,9 +25,28 @@ const sizeClasses = {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Anchor url={url} aria-label={alt}>
|
<Anchor url={url} aria-label={alt}>
|
||||||
|
<!-- Dark icon shown in light theme -->
|
||||||
<img
|
<img
|
||||||
src={icon}
|
src={darkIcon}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
class={`${sizeClasses[size]} zag-transition`}
|
class={`${sizeClasses[size]} light-theme-only zag-transition`}
|
||||||
|
/>
|
||||||
|
<!-- Light icon shown in dark theme -->
|
||||||
|
<img
|
||||||
|
src={lightIcon}
|
||||||
|
alt={alt}
|
||||||
|
class={`${sizeClasses[size]} dark-theme-only zag-transition`}
|
||||||
/>
|
/>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Hide dark icon in dark theme */
|
||||||
|
:global(.dark-theme) .light-theme-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide light icon in light theme */
|
||||||
|
:global(.light-theme) .dark-theme-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
97
src/lib/config.ts
Normal file
97
src/lib/config.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Get the directory of the current module
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
// Path to the config directory (two levels up from lib directory)
|
||||||
|
const configDir = path.resolve(__dirname, '../../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache to avoid loading config files multiple times
|
||||||
|
*/
|
||||||
|
const configCache = new Map<string, any>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse a JSON configuration file
|
||||||
|
* @param name The name of the config file without extension
|
||||||
|
* @returns The parsed configuration object
|
||||||
|
*/
|
||||||
|
export function loadConfig<T>(name: string): T {
|
||||||
|
// If the config is already in cache, return it
|
||||||
|
if (configCache.has(name)) {
|
||||||
|
return configCache.get(name) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load the config file
|
||||||
|
const configPath = path.join(configDir, `${name}.json`);
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
const config = JSON.parse(configData);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
configCache.set(name, config);
|
||||||
|
|
||||||
|
return config as T;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading config '${name}':`, error);
|
||||||
|
throw new Error(`Failed to load config: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site configuration type
|
||||||
|
*/
|
||||||
|
export interface SiteConfig {
|
||||||
|
username: string;
|
||||||
|
rootUrl: string;
|
||||||
|
shortDescription: string;
|
||||||
|
longDescription: string;
|
||||||
|
articlesName: string;
|
||||||
|
projectsName: string;
|
||||||
|
viewAll: string;
|
||||||
|
noArticles: string;
|
||||||
|
noProjects: string;
|
||||||
|
blogTitle: string;
|
||||||
|
blogShortDescription: string;
|
||||||
|
blogLongDescription: string;
|
||||||
|
projectTitle: string;
|
||||||
|
projectShortDescription: string;
|
||||||
|
projectLongDescription: string;
|
||||||
|
profileImage: string;
|
||||||
|
menu: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service category type (from types.ts)
|
||||||
|
*/
|
||||||
|
export type ServiceCategory = Record<string, Service[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service type (based on types.ts)
|
||||||
|
*/
|
||||||
|
export interface Service {
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
icon: string;
|
||||||
|
alt: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social media type (based on types.ts)
|
||||||
|
*/
|
||||||
|
export interface SocialMedia {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
iconDark: string;
|
||||||
|
iconLight: string;
|
||||||
|
alt: string;
|
||||||
|
showInFooter: boolean;
|
||||||
|
showInAbout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience exports for commonly used configs
|
||||||
|
export const site = loadConfig<SiteConfig>('site');
|
||||||
|
export const services = loadConfig<ServiceCategory>('services');
|
||||||
|
export const socials = loadConfig<SocialMedia[]>('socials');
|
@ -1,31 +1,12 @@
|
|||||||
import { type SocialMedia } from "./types";
|
import { type SocialMedia } from "./types";
|
||||||
|
import { socials as socialsConfig } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Social media profiles
|
* Social media profiles
|
||||||
|
* This is now loaded from a JSON configuration file
|
||||||
|
* located at /config/socials.json
|
||||||
|
*
|
||||||
|
* To modify social profiles, edit that JSON file rather than
|
||||||
|
* modifying the values here.
|
||||||
*/
|
*/
|
||||||
export const socials: SocialMedia[] = [
|
export const socials: SocialMedia[] = socialsConfig;
|
||||||
{
|
|
||||||
name: "Gitea",
|
|
||||||
url: "https://code.justin.deal/dealjus",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg",
|
|
||||||
alt: "Gitea Profile",
|
|
||||||
showInFooter: true,
|
|
||||||
showInAbout: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "LinkedIn",
|
|
||||||
url: "https://www.linkedin.com/in/justin-deal/",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-dark.svg",
|
|
||||||
alt: "LinkedIn Profile",
|
|
||||||
showInFooter: true,
|
|
||||||
showInAbout: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GitHub",
|
|
||||||
url: "https://github.com/justintdeal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-dark.svg",
|
|
||||||
alt: "GitHub Profile",
|
|
||||||
showInFooter: true,
|
|
||||||
showInAbout: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
@ -140,9 +140,16 @@ export type SocialMedia = {
|
|||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL to the icon from selfh.st/icons
|
* The URL to the dark version of the icon from selfh.st/icons
|
||||||
|
* Used in light theme
|
||||||
*/
|
*/
|
||||||
icon: string;
|
iconDark: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the light version of the icon from selfh.st/icons
|
||||||
|
* Used in dark theme
|
||||||
|
*/
|
||||||
|
iconLight: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alternative text for the icon
|
* Alternative text for the icon
|
||||||
|
@ -1,72 +1,12 @@
|
|||||||
// Set any item to undefined to remove it from the site or to use the default value
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global variables used throughout the site
|
* Global variables used throughout the site
|
||||||
* @property {string} username - The username displayed on the site
|
* This is now loaded from a JSON configuration file
|
||||||
* @property {string} rootUrl - The root URL of the site
|
* located at /config/site.json
|
||||||
* @property {string} shortDescription - A short description of the site
|
*
|
||||||
* @property {string} longDescription - A longer description of the site
|
* To modify site configuration, edit that JSON file
|
||||||
* @property {string} articlesName - The name used for articles
|
* rather than modifying the values here.
|
||||||
* @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 {string} githubProfile - The URL to the GitHub profile
|
|
||||||
* @property {string} linkedinProfile - The URL to the LinkedIn profile
|
|
||||||
* @property {string} giteaProfile - The URL to the Gitea profile
|
|
||||||
* @property {Object} menu - The menu items
|
|
||||||
*/
|
*/
|
||||||
export const GLOBAL = {
|
import { site } from './config';
|
||||||
// Site metadata
|
|
||||||
username: "Justin Deal",
|
|
||||||
rootUrl: "https://justin.deal",
|
|
||||||
shortDescription: "My personal slice of the internet",
|
|
||||||
longDescription: "My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js.",
|
|
||||||
|
|
||||||
// Common text names used throughout the site
|
|
||||||
articlesName: "Articles",
|
|
||||||
projectsName: "Projects",
|
|
||||||
viewAll: "View All",
|
|
||||||
|
|
||||||
// Common descriptions used throughout the site
|
|
||||||
noArticles: "No featured articles yet.",
|
|
||||||
noProjects: "No featured projects yet.",
|
|
||||||
|
|
||||||
// Blog metadata
|
// Re-export site configuration as GLOBAL for backward compatibility
|
||||||
blogTitle: "My Thoughts & Takes",
|
export const GLOBAL = site;
|
||||||
blogShortDescription: "Practical wisdom, unfiltered thoughts, and hot takes.",
|
|
||||||
blogLongDescription: "Web development, tech trends, and the occasional programming mishap.",
|
|
||||||
|
|
||||||
// Project metadata
|
|
||||||
projectTitle: "Projects and Code",
|
|
||||||
projectShortDescription: "A list of my web development projects and developer tools.",
|
|
||||||
projectLongDescription: "All of my projects, including both frontend and full-stack applications.",
|
|
||||||
|
|
||||||
// Profile image
|
|
||||||
profileImage: "pixel_avatar.png",
|
|
||||||
|
|
||||||
// Social media profiles
|
|
||||||
githubProfile: "https://github.com/justindeal",
|
|
||||||
linkedinProfile: "https://www.linkedin.com/in/justin-deal/",
|
|
||||||
giteaProfile: "https://code.justin.deal/dealjus",
|
|
||||||
|
|
||||||
// Menu items
|
|
||||||
menu: {
|
|
||||||
home: "/",
|
|
||||||
about: "/about",
|
|
||||||
blog: "/blog",
|
|
||||||
projects: "/projects",
|
|
||||||
homelab: "/homelab",
|
|
||||||
code: "https://code.justin.deal",
|
|
||||||
// videos: "https://www.youtube.com/@justin_deal",
|
|
||||||
// homelab: "https://homelab.justin.deal",
|
|
||||||
// contact: "/contact",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -299,7 +299,10 @@ const aboutSocials = socials.filter(social => social.showInAbout);
|
|||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
{aboutSocials.map((social) => (
|
{aboutSocials.map((social) => (
|
||||||
<a href={social.url} target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 zag-bg-alt p-4 rounded-lg shadow-sm hover:zag-bg-accent-light transition-colors">
|
<a href={social.url} target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 zag-bg-alt p-4 rounded-lg shadow-sm hover:zag-bg-accent-light transition-colors">
|
||||||
<img src={social.icon} alt={social.alt} class="w-6 h-6 zag-text" />
|
<!-- Dark icon for light theme -->
|
||||||
|
<img src={social.iconDark} alt={social.alt} class="w-6 h-6 zag-text light-theme-only" />
|
||||||
|
<!-- Light icon for dark theme -->
|
||||||
|
<img src={social.iconLight} alt={social.alt} class="w-6 h-6 zag-text dark-theme-only" />
|
||||||
<span>{social.name}</span>
|
<span>{social.name}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
@ -1,158 +1,12 @@
|
|||||||
import { type Service, type ServiceCategory } from "../../lib/types";
|
import { type Service, type ServiceCategory } from "../../lib/types";
|
||||||
|
import { services as servicesConfig } from "../../lib/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services available in the homelab, organized by category
|
* Services available in the homelab, organized by category
|
||||||
|
* This is now loaded from a JSON configuration file
|
||||||
|
* located at /config/services.json
|
||||||
|
*
|
||||||
|
* To modify services, edit that JSON file rather than
|
||||||
|
* modifying the values here.
|
||||||
*/
|
*/
|
||||||
export const services: ServiceCategory = {
|
export const services: ServiceCategory = servicesConfig;
|
||||||
Websites: [
|
|
||||||
{
|
|
||||||
name: "justin.deal",
|
|
||||||
link: "https://justin.deal",
|
|
||||||
icon: "/pixel_avatar.png",
|
|
||||||
alt: "Personal Website"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Utilities: [
|
|
||||||
{
|
|
||||||
name: "Silverbullet",
|
|
||||||
link: "https://notes.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/silverbullet.png",
|
|
||||||
alt: "Silverbullet",
|
|
||||||
tags: ["notes", "markdown", "knowledge base"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vikunja",
|
|
||||||
link: "https://todo.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vikunja.svg",
|
|
||||||
alt: "Vikunja",
|
|
||||||
tags: ["todo", "tasks", "productivity"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Actual",
|
|
||||||
link: "https://budget.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/actual-budget.svg",
|
|
||||||
alt: "Actual",
|
|
||||||
tags: ["finance", "budget", "money"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Searxng",
|
|
||||||
link: "https://search.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng.svg",
|
|
||||||
alt: "Searxng",
|
|
||||||
tags: ["search", "privacy", "metasearch"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "BaiKal",
|
|
||||||
link: "https://dav.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/baikal.png",
|
|
||||||
alt: "BaiKal",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Cryptpad",
|
|
||||||
link: "https://docs.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/cryptpad.svg",
|
|
||||||
alt: "Cryptpad",
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Development: [
|
|
||||||
{
|
|
||||||
name: "Gitea",
|
|
||||||
link: "https://code.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg",
|
|
||||||
alt: "Gitea",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OpenGist",
|
|
||||||
link: "https://snippets.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist.svg",
|
|
||||||
alt: "OpenGist",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IT-Tools",
|
|
||||||
link: "https://tools.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools.svg",
|
|
||||||
alt: "IT-Tools",
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Media: [
|
|
||||||
{
|
|
||||||
name: "Jellyfin",
|
|
||||||
link: "https://watch.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
|
|
||||||
alt: "Jellyfin",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Calibre-Web",
|
|
||||||
link: "https://books.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web.svg",
|
|
||||||
alt: "Calibre-Web",
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Analytics: [
|
|
||||||
{
|
|
||||||
name: "Uptime Kuma",
|
|
||||||
link: "https://status.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma.svg",
|
|
||||||
alt: "Uptime Kuma",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Umami",
|
|
||||||
link: "https://analytics.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami.svg",
|
|
||||||
alt: "Umami",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TeslaMate",
|
|
||||||
link: "https://tesla.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate.svg",
|
|
||||||
alt: "TeslaMate",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Infrastructure: [
|
|
||||||
{
|
|
||||||
name: "Pi-hole",
|
|
||||||
link: "https://pihole.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole.svg",
|
|
||||||
alt: "Pi-hole",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ntfy",
|
|
||||||
link: "https://ntfy.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy.svg",
|
|
||||||
alt: "Ntfy",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vaultwarden",
|
|
||||||
link: "https://passwords.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden.svg",
|
|
||||||
alt: "Vaultwarden",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Authentik",
|
|
||||||
link: "https://auth.justin.deal",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg",
|
|
||||||
alt: "Authentik",
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Traefik",
|
|
||||||
link: "https://proxy.justin.deal:8080",
|
|
||||||
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik.svg",
|
|
||||||
alt: "Traefik",
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
@ -394,6 +394,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
/* Theme-specific visibility utilities */
|
||||||
|
.light-theme-only {
|
||||||
|
display: block;
|
||||||
|
:where(.dark, .dark *) & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme-only {
|
||||||
|
display: none;
|
||||||
|
:where(.dark, .dark *) & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Base backgrounds and text */
|
/* Base backgrounds and text */
|
||||||
.zag-bg {
|
.zag-bg {
|
||||||
background-color: var(--color-zag-light);
|
background-color: var(--color-zag-light);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user