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 ( -