diff --git a/.github/prompts/audit-docs.md b/.github/prompts/audit-docs.md
index 26a5a11..9c7d72f 100644
--- a/.github/prompts/audit-docs.md
+++ b/.github/prompts/audit-docs.md
@@ -35,8 +35,6 @@ While reviewing this #activePullRequest #changes, analyze the entire #codebase a
---
----
-
## Execution Order (IMPORTANT)
1. **Audit `docs/` first**
diff --git a/.github/prompts/audit-quality.md b/.github/prompts/audit-quality.md
new file mode 100644
index 0000000..fffd991
--- /dev/null
+++ b/.github/prompts/audit-quality.md
@@ -0,0 +1,74 @@
+---
+title: 'Codebase Audit & Quality Improvement'
+scope: 'repo'
+targets:
+ - 'src/'
+ - 'codebase'
+labels:
+ - 'audit'
+ - 'refactoring'
+ - 'quality'
+ - 'testing'
+---
+
+**Purpose:**
+Act as a **Principal Code Reviewer and Refactoring Architect**. Perform a deep-dive audit of the #codebase to identify architectural flaws, technical debt, and maintainability issues. Once identified, proactively implement improvements to ensure a high-standard, robust, and scalable implementation.
+
+---
+
+## HARD RULES (Do Not Violate)
+
+1. **The "Any" Rule:** - Acknowledge that `any` is often required for VS Code extension architecture.
+ - Do **NOT** replace `any` with `unknown`.
+ - If `no-explicit-any` does not exist in #file:eslint.config.mjs, do **NOT** add it.
+
+2. **Proactive Improvement:**
+ - Do not ask for permission to proceed.
+ - Once vulnerabilities or smells are found, immediately proceed with the implementation and improvement of the #codebase based on your findings.
+
+3. **Validation Frequency:**
+ - Frequently run `npm run validate` from #file:package.json to verify incremental changes. If it fails, fix the issues immediately before proceeding.
+
+---
+
+## Execution Order (IMPORTANT)
+
+1. **Breadth-First Audit:** Analyze #file:src and top-level packages to identify patterns and flaws.
+2. **Test-Driven Implementation:** Apply refactors and fixes using a TDD approach.
+3. **Documentation Integrity:** Update the #file:docs directory to reflect implementation changes.
+4. **Final Sign-off:** Run a final `npm run validate` to ensure the entire workspace is stable.
+
+---
+
+## What to Do
+
+### 1. Breadth-First Discovery (Audit Strategy)
+
+Deeply analyze the source code. Do not fixate on `*.test.ts` files immediately; focus on the core logic and structure first. Evaluate through these lenses:
+
+- **Architecture & Design:** Analyze boundaries, coupling vs. modularity, and bad design patterns. Flag over-engineering or premature abstractions.
+- **Code Health:** Assess correctness, clarity, and cyclomatic complexity. Identify dead code, unused utilities, and DRY violations.
+- **Operations & Robustness:** Review resilience, idempotency, and error handling (e.g., swallowed errors).
+- **Security & Privacy:** Flag user-privacy concerns or security vulnerabilities.
+- **Standards & Style:** Check adherence to Google style guides and documentation quality.
+- **Domain Specifics:** Inspect UI/UX for **Accessibility** and analyze test design (flaky tests or over-mocking).
+
+### 2. Test-Driven Updates
+
+When creating new files or modifying existing ones, you **must** create or adjust unit tests.
+
+- Use a **test case/table format** (data-driven testing) where applicable to ensure rigorous coverage.
+
+### 3. Documentation Integrity (Step n-1)
+
+As the **second-to-last step** (before the final validation), you must review the #file:docs directory.
+
+- Ensure all documentation is accurate, up-to-date, and reflects the specific changes made during this session.
+
+---
+
+## Final Step
+
+1. Perform a final run of `npm run validate`.
+2. Ensure all code, tests, and documentation updates are complete and consistent.
+3. Remember to follow #file:copilot-instructions.md for all operations.
diff --git a/docs/architecture/app-directory.md b/docs/architecture/app-directory.md
new file mode 100644
index 0000000..b8626df
--- /dev/null
+++ b/docs/architecture/app-directory.md
@@ -0,0 +1,309 @@
+# App Directory (Next.js)
+
+This document explains the Next.js App Router directory structure and implementation in the AlexJSully Portfolio project.
+
+## Overview
+
+The project uses Next.js 16+ with the App Router architecture located in [`src/app/`](../../src/app/). This modern routing system uses file-system based routing with server and client components.
+
+## Directory Structure
+
+```text
+src/app/
+├── layout.tsx # Root layout with metadata
+├── page.tsx # Home page component
+├── manifest.ts # PWA manifest configuration
+├── robots.ts # SEO robots.txt generator
+├── error.tsx # Error boundary
+├── global-error.tsx # Global error boundary
+├── loading.tsx # Loading UI
+├── not-found.tsx # 404 page
+├── favicon.ico # Site favicon
+└── sw.js/ # Service worker route handler
+```
+
+## Architecture Pattern
+
+```mermaid
+flowchart TD
+ Layout[layout.tsx] -->|Wraps| Page[page.tsx]
+ Layout -->|Provides| Metadata[SEO & Metadata]
+ Layout -->|Includes| GL[GeneralLayout]
+ GL -->|Contains| Navbar
+ GL -->|Contains| Footer
+ GL -->|Contains| Stars[StarsBackground]
+ GL -->|Contains| Cookie[CookieSnackbar]
+ Page -->|Renders| Banner
+ Page -->|Renders| Projects[ProjectsGrid]
+ Page -->|Renders| Pubs[Publications]
+ Page -->|Initializes| Firebase
+ Page -->|Registers| SW[Service Worker]
+```
+
+## Root Layout
+
+Location: [`src/app/layout.tsx`](../../src/app/layout.tsx)
+
+The root layout defines metadata, global styles, and wraps all pages with the GeneralLayout component.
+
+### Key Features
+
+1. **Metadata Configuration:** SEO, OpenGraph, Twitter Cards, and PWA manifest
+2. **Global Styles:** Imports global SCSS styles
+3. **Analytics Integration:** Vercel Speed Insights
+4. **Service Worker Registration:** Client component for PWA support
+5. **Theme Configuration:** Viewport settings and theme color
+
+### Metadata Structure
+
+```typescript
+export const metadata: Metadata = {
+ title: {
+ template: `%s | ${metadataValues.title}`,
+ default: metadataValues.title,
+ },
+ description: metadataValues.description,
+ applicationName: metadataValues.title,
+ referrer: 'origin',
+ keywords: seoKeywords, // From src/data/keywords.ts
+ category: 'technology',
+ authors: [{ name: metadataValues.name, url: metadataValues.url }],
+ creator: metadataValues.name,
+ publisher: metadataValues.name,
+ openGraph: {
+ /* OpenGraph config */
+ },
+ twitter: {
+ /* Twitter Card config */
+ },
+ manifest: '/manifest.webmanifest',
+ // ... additional metadata
+};
+```
+
+### Viewport Configuration
+
+```typescript
+export const viewport: Viewport = {
+ themeColor: '#131518',
+ width: 'device-width',
+ initialScale: 1,
+ minimumScale: 1,
+ maximumScale: 5,
+ userScalable: true,
+};
+```
+
+## Home Page
+
+Location: [`src/app/page.tsx`](../../src/app/page.tsx)
+
+The home page is a client component that initializes services and renders main sections.
+
+### Initialization Flow
+
+```mermaid
+sequenceDiagram
+ participant Page
+ participant Firebase
+ participant Console
+ participant SW
+ participant User
+
+ Page->>Firebase: init()
+ Page->>Console: debounceConsoleLogLogo()
+ Page->>SW: navigator.serviceWorker.register('/sw.js')
+ SW-->>Page: Registration complete
+ Page->>User: Render Banner
+ Page->>User: Render ProjectsGrid
+ Page->>User: Render Publications
+```
+
+### Component Structure
+
+```typescript
+'use client';
+
+export default function Home() {
+ useEffect(() => {
+ init(); // Initialize Firebase
+ debounceConsoleLogLogo(); // Log ASCII art to console
+
+ // Register service worker
+ if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/sw.js').catch(function (err) {
+ console.error('Service Worker registration failed: ', err);
+ });
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}
+```
+
+## Special Route Handlers
+
+### Manifest (`manifest.ts`)
+
+Generates the PWA manifest dynamically:
+
+```typescript
+import type { MetadataRoute } from 'next';
+
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: "Alexander Sullivan's Portfolio",
+ short_name: "Alexander Sullivan's Portfolio",
+ icons: [
+ { src: '/icon/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
+ // ... more icons
+ ],
+ theme_color: '#131518',
+ background_color: '#131518',
+ display: 'standalone',
+ start_url: '/',
+ };
+}
+```
+
+### Robots (`robots.ts`)
+
+Generates robots.txt for SEO:
+
+```typescript
+import type { MetadataRoute } from 'next';
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: {
+ userAgent: '*',
+ allow: '/',
+ },
+ sitemap: 'https://alexjsully.me/sitemap.xml',
+ };
+}
+```
+
+## Error Handling
+
+### Error Boundary (`error.tsx`)
+
+Catches errors in the app and displays a fallback UI:
+
+```typescript
+'use client';
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+
+
Something went wrong!
+
+
+ );
+}
+```
+
+### Global Error Boundary (`global-error.tsx`)
+
+Catches errors at the root level (even in layout):
+
+```typescript
+'use client';
+
+export default function GlobalError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+
+
+ Something went wrong!
+
+
+
+ );
+}
+```
+
+## Loading States
+
+Location: [`src/app/loading.tsx`](../../src/app/loading.tsx)
+
+Displays a loading UI while the page is being rendered:
+
+```typescript
+import { CircularProgress } from '@mui/material';
+
+export default function Loading() {
+ return (
+
+
+
+ );
+}
+```
+
+## 404 Not Found
+
+Location: [`src/app/not-found.tsx`](../../src/app/not-found.tsx)
+
+Custom 404 page with navigation back to home:
+
+```typescript
+export default function NotFound() {
+ const pathname = usePathname();
+
+ return (
+
+ 404
+ {pathname}?! What is that?!
+
+
+
+
+ );
+}
+```
+
+## Best Practices
+
+1. **Server vs Client Components:** Use server components by default, mark client components with `'use client'`
+2. **Metadata:** Define metadata in layout.tsx for SEO benefits
+3. **Error Boundaries:** Implement error.tsx for graceful error handling
+4. **Loading States:** Use loading.tsx for better UX during navigation
+5. **TypeScript:** Use Next.js types like `MetadataRoute`, `Metadata`, and `Viewport`
+6. **Accessibility:** Include proper ARIA labels on all components
+
+## Testing
+
+Test files are located alongside their components:
+
+- `loading.test.tsx` - Tests loading component
+- `not-found.test.tsx` - Tests 404 page
+
+## Related Documentation
+
+- [Architecture Overview](./index.md)
+- [Layouts](./layouts.md)
+- [Components](./components/index.md)
+- [PWA Documentation](./pwa.md)
+- [Next.js App Router Documentation](https://nextjs.org/docs/app)
+
+---
+
+💡 **Tip:** The App Router automatically handles routing based on the file structure. Any `page.tsx` file becomes a route, and `layout.tsx` files wrap their children routes.
diff --git a/docs/architecture/components/avatar.md b/docs/architecture/components/avatar.md
index a3d2b05..d442f8f 100644
--- a/docs/architecture/components/avatar.md
+++ b/docs/architecture/components/avatar.md
@@ -47,10 +47,11 @@ Location: [`src/components/banner/Avatar.tsx`](../../src/components/banner/Avata
### Key Features
-1. **Sneeze Animation:** Triggered every 5 hovers
-2. **Easter Egg:** After 6 sneezes, triggers the "AAAAHHHH" transformation
+1. **Sneeze Animation:** Triggered every 5 hovers (`THRESHOLDS.SNEEZE_TRIGGER_INTERVAL`)
+2. **Easter Egg:** After 6 sneezes (`THRESHOLDS.AAAAHHHH_TRIGGER_COUNT`), triggers the "AAAAHHHH" transformation
3. **Analytics Tracking:** Logs user interactions
4. **Image Optimization:** Uses Next.js Image component
+5. **Memory Management:** Proper cleanup of debounced functions
### State Management
@@ -74,27 +75,37 @@ const imageList = {
### Sneeze Animation Sequence
+The sneeze animation uses constants from `@constants/index` for timing:
+
```typescript
+import { ANIMATIONS, THRESHOLDS } from '@constants/index';
+
handleTriggerSneeze() {
hoverProfilePic.current += 1;
- if (hoverProfilePic.current % 5 === 0 && !sneezing.current) {
+ if (hoverProfilePic.current % THRESHOLDS.SNEEZE_TRIGGER_INTERVAL === 0 && !sneezing.current) {
totalSneeze.current += 1;
- if (totalSneeze.current >= 6) {
+ if (totalSneeze.current >= THRESHOLDS.AAAAHHHH_TRIGGER_COUNT) {
logAnalyticsEvent('trigger_aaaahhhh', {...});
aaaahhhh(); // Transform entire page
} else {
sneezing.current = true;
- // Animate through sneeze sequence
+ // Animate through sneeze sequence using constants
setImage('sneeze_1');
- setTimeout(() => setImage('sneeze_2'), 500);
- setTimeout(() => setImage('sneeze_3'), 800);
setTimeout(() => {
- setImage('default');
- sneezing.current = false;
- }, 1800);
+ setImage('sneeze_2');
+
+ setTimeout(() => {
+ setImage('sneeze_3');
+
+ setTimeout(() => {
+ setImage('default');
+ sneezing.current = false;
+ }, ANIMATIONS.SNEEZE_STAGE_3);
+ }, ANIMATIONS.SNEEZE_STAGE_2);
+ }, ANIMATIONS.SNEEZE_STAGE_1);
logAnalyticsEvent('trigger_sneeze', {...});
}
@@ -102,6 +113,12 @@ handleTriggerSneeze() {
}
```
+**Animation Timing:**
+
+- Stage 1: 500ms (`ANIMATIONS.SNEEZE_STAGE_1`)
+- Stage 2: 300ms (`ANIMATIONS.SNEEZE_STAGE_2`)
+- Stage 3: 1000ms (`ANIMATIONS.SNEEZE_STAGE_3`)
+
### AAAAHHHH Easter Egg
When the avatar sneezes 6 times, it triggers [`aaaahhhh()`](../../src/helpers/aaaahhhh.ts) which:
@@ -141,10 +158,24 @@ import Avatar from '@components/banner/Avatar';
### Performance Considerations
-- **Debounced Hover:** Uses `lodash.debounce` to prevent rapid triggering
+- **Debounced Hover:** Uses `lodash.debounce` with `DELAYS.AVATAR_SNEEZE_DEBOUNCE` (100ms) to prevent rapid triggering
- **Ref-based State:** Uses refs for counters to avoid unnecessary re-renders
- **Animation Lock:** Prevents overlapping sneeze animations
- **Image Preloading:** All sneeze images should be optimized as WebP
+- **Cleanup:** Cancels debounce on component unmount to prevent memory leaks
+
+```typescript
+import { DELAYS } from '@constants/index';
+
+const debounceSneeze = debounce(handleTriggerSneeze, DELAYS.AVATAR_SNEEZE_DEBOUNCE);
+
+// Cleanup debounce on unmount
+useEffect(() => {
+ return () => {
+ debounceSneeze.cancel();
+ };
+}, [debounceSneeze]);
+```
### Accessibility
diff --git a/docs/architecture/components/cookie-snackbar.md b/docs/architecture/components/cookie-snackbar.md
new file mode 100644
index 0000000..7f4adc2
--- /dev/null
+++ b/docs/architecture/components/cookie-snackbar.md
@@ -0,0 +1,198 @@
+# Cookie Snackbar Component
+
+This document details the CookieSnackbar component that manages cookie consent notifications.
+
+## Overview
+
+Location: [`src/components/cookie-snackbar/CookieSnackbar.tsx`](../../src/components/cookie-snackbar/CookieSnackbar.tsx)
+
+The CookieSnackbar component displays a cookie consent notification to users when they first visit the site. It uses browser cookies to remember user consent and avoid showing the notification on subsequent visits.
+
+## Component Structure
+
+```mermaid
+flowchart TD
+ CookieSnackbar[CookieSnackbar] -->|Checks| Cookie[Browser Cookie]
+ Cookie -->|Not Set| Show[Show Notification]
+ Cookie -->|Set| Hide[Hide Notification]
+ Show -->|User Closes| SetCookie[Set Cookie]
+ SetCookie -->|Expires in| OneYear[1 Year]
+```
+
+## Key Features
+
+1. **Cookie Consent Management:** Tracks and stores user consent using browser cookies
+2. **SSR/Client Safety:** Prevents hydration mismatches with mounted state
+3. **Auto-dismiss:** Automatically sets consent cookie after 1 second if not dismissed
+4. **Persistent Storage:** Stores consent for 1 year (31,536,000 seconds)
+5. **MUI Integration:** Uses Material-UI Snackbar and Alert components
+6. **Accessibility:** Includes proper ARIA labels for close button
+
+## Implementation
+
+### State Management
+
+```typescript
+const [mounted, setMounted] = useState(false); // Client-side hydration guard
+const [open, setOpen] = useState(false); // Controls snackbar visibility
+```
+
+### Cookie Check Logic
+
+```typescript
+useEffect(() => {
+ setMounted(true);
+
+ // Check if consent cookie exists
+ if (document.cookie.includes('cookie-consent=true')) {
+ setOpen(false);
+ } else {
+ setOpen(true);
+ // Auto-set cookie after 1 second
+ setTimeout(() => {
+ document.cookie = 'cookie-consent=true; max-age=31536000; path=/';
+ }, 1000);
+ }
+}, []);
+```
+
+### Close Handler
+
+```typescript
+const handleClose = () => {
+ document.cookie = 'cookie-consent=true; max-age=31536000; path=/';
+ setOpen(false);
+};
+```
+
+## Component Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Component
+ participant Browser
+ participant Cookie
+
+ User->>Component: Visits site
+ Component->>Component: Mount on client
+ Component->>Browser: Check document.cookie
+ Browser->>Cookie: Read cookie-consent
+
+ alt Cookie exists
+ Cookie-->>Component: cookie-consent=true
+ Component->>Component: setOpen(false)
+ else Cookie doesn't exist
+ Cookie-->>Component: Not found
+ Component->>Component: setOpen(true)
+ Component->>User: Show notification
+ Component->>Cookie: Set after 1s timeout
+
+ User->>Component: Click close
+ Component->>Cookie: Set cookie-consent=true
+ Component->>Component: setOpen(false)
+ end
+```
+
+## Cookie Details
+
+**Cookie Name:** `cookie-consent`
+**Cookie Value:** `true`
+**Max Age:** 31,536,000 seconds (1 year)
+**Path:** `/` (site-wide)
+
+## SSR Considerations
+
+The component uses a `mounted` state to prevent server-side rendering issues:
+
+```typescript
+const [mounted, setMounted] = useState(false);
+
+useEffect(() => {
+ setMounted(true);
+ // Cookie logic here
+}, []);
+
+if (!mounted) return null;
+```
+
+This ensures:
+
+- No cookie access during SSR (server has no `document.cookie`)
+- No hydration mismatches between server and client
+- Component only renders on client after mounting
+
+## Accessibility
+
+```tsx
+
+
+
+```
+
+- **ARIA Label:** Close button has descriptive `aria-label`
+- **Keyboard Accessible:** Full keyboard navigation support
+- **Focus Management:** Proper focus indicators via MUI
+
+## Testing
+
+Test file: [`src/components/cookie-snackbar/CookieSnackbar.test.tsx`](../../src/components/cookie-snackbar/CookieSnackbar.test.tsx)
+
+**Test Coverage:**
+
+- Component renders on client
+- Snackbar opens when cookie not present
+- Snackbar closes when cookie exists
+- Close button sets cookie and hides snackbar
+- Auto-dismiss sets cookie after 1 second
+- SSR safety (no crash on server)
+
+## Integration
+
+The component is rendered in [`GeneralLayout`](../../src/layouts/GeneralLayout.tsx):
+
+```tsx
+export default function GeneralLayout({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+```
+
+## Customization
+
+To customize the cookie snackbar:
+
+1. **Message:** Modify the text in the Alert component
+2. **Cookie Duration:** Change `max-age=31536000` value
+3. **Auto-dismiss Delay:** Adjust `setTimeout(() => {...}, 1000)` delay
+4. **Severity:** Change `severity='info'` to `success`, `warning`, or `error`
+5. **Position:** Add `anchorOrigin` prop to Snackbar for positioning
+
+**Example Custom Position:**
+
+```tsx
+
+```
+
+## Related Documentation
+
+- [GeneralLayout](../layouts.md)
+- [Components Overview](./index.md)
+- [MUI Snackbar Documentation](https://mui.com/material-ui/react-snackbar/)
+
+---
+
+**Tip:** The component automatically sets the consent cookie after 1 second to avoid interrupting the user experience while still meeting consent requirements.
diff --git a/docs/architecture/components/projects.md b/docs/architecture/components/projects.md
index 10a2507..8c0f15e 100644
--- a/docs/architecture/components/projects.md
+++ b/docs/architecture/components/projects.md
@@ -14,6 +14,9 @@ The projects grid is displayed using the `ProjectsGrid` component located in [Pr
- **Grid Layout:** Responsive grid via MUI `Grid`/`Stack` and CSS-in-JS styles.
- **Project Cards:** Thumbnail, name, title, employer, resource links. The component imports the project data from `src/data/projects.ts`.
- **Analytics:** The component calls `logAnalyticsEvent` for user interactions (e.g., clicking a project link or viewing details).
+- **Network-Aware:** Uses `isNetworkFast()` utility to conditionally enable video autoplay.
+- **Hover Delay:** Uses `DELAYS.PROJECT_HOVER_VIDEO` (1000ms) before showing videos on hover.
+- **Memory Management:** Cleans up timeout on component unmount to prevent memory leaks.
### Flowchart
@@ -49,7 +52,7 @@ const example = {
};
```
-## 🔗 Related Docs
+## Related Docs
- [Component Overview](./index.md)
- [System Architecture](../architecture/index.md)
diff --git a/docs/architecture/components/stars.md b/docs/architecture/components/stars.md
index b8e8592..a9568df 100644
--- a/docs/architecture/components/stars.md
+++ b/docs/architecture/components/stars.md
@@ -24,33 +24,49 @@ flowchart TD
### 1. Dynamic Star Generation
-Stars are generated on component mount with random properties:
+Stars are generated on component mount based on window width:
```typescript
useEffect(() => {
- const starCount = Math.floor(Math.random() * 50) + 50; // 50-100 stars
-
- const newStars = Array.from({ length: starCount }, (_, i) => ({
- id: i,
- top: `${Math.random() * 100}%`,
- left: `${Math.random() * 100}%`,
- size: Math.random() * 3 + 1, // 1-4px
- animationDuration: `${Math.random() * 3 + 2}s`, // 2-5s
- animationDelay: `${Math.random() * 5}s`,
- }));
-
- setStars(newStars);
+ const maxStars = typeof window !== 'undefined' && window?.innerWidth ? window?.innerWidth : 400;
+ const numberOfStars = Math.floor(Math.random() * (maxStars / 2)) + 10;
+
+ const starsArray: ReactElement[] = [];
+
+ for (let i = 0; i < numberOfStars; i += 1) {
+ const starSize = `${Math.random() * 5 + 1}px`;
+
+ const style = {
+ background: `#ffffff50`,
+ borderRadius: '50%',
+ opacity: 0.5,
+ position: 'absolute',
+ transition: 'transform 1s',
+ animation: `twinkle ${Math.random() * 5}s ease-in-out infinite`,
+ width: starSize,
+ height: starSize,
+ top: `${Math.random() * 100}vh`,
+ left: `${Math.random() * 100}vw`,
+ };
+
+ starsArray.push();
+ }
+
+ setStars(starsArray);
}, []);
```
+**Star Count:** Based on window width (10 to width/2 stars)
+
### 2. Star Properties
Each star has:
-- **Position:** Random top and left coordinates (0-100%)
-- **Size:** Random size between 1-4px
-- **Animation Duration:** Random duration 2-5 seconds
-- **Animation Delay:** Random delay 0-5 seconds
+- **Position:** Random coordinates using viewport units (`vh`, `vw`) for consistent sizing
+- **Size:** Random size between 1-6px
+- **Animation:** Infinite twinkling with random duration (0-5 seconds)
+- **Opacity:** Semi-transparent at 0.5 for a softer appearance
+- **Background:** White with 50% opacity (`#ffffff50`)
### 3. Twinkle Animation
@@ -79,68 +95,49 @@ The `twinkle` animation should be defined in global styles:
### 4. Shooting Stars
-Occasionally, stars become shooting stars (implementation detail may vary).
+Stars can become shooting stars on hover or through automatic triggering:
-## Component Implementation
+```typescript
+import { THRESHOLDS } from '@constants/index';
+
+const handleStarAnimation = (e: React.MouseEvent | { target: HTMLElement }): void => {
+ const target = e.target as HTMLElement;
+ const shootingStarSpeed = Math.random() * 4 + 1;
+
+ target.style.animation = `shootAway ${shootingStarSpeed}s forwards`;
+ target.style.background = '#fff90050';
+ target.style.transform = `scale(${Math.random() * 2 + 1})`;
+
+ setTimeout(() => {
+ if (target) {
+ target.setAttribute('data-star-used', 'true');
+ }
+ }, shootingStarSpeed * 1000);
+};
+```
-```tsx
-'use client';
-
-import { Box } from '@mui/material';
-import { useEffect, useState } from 'react';
-
-export default function StarsBackground() {
- const [stars, setStars] = useState([]);
-
- useEffect(() => {
- // Generate stars on mount
- const starCount = Math.floor(Math.random() * 50) + 50;
- const newStars = Array.from({ length: starCount }, (_, i) => ({
- id: i,
- top: `${Math.random() * 100}%`,
- left: `${Math.random() * 100}%`,
- size: Math.random() * 3 + 1,
- animationDuration: `${Math.random() * 3 + 2}s`,
- animationDelay: `${Math.random() * 5}s`,
- }));
- setStars(newStars);
- }, []);
+**Automatic Shooting Stars:**
- return (
-
- {stars.map((star) => (
-
- ))}
-
+If there are more than `THRESHOLDS.MIN_STARS_FOR_ANIMATION` (15) unused stars, the component automatically triggers random shooting star animations:
+
+```typescript
+const handleForceStarAnimation = () => {
+ const allStars = Array.from(document.querySelectorAll('[data-testid="star"]')).filter(
+ (star) => star.getAttribute('data-star-used') !== 'true',
);
-}
+
+ if (!isEmpty(allStars) && allStars.length > THRESHOLDS.MIN_STARS_FOR_ANIMATION) {
+ const randomStar = allStars[Math.floor(Math.random() * allStars.length)] as HTMLElement;
+ if (randomStar) {
+ handleStarAnimation({ target: randomStar });
+ }
+
+ const randomTime = Math.random() * 5 + 1.5;
+ forceAnimationTimeoutRef.current = setTimeout(() => {
+ handleForceStarAnimation();
+ }, randomTime * 1000);
+ }
+};
```
## Rendering Flow
@@ -163,19 +160,45 @@ sequenceDiagram
## Accessibility
-The component uses `aria-hidden='true'` because the background is purely decorative:
+The component uses proper ARIA attributes for screen readers:
```tsx
-
+
```
+**Note:** Unlike decorative backgrounds that use `aria-hidden='true'`, this component uses `role='img'` with an `aria-label` because it's a significant visual element of the user experience.
+
## Performance Considerations
1. **Fixed Position:** Uses `position: fixed` to avoid reflow
-2. **z-index: -1:** Renders behind content
+2. **Controlled Overflow:** `overflow: hidden` prevents scrollbars
3. **GPU Acceleration:** CSS animations use GPU when possible
-4. **Random Generation:** Stars generated once on mount, not on every render
-5. **Controlled Count:** Limited to 50-100 stars for performance
+4. **Single Generation:** Stars generated once on mount, not on every render
+5. **Dynamic Count:** Star count based on viewport width for responsive performance
+6. **Memory Cleanup:** Clears timeout on unmount to prevent memory leaks
+7. **Fade Effect:** Uses MUI Fade component for smooth appearance
+8. **Analytics Throttling:** First hover tracked, subsequent hovers don't spam analytics
+
+**Cleanup Logic:**
+
+```typescript
+useEffect(() => {
+ createStars();
+
+ // Cleanup timeout on unmount to prevent memory leaks
+ return () => {
+ if (forceAnimationTimeoutRef.current) {
+ clearTimeout(forceAnimationTimeoutRef.current);
+ }
+ };
+}, []);
+```
## Integration
@@ -214,11 +237,14 @@ Test file: [`src/components/Stars/StarsBackground.test.tsx`](../../src/component
To customize the background:
-1. **Star Count:** Adjust `Math.floor(Math.random() * 50) + 50`
-2. **Star Size:** Modify `Math.random() * 3 + 1`
-3. **Animation Speed:** Change `Math.random() * 3 + 2`
-4. **Background Color:** Update `backgroundColor` in sky container
-5. **Star Color:** Modify star `backgroundColor`
+1. **Star Count:** Adjust `Math.floor(Math.random() * (maxStars / 2)) + 10` in `createStars()`
+2. **Star Size:** Modify `Math.random() * 5 + 1` (currently 1-6px)
+3. **Animation Speed:** Change `Math.random() * 5` for twinkle duration
+4. **Shooting Star Speed:** Adjust `Math.random() * 4 + 1` in `handleStarAnimation`
+5. **Background Color:** Inherited from global `body` background (`#131518`)
+6. **Star Color:** Modify `background: '#ffffff50'` in `starStyles`
+7. **Auto-trigger Threshold:** Update `THRESHOLDS.MIN_STARS_FOR_ANIMATION` in constants
+8. **Initial Trigger Delay:** Update `DELAYS.STAR_ANIMATION_INITIAL` in constants
## Visual Effect
diff --git a/docs/architecture/constants.md b/docs/architecture/constants.md
new file mode 100644
index 0000000..e65b8e3
--- /dev/null
+++ b/docs/architecture/constants.md
@@ -0,0 +1,151 @@
+# Constants Module
+
+## Overview
+
+The `constants` module provides centralized application-wide configuration values, thresholds, and magic numbers used throughout the codebase. This approach improves maintainability, testability, and makes it easier to tune application behavior.
+
+## Location
+
+**Path:** `src/constants/index.ts`
+
+## Module Exports
+
+### `DELAYS`
+
+Time delays in milliseconds for various debounce and timing operations.
+
+```typescript
+export const DELAYS = {
+ /** Debounce delay for console logo (1000ms) */
+ CONSOLE_LOGO_DEBOUNCE: 1000,
+
+ /** Delay before showing project video on hover (1000ms) */
+ PROJECT_HOVER_VIDEO: 1000,
+
+ /** Delay for avatar sneeze debounce (100ms) */
+ AVATAR_SNEEZE_DEBOUNCE: 100,
+
+ /** Initial delay for force star animation (1000ms) */
+ STAR_ANIMATION_INITIAL: 1000,
+} as const;
+```
+
+**Usage Example:**
+
+```typescript
+import { DELAYS } from '@constants/index';
+
+const debouncedFunc = debounce(handler, DELAYS.AVATAR_SNEEZE_DEBOUNCE);
+```
+
+---
+
+### `THRESHOLDS`
+
+Trigger thresholds for interactive features and animations.
+
+```typescript
+export const THRESHOLDS = {
+ /** Number of hovers before triggering sneeze (5) */
+ SNEEZE_TRIGGER_INTERVAL: 5,
+
+ /** Total sneezes before triggering aaaahhhh easter egg (6) */
+ AAAAHHHH_TRIGGER_COUNT: 6,
+
+ /** Minimum number of stars before forcing animation (15) */
+ MIN_STARS_FOR_ANIMATION: 15,
+} as const;
+```
+
+**Usage Example:**
+
+```typescript
+import { THRESHOLDS } from '@constants/index';
+
+if (hoverCount % THRESHOLDS.SNEEZE_TRIGGER_INTERVAL === 0) {
+ triggerSneeze();
+}
+```
+
+---
+
+### `NETWORK`
+
+Network performance thresholds used to detect slow connections and adapt behavior accordingly.
+
+```typescript
+export const NETWORK = {
+ /** Maximum downlink speed (Mbps) to be considered slow (1.5) */
+ SLOW_DOWNLINK_THRESHOLD: 1.5,
+
+ /** Maximum RTT (ms) to be considered fast (100) */
+ FAST_RTT_THRESHOLD: 100,
+
+ /** Network types considered slow */
+ SLOW_NETWORK_TYPES: ['slow-2g', '2g', '3g'] as const,
+} as const;
+```
+
+**Usage Example:**
+
+```typescript
+import { NETWORK } from '@constants/index';
+
+const isSlow = connection.downlink < NETWORK.SLOW_DOWNLINK_THRESHOLD;
+```
+
+---
+
+### `ANIMATIONS`
+
+Animation duration values in milliseconds for multi-stage animations.
+
+```typescript
+export const ANIMATIONS = {
+ /** Avatar sneeze animation stage 1 (500ms) */
+ SNEEZE_STAGE_1: 500,
+
+ /** Avatar sneeze animation stage 2 (300ms) */
+ SNEEZE_STAGE_2: 300,
+
+ /** Avatar sneeze animation stage 3 (1000ms) */
+ SNEEZE_STAGE_3: 1000,
+} as const;
+```
+
+**Usage Example:**
+
+```typescript
+import { ANIMATIONS } from '@constants/index';
+
+setTimeout(() => {
+ setImage('sneeze_2');
+}, ANIMATIONS.SNEEZE_STAGE_1);
+```
+
+---
+
+## Design Rationale
+
+### Why Centralize Constants?
+
+1. **Single Source of Truth:** All timing and threshold values are defined in one place
+2. **Easier Tuning:** Adjust application behavior by changing values in one file
+3. **Better Testing:** Constants can be imported and verified in tests
+4. **Type Safety:** Using `as const` provides literal type inference
+5. **Documentation:** Constants are self-documenting with descriptive names
+
+### Best Practices
+
+- **Always use constants instead of magic numbers** in application code
+- **Document what each constant represents** using JSDoc comments
+- **Group related constants** under appropriate namespaces
+- **Use `as const`** for immutability and better type inference
+
+---
+
+## See Also
+
+- [Utils Documentation](./utils.md) - Utility functions that use these constants
+- [Helpers Documentation](./helpers.md) - Helper functions that use these constants
+- [Components Documentation](./components/index.md) - Components that consume constants
diff --git a/docs/architecture/helpers.md b/docs/architecture/helpers.md
index db72c16..c18d2d3 100644
--- a/docs/architecture/helpers.md
+++ b/docs/architecture/helpers.md
@@ -2,11 +2,11 @@
This document describes the purpose, architecture, and usage of helper functions in the AlexJSully Portfolio project, with technical details and integration patterns.
-## 📦 Purpose
+## Purpose
Helpers provide reusable utility functions for formatting, logic, and data manipulation. They help keep components clean and focused on UI, separating business logic from presentation.
-## 🏗️ Structure
+## Structure
- Location: `src/helpers/`
- Example files:
@@ -16,7 +16,7 @@ Helpers provide reusable utility functions for formatting, logic, and data manip
- `ascii.test.ts`: Unit tests for ASCII art generation.
- `aaaahhhh.test.ts`: Tests for custom logic.
-## 🔍 Usage Examples
+## Usage Examples
### ASCII Art Helper
@@ -26,37 +26,86 @@ import { consoleLogLogo, debounceConsoleLogLogo } from '@helpers/ascii';
// Print ASCII logo once
consoleLogLogo();
-// Debounced version for repeated calls
+// Debounced version for repeated calls (uses DELAYS.CONSOLE_LOGO_DEBOUNCE)
debounceConsoleLogLogo();
```
-### Custom Logic Helper
+The ASCII helper uses constants for timing:
+
+```typescript
+import { DELAYS } from '@constants/index';
+import { debounce } from 'lodash';
+
+export const debounceConsoleLogLogo = debounce(consoleLogLogo, DELAYS.CONSOLE_LOGO_DEBOUNCE);
+```
+
+### AAAAHHHH Easter Egg Helper
+
+The `aaaahhhh` helper provides a playful page transformation triggered after multiple avatar sneezes:
```ts
-import { aaaahhhh, convertAAAAHH } from '@helpers/aaaahhhh';
+import { aaaahhhh, convertAAAAHH, imageAAAAHHHH, textAAAAHHHH } from '@helpers/aaaahhhh';
-// Run the playful page-wide transform
+// Trigger full page transformation
aaaahhhh();
-// Convert text to the AAAAHHHH form
-const converted = convertAAAAHH('Hello World');
+// Convert text to AAAAHHHH format
+const converted = convertAAAAHH('Hello World'); // Returns: 'Aaaaa HHHHHH'
+
+// Transform all text on page
+textAAAAHHHH();
+
+// Transform all images on page
+imageAAAAHHHH();
```
-## 🧩 Integration & Relationships
+**How it Works:**
+
+```mermaid
+flowchart TD
+ Trigger[aaaahhhh called] --> Text[textAAAAHHHH]
+ Trigger --> Images[imageAAAAHHHH]
+ Text --> Convert[convertAAAAHH]
+ Convert --> FirstHalf[First half → A]
+ Convert --> SecondHalf[Second half → H]
+ Images --> ReplaceImg[Replace all img src]
+ Images --> ReplaceBg[Replace backgrounds]
+```
+
+**Text Conversion Logic:**
+
+- First half of word → 'A' (or 'a' if lowercase)
+- Second half of word → 'H' (or 'h' if lowercase)
+- Spaces preserved
+- Example: "Alexander" → "Aaaahhhhr", "SULLIVAN" → "AAAAHHHH"
+
+**Image Transformation:**
+
+- Replaces all `
` src and srcset attributes
+- Replaces all background images in style attributes
+- Updates the stars background (#sky) with cover image
+- Uses `/images/aaaahhhh/aaaahhhh.webp` as replacement
+
+**Target Elements:**
+
+- Text: ``, ``, `
`, ``, ``, `