diff --git a/src/components/common/Anchor.astro b/src/components/common/Anchor.astro index d301cb8..00c21b2 100644 --- a/src/components/common/Anchor.astro +++ b/src/components/common/Anchor.astro @@ -12,7 +12,7 @@ const { url, external, class: className } = Astro.props; + + + + + diff --git a/src/components/common/FormInput.astro b/src/components/common/FormInput.astro new file mode 100644 index 0000000..bf36ef7 --- /dev/null +++ b/src/components/common/FormInput.astro @@ -0,0 +1,183 @@ +--- +interface Props { + type?: string; + name: string; + label: string; + placeholder?: string; + required?: boolean; + pattern?: string; + minlength?: number; + maxlength?: number; + min?: number; + max?: number; + value?: string | number; + helperText?: string; + errorMessage?: string; + class?: string; +} + +const { + type = 'text', + name, + label, + placeholder = '', + required = false, + pattern, + minlength, + maxlength, + min, + max, + value = '', + helperText = '', + errorMessage = '', + class: className = '', +} = Astro.props; + +const id = `input-${name}`; +--- + +
+ + +
+ + +
+ + + + + +
+
+ + +

+ {helperText} +

+ + + +
+ + + + diff --git a/src/components/common/LoadingIndicator.astro b/src/components/common/LoadingIndicator.astro new file mode 100644 index 0000000..1a5ccf9 --- /dev/null +++ b/src/components/common/LoadingIndicator.astro @@ -0,0 +1,126 @@ +--- +interface Props { + size?: 'small' | 'medium' | 'large'; + type?: 'spinner' | 'dots' | 'pulse'; + color?: string; + class?: string; +} + +const { + size = 'medium', + type = 'spinner', + color = 'currentColor', + class: className = '', +} = Astro.props; + +const sizeMap = { + small: 'w-4 h-4', + medium: 'w-8 h-8', + large: 'w-12 h-12', +}; + +const sizeClass = sizeMap[size] || sizeMap.medium; +--- + +{type === 'spinner' && ( +
+
+
+
+
+)} + +{type === 'dots' && ( +
+
+
+
+
+)} + +{type === 'pulse' && ( +
+
+)} + + diff --git a/src/components/common/LoadingOverlay.astro b/src/components/common/LoadingOverlay.astro index 84addcc..7f2b1a0 100644 --- a/src/components/common/LoadingOverlay.astro +++ b/src/components/common/LoadingOverlay.astro @@ -1,4 +1,6 @@ --- +import LoadingIndicator from "./LoadingIndicator.astro"; + interface Props { message?: string; subMessage?: string; @@ -18,14 +20,16 @@ const { aria-live="polite" >
- -
-
+ +
+
- -

{message}

-

{subMessage}

+ +
+

{message}

+

{subMessage}

+
@@ -41,26 +45,23 @@ const { visibility: visible; } - .spinner { - width: 48px; - height: 48px; - border: 4px solid var(--color-zag-dark-muted); - border-radius: 50%; - border-top-color: var(--color-zag-accent-dark); - animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; + /* Message animations */ + .message-animate { + animation: fadeSlideUp 0.6s ease-out both; } - :global(.dark) .spinner { - border-color: var(--color-zag-light-muted); - border-top-color: var(--color-zag-accent-dark); + .message-animate-delay { + animation: fadeSlideUp 0.6s ease-out 0.2s both; } - @keyframes spin { - 0% { - transform: rotate(0deg); + @keyframes fadeSlideUp { + from { + opacity: 0; + transform: translateY(10px); } - 100% { - transform: rotate(360deg); + to { + opacity: 1; + transform: translateY(0); } } diff --git a/src/components/common/ScrollReveal.astro b/src/components/common/ScrollReveal.astro new file mode 100644 index 0000000..d392df2 --- /dev/null +++ b/src/components/common/ScrollReveal.astro @@ -0,0 +1,135 @@ +--- +interface Props { + animation?: 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'zoom-in' | 'zoom-out'; + duration?: number; // in milliseconds + delay?: number; // in milliseconds + threshold?: number; // 0-1, percentage of element visible to trigger + rootMargin?: string; // CSS margin value + once?: boolean; // animate only once or every time element enters viewport + class?: string; +} + +const { + animation = 'fade-up', + duration = 600, + delay = 0, + threshold = 0.1, + rootMargin = '0px', + once = true, + class: className = '', +} = Astro.props; + +const id = `scroll-reveal-${Math.random().toString(36).substring(2, 9)}`; +--- + +
+ +
+ + + + diff --git a/src/components/common/ServiceCard.astro b/src/components/common/ServiceCard.astro index 2b67bd4..c613461 100644 --- a/src/components/common/ServiceCard.astro +++ b/src/components/common/ServiceCard.astro @@ -39,7 +39,7 @@ const { name, href, img, alt } = Astro.props;
@@ -141,12 +141,56 @@ const { name, href, img, alt } = Astro.props; height: 6rem; } - /* Hover effect */ + /* Enhanced hover effects */ + .service-card { + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 2px solid transparent; + background-color: var(--color-zag-bg); + border-radius: 0.5rem; + overflow: hidden; + position: relative; + } + + .service-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, transparent 0%, var(--color-zag-accent) 300%); + opacity: 0; + transition: opacity 0.3s ease; + z-index: -1; + } + .service-card:hover { - transform: translateY(-2px); + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + border-color: var(--color-zag-accent); + background-color: var(--color-zag-bg-hover); + z-index: 10; + } + + .service-card:hover::before { + opacity: 0.1; + } + + .service-card:hover .service-icon { + transform: scale(1.1); } :global(.view-mode-list) .service-card:hover { - transform: translateX(2px); + transform: translateX(4px); + } + + /* Dark mode adjustments */ + :global(.dark) .service-card { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + :global(.dark) .service-card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); } diff --git a/src/components/home/Hero.astro b/src/components/home/Hero.astro index 2a687ac..4e6fb1d 100644 --- a/src/components/home/Hero.astro +++ b/src/components/home/Hero.astro @@ -1,27 +1,34 @@ --- import { GLOBAL } from "../../lib/variables"; import OptimizedImage from "../common/OptimizedImage.astro"; +import AnimatedElement from "../common/AnimatedElement.astro"; import profileImage from "../../assets/images/pixel_avatar.png"; ---
- + + +
-

- {GLOBAL.username} -

-

-

-

+ +

+ {GLOBAL.username} +

+
+ +

+

+

+
diff --git a/src/components/homelab/CategorySection.astro b/src/components/homelab/CategorySection.astro index 394ff82..42452d6 100644 --- a/src/components/homelab/CategorySection.astro +++ b/src/components/homelab/CategorySection.astro @@ -1,6 +1,7 @@ --- import { type Service } from "../../lib/types"; import ServiceCard from "../common/ServiceCard.astro"; +import ScrollReveal from "../common/ScrollReveal.astro"; /** * CategorySection component displays a collapsible section of services grouped by category @@ -71,19 +72,26 @@ const categoryLower = category.toLowerCase(); const appTags = app.tags ? app.tags.join(' ').toLowerCase() : ''; return ( -
- -
+
+ +
+ ); }) ) : ( diff --git a/src/pages/homelab/services.ts b/src/pages/homelab/services.ts index 4820fa6..d1c245a 100644 --- a/src/pages/homelab/services.ts +++ b/src/pages/homelab/services.ts @@ -6,7 +6,7 @@ import { type Service, type ServiceCategory } from "../../lib/types"; export const services: ServiceCategory = { Websites: [ { - name: "https://justin.deal", + name: "justin.deal", link: "https://justin.deal", icon: "/pixel_avatar.png", alt: "Personal Website" diff --git a/src/styles/global.css b/src/styles/global.css index 3284daf..5623dcb 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -79,6 +79,11 @@ html.fonts-loaded body { --color-zag-accent-light-muted: #a89984; /* button2 */ --color-zag-accent-dark: #fe8019; /* secondary */ --color-zag-accent-dark-muted: #fabd2f; /* tertiary */ + + /* Card hover effect variables */ + --color-zag-bg: rgba(235, 219, 178, 0.8); /* Light mode card background */ + --color-zag-bg-hover: rgba(235, 219, 178, 1); /* Light mode card hover background */ + --color-zag-accent: rgba(184, 187, 38, 0.5); /* Light mode accent border */ /* Additional special colors */ --color-zag-button-primary: #b8bb26; @@ -100,6 +105,81 @@ html.fonts-loaded body { --zag-transition-duration: 0.15s; --zag-transition-timing-function: ease-in-out; } + + .dark { + --color-zag-bg: rgba(40, 40, 40, 0.8); /* Dark mode card background */ + --color-zag-bg-hover: rgba(40, 40, 40, 1); /* Dark mode card hover background */ + --color-zag-accent: rgba(254, 128, 25, 0.5); /* Dark mode accent border */ + } + + /* Interactive element base transitions */ + .zag-interactive { + position: relative; + transition: all 0.2s ease; + transform-origin: center; + } + + /* Standard hover effect for buttons and interactive elements */ + .zag-interactive:hover { + transform: translateY(-2px); + } + + /* Active/pressed state */ + .zag-interactive:active { + transform: translateY(0); + } + + /* Focus state for keyboard navigation */ + .zag-interactive:focus-visible { + outline: 2px solid var(--color-zag-accent-dark); + outline-offset: 2px; + } + + /* Button hover effects */ + .zag-button { + position: relative; + overflow: hidden; + } + + .zag-button::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: currentColor; + opacity: 0; + transition: opacity 0.2s ease; + } + + .zag-button:hover::after { + opacity: 0.1; + } + + .zag-button:active::after { + opacity: 0.2; + } + + /* Link hover effects */ + .zag-link { + position: relative; + } + + .zag-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background-color: currentColor; + transition: width 0.2s ease; + } + + .zag-link:hover::after { + width: 100%; + } .zag-transition { @media (prefers-reduced-motion: no-preference) {