From 53422af8470949999e811ffb36f694003ba9d631 Mon Sep 17 00:00:00 2001 From: Alexander Sullivan Date: Thu, 1 Jan 2026 20:34:29 -0500 Subject: [PATCH 1/4] new prompts --- .github/prompts/audit-docs.md | 2 - .github/prompts/audit-quality.md | 74 ++++++++++++++++++++++++++++++++ package-lock.json | 66 +++++++++++++++++----------- package.json | 4 +- 4 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 .github/prompts/audit-quality.md 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/package-lock.json b/package-lock.json index 2d8d6d4..b327928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "caniuse-lite": "^1.0.30001761", + "caniuse-lite": "^1.0.30001762", "concurrently": "^9.2.1", "cypress": "^15.8.1", "cypress-axe": "^1.7.0", @@ -45,7 +45,7 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", - "globals": "^16.5.0", + "globals": "^17.0.0", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-transform-stub": "^2.0.0", @@ -2373,9 +2373,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6625,9 +6625,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.41", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "version": "0.34.46", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.46.tgz", + "integrity": "sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==", "dev": true, "license": "MIT" }, @@ -8876,9 +8876,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "funding": [ { "type": "opencollective", @@ -10306,6 +10306,19 @@ "eslint": ">=9" } }, + "node_modules/eslint-plugin-cypress/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", @@ -10519,9 +10532,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11297,9 +11310,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", "dev": true, "license": "MIT", "engines": { @@ -15807,9 +15820,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -17428,9 +17441,9 @@ } }, "node_modules/systeminformation": { - "version": "5.27.16", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.16.tgz", - "integrity": "sha512-aimHO/bE7QFtu3uB3vtpwn7V2DXXGX7NyTY7V1g+hPa7in2k10Bp3AL+Enmg3X71n7HbgLfwy/bbf+2cBSKURQ==", + "version": "5.28.8", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.28.8.tgz", + "integrity": "sha512-W2rXK+tTIoa1svfOEfhKPzJTw2OnoJ2XS57CftQkzvwt9Hj7RC2pfHKFAk8cHH+UkDAlGMW9Sf31kdOu5PZNIA==", "dev": true, "license": "MIT", "os": [ @@ -18461,6 +18474,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -18869,9 +18883,9 @@ } }, "node_modules/zod": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", - "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz", + "integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 4479bce..15af493 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "caniuse-lite": "^1.0.30001761", + "caniuse-lite": "^1.0.30001762", "concurrently": "^9.2.1", "cypress": "^15.8.1", "cypress-axe": "^1.7.0", @@ -69,7 +69,7 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", - "globals": "^16.5.0", + "globals": "^17.0.0", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-transform-stub": "^2.0.0", From 3570d2cf53c8f2e233f1419e86d9eb8e98a8187b Mon Sep 17 00:00:00 2001 From: Alexander Sullivan Date: Thu, 1 Jan 2026 21:03:14 -0500 Subject: [PATCH 2/4] audit --- docs/architecture/constants.md | 188 +++++++++++++++++++++++ docs/architecture/index.md | 3 + jest.config.js | 1 + src/app/not-found.test.tsx | 2 +- src/app/not-found.tsx | 2 +- src/components/Stars/StarsBackground.tsx | 23 ++- src/components/banner/Avatar.tsx | 22 ++- src/components/projects/ProjectsGrid.tsx | 14 +- src/configs/firebase.ts | 10 +- src/constants/index.ts | 39 +++++ src/helpers/ascii.ts | 3 +- src/util/isNetworkFast.test.ts | 5 +- src/util/isNetworkFast.ts | 29 +++- tsconfig.json | 1 + 14 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 docs/architecture/constants.md create mode 100644 src/constants/index.ts diff --git a/docs/architecture/constants.md b/docs/architecture/constants.md new file mode 100644 index 0000000..fc4fd0a --- /dev/null +++ b/docs/architecture/constants.md @@ -0,0 +1,188 @@ +# 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 +- **Test constants** to ensure they have expected values + +--- + +## Testing + +The constants module includes comprehensive unit tests to verify: + +- All constant values are positive numbers (where applicable) +- All required constants are exported +- Constants have expected types and structures + +**Test Location:** `src/constants/index.test.ts` + +--- + +## Migration Guide + +When encountering magic numbers in code, follow these steps: + +1. Identify the magic number and its purpose +2. Add an appropriately named constant to the relevant group +3. Replace the magic number with the constant import +4. Update tests if necessary +5. Update documentation + +**Example:** + +```typescript +// After +import { DELAYS } from '@constants/index'; + +// Before +setTimeout(handler, 1000); + +setTimeout(handler, DELAYS.CONSOLE_LOGO_DEBOUNCE); +``` + +--- + +## 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/index.md b/docs/architecture/index.md index bb12974..a3573da 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -38,6 +38,8 @@ src/ ServiceWorkerRegister.tsx configs/ # Configuration files firebase.ts # Firebase initialization + constants/ # Application-wide constants + index.ts # Delays, thresholds, network, animations data/ # Static data sources keywords.ts # SEO keywords projects.ts # Project data @@ -92,6 +94,7 @@ flowchart TD ## Subsystems - **Components:** UI elements (see [Components Docs](./components/index.md)) +- **Constants:** Application-wide configuration values (see [Constants Docs](./constants.md)) - **Data:** Static and dynamic data sources - **Helpers/Utils:** Utility functions for logic and formatting - **Layouts:** Page and section layouts diff --git a/jest.config.js b/jest.config.js index 0bd04c5..fb88185 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,7 @@ module.exports = { '^@/(.*)$': '/src/$1', '^@components/(.*)$': '/src/components/$1', '^@configs/(.*)$': '/src/configs/$1', + '^@constants/(.*)$': '/src/constants/$1', '^@data/(.*)$': '/src/data/$1', '^@helpers/(.*)$': '/src/helpers/$1', '^@images/(.*)$': '/src/images/$1', diff --git a/src/app/not-found.test.tsx b/src/app/not-found.test.tsx index 021273b..7fa3321 100644 --- a/src/app/not-found.test.tsx +++ b/src/app/not-found.test.tsx @@ -24,7 +24,7 @@ describe('NotFound', () => { expect(screen.getByRole('heading', { name: /page not found/i })).toBeInTheDocument(); expect(screen.getByText('404')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: /go back home/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /go home/i })).toBeInTheDocument(); expect(screen.getByText('/some-path')).toBeInTheDocument(); }); }); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index ef0c341..5dd4ad4 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -70,7 +70,7 @@ export default function NotFound(): ReactElement { { if (pathname === '/' && typeof window !== 'undefined') { diff --git a/src/components/Stars/StarsBackground.tsx b/src/components/Stars/StarsBackground.tsx index c148bf8..0ee7316 100644 --- a/src/components/Stars/StarsBackground.tsx +++ b/src/components/Stars/StarsBackground.tsx @@ -3,20 +3,23 @@ import { logAnalyticsEvent } from '@configs/firebase'; import { Box, Fade } from '@mui/material'; import { isEmpty } from 'lodash'; -import { ReactElement, useEffect, useState } from 'react'; +import { ReactElement, useEffect, useRef, useState } from 'react'; /** Create a starry background with shooting stars */ -export default function StarsBackground(): ReactElement { +export default function StarsBackground(): ReactElement | null { // There is a lot of random math in here // but it's all just to make the stars look random /** The stars to be rendered */ - const [stars, setStars] = useState(null); + const [stars, setStars] = useState(null); const [fade, setFade] = useState(false); /** Whether or not the stars have been triggered [true] or not [false] */ const [starsTriggered, setStarsTriggered] = useState(false); + /** Ref to store the force animation timeout for cleanup */ + const forceAnimationTimeoutRef = useRef(null); + /** The styles for the stars */ const starStyles = { background: `#ffffff50`, @@ -27,7 +30,7 @@ export default function StarsBackground(): ReactElement { }; /** Handle the shooting star animation */ - const handleStarAnimation = (e: any) => { + const handleStarAnimation = (e: React.MouseEvent | { target: HTMLElement }): void => { /** The star DOM element */ const target = e.target as HTMLElement; /** The speed of the shooting star */ @@ -67,8 +70,8 @@ export default function StarsBackground(): ReactElement { /** The random time to wait before triggering the next star */ const randomTime = Math.random() * 5 + 1.5; - // Recursively call this function - setTimeout(() => { + // Recursively call this function and store timeout for cleanup + forceAnimationTimeoutRef.current = setTimeout(() => { handleForceStarAnimation(); }, randomTime * 1000); } else { @@ -86,7 +89,7 @@ export default function StarsBackground(): ReactElement { setFade(false); /** The array of stars */ - const starsArray: any = []; + const starsArray: ReactElement[] = []; /** The max number of stars to create */ const maxStars = typeof window !== 'undefined' && window?.innerWidth ? window?.innerWidth : 400; /** The number of stars to create */ @@ -143,6 +146,12 @@ export default function StarsBackground(): ReactElement { // We want to use useEffect here so that we can use the window object createStars(); + // Cleanup timeout on unmount to prevent memory leaks + return () => { + if (forceAnimationTimeoutRef.current) { + clearTimeout(forceAnimationTimeoutRef.current); + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/banner/Avatar.tsx b/src/components/banner/Avatar.tsx index acecc07..45f1b1a 100644 --- a/src/components/banner/Avatar.tsx +++ b/src/components/banner/Avatar.tsx @@ -1,8 +1,9 @@ import { logAnalyticsEvent } from '@configs/firebase'; +import { ANIMATIONS, DELAYS, THRESHOLDS } from '@constants/index'; import { aaaahhhh } from '@helpers/aaaahhhh'; import { debounce } from 'lodash'; import Image from 'next/image'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; export default function Avatar() { /** The number of times the profile pic has been hovered */ @@ -27,10 +28,10 @@ export default function Avatar() { const 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', { name: 'trigger_aaaahhhh', type: 'hover', @@ -53,9 +54,9 @@ export default function Avatar() { setImage(imageList[`default`]); sneezing.current = false; - }, 1000); - }, 300); - }, 500); + }, ANIMATIONS.SNEEZE_STAGE_3); + }, ANIMATIONS.SNEEZE_STAGE_2); + }, ANIMATIONS.SNEEZE_STAGE_1); logAnalyticsEvent('trigger_sneeze', { name: 'trigger_sneeze', @@ -66,7 +67,14 @@ export default function Avatar() { }; /** Debounce the sneeze animation */ - const debounceSneeze = debounce(handleTriggerSneeze, 100); + const debounceSneeze = debounce(handleTriggerSneeze, DELAYS.AVATAR_SNEEZE_DEBOUNCE); + + // Cleanup debounce on unmount + useEffect(() => { + return () => { + debounceSneeze.cancel(); + }; + }, [debounceSneeze]); return ( { hoverTimeout.current = setTimeout(() => { setHoveredProject(projectId); - }, 1000); // 2 seconds delay + }, DELAYS.PROJECT_HOVER_VIDEO); }; const handleMouseLeave = () => { @@ -33,6 +34,15 @@ export default function ProjectsGrid(): ReactElement { return isNetworkFast() ? `${url}&autoplay=1` : url; }; + useEffect(() => { + // Cleanup timeout on unmount + return () => { + if (hoverTimeout.current) { + clearTimeout(hoverTimeout.current); + } + }; + }, []); + return ( projects && ( { connection: { saveData: false, effectiveType: '4g', - downlink: 1, + downlink: NETWORK.SLOW_DOWNLINK_THRESHOLD - 0.1, rtt: 10, }, }; @@ -86,7 +87,7 @@ describe('isNetworkFast', () => { saveData: false, effectiveType: '4g', downlink: 10, - rtt: 200, + rtt: NETWORK.FAST_RTT_THRESHOLD + 50, }, }; Object.defineProperty(global, 'navigator', { diff --git a/src/util/isNetworkFast.ts b/src/util/isNetworkFast.ts index 4b45cb9..a6283ec 100644 --- a/src/util/isNetworkFast.ts +++ b/src/util/isNetworkFast.ts @@ -1,3 +1,17 @@ +import { NETWORK } from '@constants/index'; + +/** Network connection information interface */ +interface NetworkInformation { + saveData?: boolean; + effectiveType?: '2g' | '3g' | '4g' | 'slow-2g'; + downlink?: number; + rtt?: number; +} + +interface NavigatorWithConnection extends Navigator { + connection?: NetworkInformation; +} + /** * Checks if the current network connection is fast. * @@ -7,7 +21,11 @@ export function isNetworkFast(): boolean { // Check if the connection API is available in the navigator if ('connection' in navigator) { /** Get the connection object from the navigator */ - const connection = (navigator as any).connection; + const connection = (navigator as NavigatorWithConnection).connection; + + if (!connection) { + return true; + } if (connection.saveData) { // Save data mode is enabled @@ -15,11 +33,14 @@ export function isNetworkFast(): boolean { } /** Check if the network is slow based on the known slow network types */ - const slowType = ['slow-2g', '2g', '3g'].includes(connection.effectiveType); + const slowType = connection.effectiveType + ? (NETWORK.SLOW_NETWORK_TYPES as readonly string[]).includes(connection.effectiveType) + : false; /** Check if the network is slow based on the downlink/download speed */ - const slowDown = connection.downlink < 1.5; + const slowDown = + connection.downlink !== undefined ? connection.downlink < NETWORK.SLOW_DOWNLINK_THRESHOLD : false; /** Check if the network is slow based on the round-trip time (RTT) */ - const slowRTT = connection.rtt > 100; + const slowRTT = connection.rtt !== undefined ? connection.rtt > NETWORK.FAST_RTT_THRESHOLD : false; return !(slowType || slowDown || slowRTT); } diff --git a/tsconfig.json b/tsconfig.json index 70f87f4..485b176 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "@*": ["./src/*"], "@components/*": ["./src/components/*"], "@configs/*": ["./src/configs/*"], + "@constants/*": ["./src/constants/*"], "@data/*": ["./src/data/*"], "@helpers/*": ["./src/helpers/*"], "@images/*": ["./src/images/*"], From fefb1ec237a3e0b28c297a6c5ae79e379e50435a Mon Sep 17 00:00:00 2001 From: Alexander Sullivan Date: Thu, 1 Jan 2026 21:15:01 -0500 Subject: [PATCH 3/4] update docs --- docs/architecture/app-directory.md | 309 ++++++++++++++++++ docs/architecture/components/avatar.md | 53 ++- .../components/cookie-snackbar.md | 198 +++++++++++ docs/architecture/components/projects.md | 5 +- docs/architecture/components/stars.md | 196 ++++++----- docs/architecture/helpers.md | 75 ++++- docs/index.md | 1 + 7 files changed, 727 insertions(+), 110 deletions(-) create mode 100644 docs/architecture/app-directory.md create mode 100644 docs/architecture/components/cookie-snackbar.md 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 ( -