Skip to content

feat: introduce new modular Volcanic Forge template with fluid lava animations #1955#2333

Merged
anurag3407 merged 10 commits into
anurag3407:mainfrom
ShaktiShrey-01:feat/add-volcanic-forge-theme
May 28, 2026
Merged

feat: introduce new modular Volcanic Forge template with fluid lava animations #1955#2333
anurag3407 merged 10 commits into
anurag3407:mainfrom
ShaktiShrey-01:feat/add-volcanic-forge-theme

Conversation

@ShaktiShrey-01

@ShaktiShrey-01 ShaktiShrey-01 commented May 28, 2026

Copy link
Copy Markdown
Contributor

User description

Description

This PR introduces the completely modularized Volcanic_Forge portfolio template. It features highly responsive structures that scale naturally on all modern desktop resolutions without distortion, completely running on dynamic data structures.

Core Animation Highlights

  • Volcanic Particle Eruption (LavaAnimate): Launches a dense shower of small, asymmetrical, tumbling molten droplets that dynamically fuse together to organically forge elements right as they enter the viewport.
  • Molten Text Ripple (textMelt): Drives a persistent, multi-layered fluid wave and thermal glow underneath titles to simulate ongoing glowing heat radiation.
  • Atmospheric Thermal Particles (EmbersBackground): Runs a localized canvas-simulation of glowing orange embers continuously rising and shifting across the backdrop.
  • Cracked Earth Overlay (CrackedEarthOverlay): Projects a clear, high-contrast structural tectonic fissure network over the basalt background to establish the volcanic bedrock environment.
  • Experience Timeline Flare (Hover Animations): Reactively transforms passive timeline boundary circles into white-hot glowing orbs while fluidly elevating the associated task container.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Performance improvement
  • Other (describe)

Related Issue

Fixes #1955

Testing

  • Unit tests pass
  • Tested Locally
  • New tests added

Checklist

  • Code follows project style guidelines
  • Self-reviewed my code
  • Added comments where necessary
  • Updated documentation
  • No new warnings generated

Screenshots (MANDATORY for UI/UX changes)

  • Attached Screenshots or Screen Recordings (showing before and after)
Screen.Recording.2026-05-28.at.2.06.58.PM.mov
Screenshot 2026-05-28 at 2 07 54 PM Screenshot 2026-05-28 at 2 08 05 PM Screenshot 2026-05-28 at 2 08 17 PM

CodeAnt-AI Description

Add fully built portfolio templates for Mosaic Assemble and Volcanic Forge

What Changed

  • Replaced the old placeholder screens with complete portfolio sections for both themes, including hero, about, skills, projects, experience, testimonials, and contact areas
  • Mosaic Assemble now uses animated tile-style text, assembled image mosaics, responsive section widths, and project/contact links that adapt to available data
  • Volcanic Forge now uses lava-themed motion, ember and cracked-earth backgrounds, molten text effects, and polished sections for profile, skills, work history, projects, and contact
  • Page Turner now works as a full flip-book portfolio with per-page sections, safer scrolling between the book and inner content, paginated lists, and a back cover page

Impact

✅ Complete portfolio pages instead of placeholder landing screens
✅ More reliable navigation in long, scrollable page-flip layouts
✅ Clearer contact and project browsing across both themes

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features
    • Mosaic Assemble: full multi-section portfolio (Hero, About, Skills, Projects, Experience, Testimonials, Contact) with animated mosaic images, per-character text animation, and interactive visuals.
    • Page Turner: flipbook-style portfolio with paginated pages (Hero, About, Skills, Projects, Experience, Testimonials, Contact, Back Cover) and wheel/gesture flipping.
    • Volcanic Forge: volcanic theme with lava/background particle animations, animated sections (Hero, About, Skills, Projects, Experience, Contact) and molten text effects.

Review Change Stack

Copilot AI review requested due to automatic review settings May 28, 2026 08:47
@codeant-ai

codeant-ai Bot commented May 28, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds three full portfolio templates: Mosaic Assemble (mosaic tiles + character/element animations), Page Turner (HTMLFlipBook pages with wheel-flip hooks and paginated pages), and Volcanic Forge (lava/ember visual systems and themed sections). Each template composes multiple new subcomponents and wires data from dummy_data.json.

Changes

Mosaic Assemble Portfolio Template

Layer / File(s) Summary
Animation primitives: TileSnappingText and ScatterItem
frontend/src/components/portfolio/templates/Mosaic_Assemble/TileSnappingText.jsx, frontend/src/components/portfolio/templates/Mosaic_Assemble/ScatterText.jsx
TileSnappingText splits text into characters and animates each with per-character drift/rotation; ScatterItem provides seeded entrance animations and respects reduced-motion.
MosaicImage tiled grid animation
frontend/src/components/portfolio/templates/Mosaic_Assemble/MosaicImage.jsx
Renders an image as a CSS grid of animated tiles with randomized spring configs and staggered reveal timing.
Hero section with responsive heading and social links
frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx
Full-viewport hero with animated TileSnappingText heading/subtitle, status badge, CTA to #projects, and animated social icon panel constructed from socials prop.
About, Skills, Projects, Experience, Testimonials, Contact sections
frontend/src/components/portfolio/templates/Mosaic_Assemble/{About,Skills,Projects,Experience,Testimonials,Contact}.jsx
Section components using viewport-width tracking for responsive heading widths, animated tiles/cards, alternating layouts for projects/experience, and a MagneticButton CTA that tracks pointer for a magnetism effect.
MosaicAssemble template composition
frontend/src/components/portfolio/templates/Mosaic_Assemble/index.jsx
Composes the section components and passes dummy_data.json slices (personal, skills, projects, experience, testimonials, socials) as props inside a styled root container.

Page Turner Flipbook Portfolio Template

Layer / File(s) Summary
Shared utilities, hooks, and layout components
frontend/src/components/portfolio/templates/Page_Turner/shared.jsx
Exports safe data defaults from dummy_data.json, hooks useBookScale/useBookSize, and useWheelFlip(bookRef) for non-passive wheel page flipping with scroll-area checks and debounce; also provides small presentational components (SectionHeader, Badge, MetricCard, PageFrame, PageShell, DarkCover, CoverStats) and icon/motion re-exports.
Page components: HeroPage through BackCoverPage
frontend/src/components/portfolio/templates/Page_Turner/{HeroPage,AboutPage,SkillsPage,ProjectsPage,ExperiencePage,TestimonialsPage,ContactPage,BackCoverPage}.jsx
Eight forwardRef page components implementing dedicated flipbook pages: HeroPage, AboutPage (avatar/bio/metrics/traits), SkillsPage (grouped categories + progress bars), ProjectsPage (paginated projects), ExperiencePage (paginated experience), TestimonialsPage (paginated testimonials), ContactPage (form → mailto), BackCoverPage (closing cover).
PageTurner flipbook composition and wheel-flip wiring
frontend/src/components/portfolio/templates/Page_Turner/index.jsx
Creates a bookRef, wires useWheelFlip for wheel navigation, configures HTMLFlipBook with viewport-derived sizing from useBookSize, and sequences the page components as flipbook pages inside a motion wrapper.

Volcanic Forge Lava-Themed Portfolio Template

Layer / File(s) Summary
GlobalBackground and LavaAnimate animation systems
frontend/src/components/portfolio/templates/Volcanic_Forge/{GlobalBackground,LavaAnimate}.jsx
GlobalBackground renders a cracked-earth SVG overlay and memoized ember particles with randomized parameters; LavaAnimate orchestrates droplet particles, a formed-state transition, and optional text-melt on children with configurable timing and particle properties.
SectionHeader and shared layout component
frontend/src/components/portfolio/templates/Volcanic_Forge/components/SectionHeader.jsx
Animated section header with fade-in-up variant and decorative gradient divider used across volcanic sections.
Hero, About, Skills, Projects, Experience, Contact section components
frontend/src/components/portfolio/templates/Volcanic_Forge/components/{Hero,About,Skills,Projects,Experience,Contact}.jsx
Themed sections integrated with LavaAnimate/SectionHeader: Hero (name/title + social buttons), About (avatar/location/bio/top-skills/stats), Skills (animated progress bars + shimmer), Projects (project cards with overlays and links), Experience (timeline cards), Contact (mailto CTA with particle background).
VolcanicForgeTemplate composition with data wiring
frontend/src/components/portfolio/templates/Volcanic_Forge/index.jsx
Composes volcanic subcomponents, wraps where appropriate with GlobalBackground and LavaAnimate, and wires dummy_data.json fields (personal, socials, skills, projects, experience, stats) into the composed sections with updated footer/year rendering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

portfolio, portfolio-template, ui-revamp, type:feature, type:design, gssoc:approved, level:advanced

Poem

🐇 I hopped through tiles and glowing streams,
I stitched the pages and molten dreams,
Three templates bloom with motion bright,
Tiles, flips, and lava — what a sight!
Bravo — now ship them into light.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Beyond the Volcanic Forge template, the PR also includes comprehensive implementations of MosaicAssemble and PageTurner portfolio templates (22 new components total) that are not mentioned in linked issue #1955, representing significant scope expansion beyond the single required template. Consider splitting this PR: isolate the Volcanic Forge template changes into a focused PR for #1955, and submit the MosaicAssemble and PageTurner template implementations as separate pull requests with their own issue references.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective—introducing a new modularized Volcanic Forge portfolio template with lava animations. It is specific, concise, and clearly summarizes the primary change.
Linked Issues check ✅ Passed All coding requirements from issue #1955 are met: complete modularized Volcanic Forge template with Hero, About, Skills, Projects, Experience, Testimonials, and Contact sections; all data sourced from dummy_data.json; uses React/JSX, Tailwind CSS, framer-motion animations, and lucide-react icons; responsive design implemented; no hardcoded data.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label May 28, 2026
Comment on lines +7 to +15
const [viewportWidth, setViewportWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 1200,
);

useEffect(() => {
const onResize = () => setViewportWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: viewportWidth is tracked with a resize listener but never used in rendering logic. This creates avoidable state updates and component re-renders on every window resize for no functional benefit. Remove this state/effect pair (or actually use viewportWidth) to prevent unnecessary work. [performance]

Severity Level: Major ⚠️
- ⚠️ Hero section re-renders on every browser resize.
- ⚠️ MosaicAssemble template does extra work without UI change.
Steps of Reproduction ✅
1. Render the `MosaicAssemble` template component from
`frontend/src/components/portfolio/templates/Mosaic_Assemble/index.jsx:11-20`, which
includes `<Hero data={data.personal} socials={data.socials} />` at line 14.

2. When `MosaicAssemble` renders, the `Hero` component in
`frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx:6-15` runs its body
and initializes the `viewportWidth` state via `useState` at lines 7-9.

3. The `useEffect` in `Hero.jsx` at lines 11-15 registers a `window` resize listener; each
time the browser window is resized, `onResize` calls
`setViewportWidth(window.innerWidth)`, causing React to schedule a re-render of `Hero`
(and its children) on every resize event.

4. A `Grep` search for `viewportWidth` in `Hero.jsx` (and visual inspection of
`frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx:1-115`) shows
`viewportWidth` is only defined at lines 7-9 and never read in the JSX tree, so every
resize-triggered state update produces a re-render with identical output, resulting in
unnecessary work whenever the template is used and the viewport is resized.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx
**Line:** 7:15
**Comment:**
	*Performance: `viewportWidth` is tracked with a resize listener but never used in rendering logic. This creates avoidable state updates and component re-renders on every window resize for no functional benefit. Remove this state/effect pair (or actually use `viewportWidth`) to prevent unnecessary work.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +8 to +13
const contactLinks = [
{ label: 'Email', value: email, href: email ? `mailto:${email}` : null, icon: Mail },
{ label: 'GitHub', value: github, href: github || null, icon: Github },
{ label: 'LinkedIn', value: linkedin, href: linkedin || null, icon: Linkedin },
{ label: 'Twitter', value: twitter, href: twitter || null, icon: Sparkles },
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: href values are taken directly from profile data and rendered into clickable anchors without protocol validation. If a malicious value like javascript:... is present, clicking the link can execute script in the browser. Restrict links to safe protocols (https:, mailto:) before rendering. [security]

Severity Level: Critical 🚨
- ❌ Crafted social links execute arbitrary JavaScript in visitors' browsers.
- ⚠️ Hero and Contact pages both trust unvalidated socials URLs.
- ⚠️ Multi-tenant portfolios allow stored XSS between different user sites.
Steps of Reproduction ✅
1. Note that portfolio profile data is sourced from JSON:
`frontend/src/data/dummy_data.json` defines `"socials"` at lines 10-14, including
`"github"`, `"linkedin"`, `"twitter"`, and `"email"` (e.g., line 11 currently `"github":
"https://github.com"`).

2. Observe in `frontend/src/components/portfolio/templates/Page_Turner/shared.jsx` lines
17-20 that `data` is imported from this JSON (line 17) and `export const safeSocials =
data.socials || {}` (line 20) exposes these values without any normalization or protocol
checks.

3. In `frontend/src/components/portfolio/templates/Page_Turner/ContactPage.jsx` lines 5
and 8-12, the component destructures `{ email = '', github = '', linkedin = '', twitter =
'' } = safeSocials` (line 5) and builds `contactLinks` (lines 8-13) where
GitHub/LinkedIn/Twitter `href` values are taken directly from `github`, `linkedin`, and
`twitter` (lines 10-12) with no validation.

4. In the same file, the Social Grid renders those links as `<a>` tags at lines 54-61:
`href={item.href}` with `target="_blank"` and `rel="noreferrer"` (lines 56-60), meaning
any string in `data.socials.github` (or linkedin/twitter) becomes a clickable navigation.

5. To reproduce a script-URL injection, change `frontend/src/data/dummy_data.json` line 11
to `"github": "javascript:alert('XSS-from-github')"` and run the frontend (e.g., `npm run
dev`); render the Page Turner template, which includes `ContactPage` via
`frontend/src/components/portfolio/templates/Page_Turner/index.jsx` lines 49-55.

6. Navigate in the UI to the "Let's Connect" contact page (rendered by `ContactPage`) and
click the "GitHub" card rendered from `contactLinks` (ContactPage.jsx lines 54-61); the
browser follows the `javascript:` URL and executes the injected script, demonstrating that
unvalidated profile URLs can execute arbitrary JavaScript when clicked.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/portfolio/templates/Page_Turner/ContactPage.jsx
**Line:** 8:13
**Comment:**
	*Security: `href` values are taken directly from profile data and rendered into clickable anchors without protocol validation. If a malicious value like `javascript:...` is present, clicking the link can execute script in the browser. Restrict links to safe protocols (`https:`, `mailto:`) before rendering.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +76 to +86
href={proj.liveUrl}
whileHover={{ y: -4 }}
className="flex items-center gap-2 bg-white text-slate-950 px-4 py-2 rounded-lg font-bold text-sm shadow-lg shadow-white/5"
>
<ExternalLink size={14} />
Live Matrix
</motion.a>
)}
{proj.githubUrl && (
<motion.a
href={proj.githubUrl}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: proj.liveUrl and proj.githubUrl are injected directly into anchor href values without protocol validation. If this data is user-controlled, a javascript: URL can execute script when clicked. Validate URLs to allow only safe schemes (e.g. https:/http:) before rendering links. [security]

Severity Level: Critical 🚨
- ❌ Mosaic_Assemble project buttons can execute arbitrary JavaScript on click.
- ⚠️ Viewers of Mosaic_Assemble template risk client-side script injection.
- ⚠️ Similar unvalidated links appear in other templates (e.g. Volcanic_Forge).
Steps of Reproduction ✅
1. Open `frontend/src/components/portfolio/templates/Mosaic_Assemble/index.jsx` where
`MosaicAssemble` renders `<Projects projects={data.projects} />` at line 17 using data
from `frontend/src/data/dummy_data.json` (lines 38–87).

2. In `frontend/src/data/dummy_data.json`, modify one project's `"liveUrl"` (line 43, 51,
59, etc.) to a JavaScript URL such as `"javascript:alert('XSS from project link')"` while
keeping the rest of the structure unchanged.

3. Render the `MosaicAssemble` template in any page by importing its default export from
`frontend/src/components/portfolio/templates/Mosaic_Assemble/index.jsx`; the `Projects`
component at `frontend/src/components/portfolio/templates/Mosaic_Assemble/Projects.jsx`
lines 73–83 will render a `<motion.a>` with `href={proj.liveUrl}` (line 76) and label
"Live Matrix".

4. In the browser, click the "Live Matrix" button for the modified project; because the
`href` is a `javascript:` URL passed through without validation, the browser executes the
embedded script in the context of the portfolio page.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/portfolio/templates/Mosaic_Assemble/Projects.jsx
**Line:** 76:86
**Comment:**
	*Security: `proj.liveUrl` and `proj.githubUrl` are injected directly into anchor `href` values without protocol validation. If this data is user-controlled, a `javascript:` URL can execute script when clicked. Validate URLs to allow only safe schemes (e.g. `https:`/`http:`) before rendering links.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented May 28, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
frontend/src/components/portfolio/templates/Mosaic_Assemble/Testimonials.jsx (1)

31-31: ⚡ Quick win

Use a stable key instead of array index.

Using the array index as a key can cause issues if the testimonials array is reordered or filtered. If dummy_data.json provides a unique id field for each testimonial, use it; otherwise consider generating a stable key from t.name and t.role.

♻️ Suggested fix
-          key={i}
+          key={t.id || `${t.name}-${t.role}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/Testimonials.jsx`
at line 31, Replace the unstable array index key in the Testimonials component
map (currently key={i}) with a stable unique identifier: if each testimonial
object has an id use key={t.id}; otherwise derive a stable key from testimonial
fields (e.g., combine t.name and t.role) when mapping over the testimonials (the
mapped item is referenced as t) so React can track items reliably during
reorders/filters.
frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx (1)

26-42: ⚡ Quick win

Stabilize ember configs to prevent animation resets on re-render.

Line 27–41 recomputes random size, timing, drift, and opacity every render, which can cause ember jumps if this component re-renders. Memoize per-particle config once and render from that array.

💡 Suggested refactor
 export default function GlobalBackground() {
-  const embers = useMemo(() => Array.from({ length: 40 }), []);
+  const embers = useMemo(
+    () =>
+      Array.from({ length: 40 }, (_, i) => ({
+        id: i,
+        size: Math.random() * 4 + 1,
+        duration: (Math.random() * 10 + 10) / Math.max(PARTICLE_SPEED, 0.1),
+        delay: Math.random() * 10,
+        startX: Math.random() * 100,
+        driftX: (Math.random() - 0.5) * 200,
+        peakOpacity: Math.random() * 0.8 + 0.2,
+      })),
+    []
+  );
@@
-        {embers.map((_, i) => {
-          const size = Math.random() * 4 + 1;
-          const duration = (Math.random() * 10 + 10) / Math.max(PARTICLE_SPEED, 0.1);
-          const delay = Math.random() * 10;
-          const startX = Math.random() * 100;
-
+        {embers.map((ember) => {
           return (
             <motion.div
-              key={i}
+              key={ember.id}
               className="absolute bottom-0 rounded-full bg-orange-500"
               style={{
-                width: size, height: size, left: `${startX}vw`,
+                width: ember.size, height: ember.size, left: `${ember.startX}vw`,
                 filter: `blur(${1 * Math.min(PARTICLE_BRIGHTNESS, 3)}px) brightness(${PARTICLE_BRIGHTNESS})`,
                 boxShadow: `0 0 ${10 * PARTICLE_BRIGHTNESS}px ${2 * PARTICLE_BRIGHTNESS}px rgba(249, 115, 22, ${0.5 * Math.min(PARTICLE_BRIGHTNESS, 2)})`
               }}
-              animate={{ y: ['10vh', '-110vh'], x: [0, (Math.random() - 0.5) * 200], opacity: [0, Math.random() * 0.8 + 0.2, 0] }}
-              transition={{ duration: duration, repeat: Infinity, delay: delay, ease: 'linear' }}
+              animate={{ y: ['10vh', '-110vh'], x: [0, ember.driftX], opacity: [0, ember.peakOpacity, 0] }}
+              transition={{ duration: ember.duration, repeat: Infinity, delay: ember.delay, ease: 'linear' }}
             />
           );
         })}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx`
around lines 26 - 42, The ember props are being re-generated on every render
causing animation jumps; in GlobalBackground.jsx create a stable emberConfigs
array (use useMemo) that maps each ember index to random values (size, duration
computed using PARTICLE_SPEED, delay, startX, x drift, opacity peak, and any
precomputed blur/boxShadow factors using PARTICLE_BRIGHTNESS) and then use
emberConfigs[i] inside the embers.map render (use a stable key from the config
or index) instead of calling Math.random inline so the per-particle config is
computed once and reused across re-renders.
frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx (1)

11-11: ⚡ Quick win

Replace !flex with Tailwind v4’s preferred important syntax flex! (align with v4’s non-deprecated form).

In Tailwind v4, !flex is deprecated but still supported; switching to flex! avoids future breakage/lint warnings.

File: frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx (line 11)

🛠️ Suggested fix
-        <LavaAnimate className="!flex w-full" particleCount={90} formedDelay={1800} meltAmount={3}>
+        <LavaAnimate className="flex! w-full" particleCount={90} formedDelay={1800} meltAmount={3}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx`
at line 11, The className on the LavaAnimate usage currently uses Tailwind v3's
important syntax "!flex" which is deprecated; update the className string on the
LavaAnimate component (the JSX element named LavaAnimate) to replace "!flex"
with the Tailwind v4 preferred "flex!" while keeping the rest of the classes
(e.g., "w-full") unchanged so the prop reads like "flex! w-full".
frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx (1)

10-10: ⚡ Quick win

Update Tailwind v4 important syntax: use flex! instead of !flex
This project uses Tailwind v4 (frontend/package.json), where the leading-! form like !flex is deprecated but still supported for compatibility—switching to the suffix form keeps styles consistent.

🛠️ Suggested fix
-      <LavaAnimate className="!flex w-full justify-center" particleCount={120} formedDelay={1800} meltAmount={10} duration={3.5} particleSize={[15, 38]}>
+      <LavaAnimate className="flex! w-full justify-center" particleCount={120} formedDelay={1800} meltAmount={10} duration={3.5} particleSize={[15, 38]}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx`
at line 10, Replace deprecated leading-! Tailwind syntax in the About.jsx
component: update the className passed to LavaAnimate (currently containing
"!flex w-full justify-center") to use the Tailwind v4 suffix form by changing
"!flex" to "flex!" so it reads "flex! w-full justify-center" to keep styles
consistent with Tailwind v4 conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/Contact.jsx`:
- Around line 24-26: The controlProps assignment currently sets 'aria-disabled'
to a boolean/undefined; change it so aria-disabled uses the string "true" when
disabled and is omitted otherwise. Specifically, in the controlProps conditional
that checks Tag === motion.button, update the non-button branch to set {
'aria-disabled': disabled ? 'true' : undefined } (keep the motion.button branch
as { disabled: disabled || undefined }); this ensures aria-disabled conforms to
ARIA spec while preserving existing behavior.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx`:
- Around line 51-64: The TileSnappingText usages are passing hardcoded fallback
strings (data.name || "Shakti Shrey" and data.title || "Full Stack Developer");
remove those fixed identity fallbacks so the component only renders real profile
data: either pass an empty string or conditionally render TileSnappingText only
when data.name / data.title exist (refer to the TileSnappingText instances in
this file), ensuring no hardcoded personal identity appears when data is
missing.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/MosaicImage.jsx`:
- Around line 55-74: The tiles currently render a visually-hidden <span
className="sr-only">{alt}</span> inside each motion.div causing repeated alt
announcements; instead mark each tile as decorative (add aria-hidden="true" to
the motion.div or otherwise remove the sr-only span) and expose one single
accessible image node with the real alt text (for example a single <img> or a
single sr-only element outside the tile map). Update the tile rendering in
MosaicImage.jsx (the motion.div instances keyed by `${tile.r}-${tile.c}`) to be
aria-hidden and remove the per-tile sr-only span, and ensure one central
accessible element uses the alt string.

In `@frontend/src/components/portfolio/templates/Page_Turner/ContactPage.jsx`:
- Around line 45-47: The inputs and textarea in ContactPage.jsx currently rely
only on placeholders (value={form.name}, value={form.email},
value={form.message} with onChange using setForm), so add accessible labels by
giving each control a unique id (e.g. nameInput, emailInput, messageInput) and
adding corresponding <label htmlFor="..."> elements with visible or
visually-hidden text, or at minimum add descriptive aria-label attributes to the
<input> and <textarea> (e.g. aria-label="Name", aria-label="Email",
aria-label="Message"); keep the existing placeholders and onChange handlers
(setForm) intact and ensure the textarea also receives the id/aria-label.

In `@frontend/src/components/portfolio/templates/Page_Turner/shared.jsx`:
- Around line 75-77: Guard uses of event.target before calling .closest: ensure
event.target is an Element (or has a closest method) before assigning to target
and invoking target.closest; update the handler around the
target/scrollableParent logic (the variables target, scrollableParent and the
call to .closest) to first check e.g. event.target instanceof Element (or
Boolean(event.target && event.target.closest)) and only call .closest when that
check passes, otherwise treat scrollableParent as null/undefined and skip the
scrolling logic.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Contact.jsx`:
- Line 9: Update Tailwind v4 important-suffix syntax by replacing the old
"!flex" important modifier with the new "flex!" form in the Volcanic_Forge
template components; specifically change the className on the LavaAnimate usage
(LavaAnimate className="!flex w-full justify-center" → use "flex! w-full
justify-center") and make the same replacement in the other components mentioned
(Projects, Skills, Experience, Contact, About) wherever "!flex" appears so they
use the "flex!" suffix form.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Experience.jsx`:
- Line 10: Update the Tailwind important modifier in the LavaAnimate component's
className: locate the JSX element LavaAnimate and replace the old prefix syntax
"!flex" with the Tailwind v4 suffix form "flex!" inside the className string
(e.g., className="!flex w-full flex-col" -> className="flex! w-full flex-col");
also scan the same file for any other "!<utility>" occurrences and convert them
to the "<utility>!" suffix form to keep Tailwind v4 syntax consistent.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Hero.jsx`:
- Around line 37-43: The icon-only social links rendered by motion.a lack
accessible names; update the anchor (motion.a) to include a clear accessible
name by adding an aria-label (e.g., aria-label={item.label || item.name ||
item.url}) and/or include a visually-hidden text node (screen-reader-only span)
inside the anchor alongside <item.icon> so assistive technologies can identify
the link; ensure you reference the same item object used for icon and
waveGradient and provide a sensible fallback if item.label is missing.

---

Nitpick comments:
In
`@frontend/src/components/portfolio/templates/Mosaic_Assemble/Testimonials.jsx`:
- Line 31: Replace the unstable array index key in the Testimonials component
map (currently key={i}) with a stable unique identifier: if each testimonial
object has an id use key={t.id}; otherwise derive a stable key from testimonial
fields (e.g., combine t.name and t.role) when mapping over the testimonials (the
mapped item is referenced as t) so React can track items reliably during
reorders/filters.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx`:
- Line 10: Replace deprecated leading-! Tailwind syntax in the About.jsx
component: update the className passed to LavaAnimate (currently containing
"!flex w-full justify-center") to use the Tailwind v4 suffix form by changing
"!flex" to "flex!" so it reads "flex! w-full justify-center" to keep styles
consistent with Tailwind v4 conventions.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx`:
- Line 11: The className on the LavaAnimate usage currently uses Tailwind v3's
important syntax "!flex" which is deprecated; update the className string on the
LavaAnimate component (the JSX element named LavaAnimate) to replace "!flex"
with the Tailwind v4 preferred "flex!" while keeping the rest of the classes
(e.g., "w-full") unchanged so the prop reads like "flex! w-full".

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx`:
- Around line 26-42: The ember props are being re-generated on every render
causing animation jumps; in GlobalBackground.jsx create a stable emberConfigs
array (use useMemo) that maps each ember index to random values (size, duration
computed using PARTICLE_SPEED, delay, startX, x drift, opacity peak, and any
precomputed blur/boxShadow factors using PARTICLE_BRIGHTNESS) and then use
emberConfigs[i] inside the embers.map render (use a stable key from the config
or index) instead of calling Math.random inline so the per-particle config is
computed once and reused across re-renders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b19040ea-45ab-4ba9-abe5-43933ce46ad8

📥 Commits

Reviewing files that changed from the base of the PR and between 381624a and f8b72a6.

📒 Files selected for processing (31)
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/About.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Contact.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Experience.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/MosaicImage.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Projects.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/ScatterText.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Skills.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/Testimonials.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/TileSnappingText.jsx
  • frontend/src/components/portfolio/templates/Mosaic_Assemble/index.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/AboutPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/BackCoverPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/ContactPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/ExperiencePage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/HeroPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/ProjectsPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/SkillsPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/TestimonialsPage.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/index.jsx
  • frontend/src/components/portfolio/templates/Page_Turner/shared.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/LavaAnimate.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Contact.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Experience.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Hero.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Projects.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/SectionHeader.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/index.jsx

Comment on lines +24 to +26
const controlProps = Tag === motion.button
? { disabled: disabled || undefined }
: { 'aria-disabled': disabled || undefined };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct aria-disabled to use string value.

The aria-disabled attribute expects a string value "true" or "false", not a boolean or undefined. Per ARIA spec, the attribute should either be set to the string "true" or omitted entirely.

🛠️ Proposed fix
  const Tag = disabled ? motion.button : motion.a;
  const controlProps = Tag === motion.button
-    ? { disabled: disabled || undefined }
-    : { 'aria-disabled': disabled || undefined };
+    ? { disabled }
+    : disabled ? { 'aria-disabled': 'true' } : {};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const controlProps = Tag === motion.button
? { disabled: disabled || undefined }
: { 'aria-disabled': disabled || undefined };
const controlProps = Tag === motion.button
? { disabled }
: disabled ? { 'aria-disabled': 'true' } : {};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/Contact.jsx`
around lines 24 - 26, The controlProps assignment currently sets 'aria-disabled'
to a boolean/undefined; change it so aria-disabled uses the string "true" when
disabled and is omitted otherwise. Specifically, in the controlProps conditional
that checks Tag === motion.button, update the non-button branch to set {
'aria-disabled': disabled ? 'true' : undefined } (keep the motion.button branch
as { disabled: disabled || undefined }); this ensures aria-disabled conforms to
ARIA spec while preserving existing behavior.

Comment on lines +51 to +64
<TileSnappingText
text={data.name || "Shakti Shrey"}
className="text-transparent bg-clip-text bg-gradient-to-r from-white via-slate-200 to-slate-400"
baseDelay={0.1}
/>
</h1>
</div>

{/* 2. Subtitle Description */}
<div className="mb-12 w-full overflow-visible flex justify-center">
<p className="text-xl sm:text-2xl md:text-3xl font-medium tracking-tight">
<TileSnappingText
text={data.title || "Full Stack Developer"}
className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-teal-300 to-indigo-400"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove hardcoded profile identity fallbacks in data-driven content.

Lines 52-53 and 63-64 can render fixed identity text when data is absent, which risks showing the wrong profile content.

Proposed fix
               <TileSnappingText 
-                text={data.name || "Shakti Shrey"} 
+                text={data.name ?? ""} 
                 className="text-transparent bg-clip-text bg-gradient-to-r from-white via-slate-200 to-slate-400"
                 baseDelay={0.1}
               />
@@
               <TileSnappingText 
-                text={data.title || "Full Stack Developer"} 
+                text={data.title ?? ""} 
                 className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-teal-300 to-indigo-400"
                 baseDelay={0.4} 
               />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/Hero.jsx` around
lines 51 - 64, The TileSnappingText usages are passing hardcoded fallback
strings (data.name || "Shakti Shrey" and data.title || "Full Stack Developer");
remove those fixed identity fallbacks so the component only renders real profile
data: either pass an empty string or conditionally render TileSnappingText only
when data.name / data.title exist (refer to the TileSnappingText instances in
this file), ensuring no hardcoded personal identity appears when data is
missing.

Comment on lines +55 to +74
<motion.div
key={`${tile.r}-${tile.c}`}
initial={{ opacity: 0, x: tile.x, y: tile.y, rotate: tile.rot, scale: tile.scale }}
variants={{
hidden: {},
show: {
opacity: 1, x: 0, y: 0, rotate: 0, scale: 1,
transition: { ...tile.spring, delay: tile.delay },
},
}}
className="relative overflow-hidden shadow-sm"
style={{
backgroundImage: `url(${src})`,
backgroundSize: `${cols * 100}% ${rows * 100}%`,
backgroundPosition: `${-tile.c * 100}% ${-tile.r * 100}%`,
backgroundRepeat: 'no-repeat',
}}
>
<span className="sr-only">{alt}</span>
</motion.div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid repeating image alt text for every tile.

Line 73 duplicates screen-reader text per tile, which can cause the same alt to be announced many times. Keep a single accessible image node and hide decorative tiles from assistive tech.

Proposed fix
-          <motion.div
+          <motion.div
             key={`${tile.r}-${tile.c}`}
+            aria-hidden="true"
             initial={{ opacity: 0, x: tile.x, y: tile.y, rotate: tile.rot, scale: tile.scale }}
             variants={{
               hidden: {},
               show: {
                 opacity: 1, x: 0, y: 0, rotate: 0, scale: 1,
                 transition: { ...tile.spring, delay: tile.delay },
               },
             }}
             className="relative overflow-hidden shadow-sm"
             style={{
               backgroundImage: `url(${src})`,
               backgroundSize: `${cols * 100}% ${rows * 100}%`,
               backgroundPosition: `${-tile.c * 100}% ${-tile.r * 100}%`,
               backgroundRepeat: 'no-repeat',
             }}
           >
-            <span className="sr-only">{alt}</span>
           </motion.div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<motion.div
key={`${tile.r}-${tile.c}`}
initial={{ opacity: 0, x: tile.x, y: tile.y, rotate: tile.rot, scale: tile.scale }}
variants={{
hidden: {},
show: {
opacity: 1, x: 0, y: 0, rotate: 0, scale: 1,
transition: { ...tile.spring, delay: tile.delay },
},
}}
className="relative overflow-hidden shadow-sm"
style={{
backgroundImage: `url(${src})`,
backgroundSize: `${cols * 100}% ${rows * 100}%`,
backgroundPosition: `${-tile.c * 100}% ${-tile.r * 100}%`,
backgroundRepeat: 'no-repeat',
}}
>
<span className="sr-only">{alt}</span>
</motion.div>
<motion.div
key={`${tile.r}-${tile.c}`}
aria-hidden="true"
initial={{ opacity: 0, x: tile.x, y: tile.y, rotate: tile.rot, scale: tile.scale }}
variants={{
hidden: {},
show: {
opacity: 1, x: 0, y: 0, rotate: 0, scale: 1,
transition: { ...tile.spring, delay: tile.delay },
},
}}
className="relative overflow-hidden shadow-sm"
style={{
backgroundImage: `url(${src})`,
backgroundSize: `${cols * 100}% ${rows * 100}%`,
backgroundPosition: `${-tile.c * 100}% ${-tile.r * 100}%`,
backgroundRepeat: 'no-repeat',
}}
>
</motion.div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Mosaic_Assemble/MosaicImage.jsx`
around lines 55 - 74, The tiles currently render a visually-hidden <span
className="sr-only">{alt}</span> inside each motion.div causing repeated alt
announcements; instead mark each tile as decorative (add aria-hidden="true" to
the motion.div or otherwise remove the sr-only span) and expose one single
accessible image node with the real alt text (for example a single <img> or a
single sr-only element outside the tile map). Update the tile rendering in
MosaicImage.jsx (the motion.div instances keyed by `${tile.r}-${tile.c}`) to be
aria-hidden and remove the per-tile sr-only span, and ensure one central
accessible element uses the alt string.

Comment on lines +45 to +47
<input value={form.name} onChange={(e) => setForm(c => ({...c, name: e.target.value}))} placeholder="NAME" className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<input value={form.email} onChange={(e) => setForm(c => ({...c, email: e.target.value}))} placeholder="EMAIL" className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<textarea value={form.message} onChange={(e) => setForm(c => ({...c, message: e.target.value}))} placeholder="MESSAGE" rows={3} className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500 resize-none" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible labels to the form controls.

These fields currently depend on placeholders only, which is not sufficient for assistive tech.

♿ Proposed fix
-                <input value={form.name} onChange={(e) => setForm(c => ({...c, name: e.target.value}))} placeholder="NAME" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
-                <input value={form.email} onChange={(e) => setForm(c => ({...c, email: e.target.value}))} placeholder="EMAIL" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
-                <textarea value={form.message} onChange={(e) => setForm(c => ({...c, message: e.target.value}))} placeholder="MESSAGE" rows={3} className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500 resize-none" />
+                <label htmlFor="contact-name" className="sr-only">Name</label>
+                <input id="contact-name" value={form.name} onChange={(e) => setForm(c => ({...c, name: e.target.value}))} placeholder="NAME" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
+                <label htmlFor="contact-email" className="sr-only">Email</label>
+                <input id="contact-email" type="email" value={form.email} onChange={(e) => setForm(c => ({...c, email: e.target.value}))} placeholder="EMAIL" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
+                <label htmlFor="contact-message" className="sr-only">Message</label>
+                <textarea id="contact-message" value={form.message} onChange={(e) => setForm(c => ({...c, message: e.target.value}))} placeholder="MESSAGE" rows={3} className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500 resize-none" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input value={form.name} onChange={(e) => setForm(c => ({...c, name: e.target.value}))} placeholder="NAME" className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<input value={form.email} onChange={(e) => setForm(c => ({...c, email: e.target.value}))} placeholder="EMAIL" className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<textarea value={form.message} onChange={(e) => setForm(c => ({...c, message: e.target.value}))} placeholder="MESSAGE" rows={3} className="w-full rounded border border-slate-800 bg-[#020617] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500 resize-none" />
<label htmlFor="contact-name" className="sr-only">Name</label>
<input id="contact-name" value={form.name} onChange={(e) => setForm(c => ({...c, name: e.target.value}))} placeholder="NAME" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<label htmlFor="contact-email" className="sr-only">Email</label>
<input id="contact-email" type="email" value={form.email} onChange={(e) => setForm(c => ({...c, email: e.target.value}))} placeholder="EMAIL" className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500" />
<label htmlFor="contact-message" className="sr-only">Message</label>
<textarea id="contact-message" value={form.message} onChange={(e) => setForm(c => ({...c, message: e.target.value}))} placeholder="MESSAGE" rows={3} className="w-full rounded border border-slate-800 bg-[`#020617`] px-3 py-2 text-[10px] text-white outline-none focus:border-cyan-500 resize-none" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Page_Turner/ContactPage.jsx`
around lines 45 - 47, The inputs and textarea in ContactPage.jsx currently rely
only on placeholders (value={form.name}, value={form.email},
value={form.message} with onChange using setForm), so add accessible labels by
giving each control a unique id (e.g. nameInput, emailInput, messageInput) and
adding corresponding <label htmlFor="..."> elements with visible or
visually-hidden text, or at minimum add descriptive aria-label attributes to the
<input> and <textarea> (e.g. aria-label="Name", aria-label="Email",
aria-label="Message"); keep the existing placeholders and onChange handlers
(setForm) intact and ensure the textarea also receives the id/aria-label.

Comment on lines +75 to +77
const target = event.target
const scrollableParent = target.closest('.custom-scrollbar, .overflow-y-auto')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that `.closest()` is called on `event.target` without an Element guard.
rg -n -C3 'event\.target|target\.closest|instanceof Element' frontend/src/components/portfolio/templates/Page_Turner/shared.jsx

Repository: anurag3407/career-pilot

Length of output: 374


Guard event.target before calling .closest()

frontend/src/components/portfolio/templates/Page_Turner/shared.jsx calls target.closest(...) where target is assigned directly from event.target; if event.target isn’t an Element, closest can throw during wheel handling.

<details>
<summary>🔧 Proposed fix</summary>

-      const target = event.target
-      const scrollableParent = target.closest('.custom-scrollbar, .overflow-y-auto')
+      const target = event.target instanceof Element ? event.target : null
+      const scrollableParent = target?.closest('.custom-scrollbar, .overflow-y-auto')

</details>

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/portfolio/templates/Page_Turner/shared.jsx` around
lines 75 - 77, Guard uses of event.target before calling .closest: ensure
event.target is an Element (or has a closest method) before assigning to target
and invoking target.closest; update the handler around the
target/scrollableParent logic (the variables target, scrollableParent and the
call to .closest) to first check e.g. event.target instanceof Element (or
Boolean(event.target && event.target.closest)) and only call .closest when that
check passes, otherwise treat scrollableParent as null/undefined and skip the
scrolling logic.

Comment thread frontend/src/components/portfolio/templates/Volcanic_Forge/components/Contact.jsx Outdated
Comment on lines +37 to +43
<motion.a
key={idx} href={item.url} target="_blank" rel="noreferrer" initial="initial" whileHover="hover"
className={`lava-btn relative inline-flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-full overflow-hidden border p-0 ${item.toneClass}`}
>
<motion.span variants={lavaVariants} className="wave-fill absolute left-0 top-0 bottom-0" style={{ width: '0%', backgroundImage: item.waveGradient }} />
<item.icon className="relative z-10 block h-5 w-5 md:h-6 md:w-6 shrink-0" />
</motion.a>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible names to icon-only social links.

Line 37 renders icon-only anchors without an accessible name, so assistive tech users can’t identify destination links reliably.

♿ Suggested fix
               <motion.a
                 key={idx} href={item.url} target="_blank" rel="noreferrer" initial="initial" whileHover="hover"
+                aria-label={item.label}
+                title={item.label}
                 className={`lava-btn relative inline-flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-full overflow-hidden border p-0 ${item.toneClass}`}
               >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<motion.a
key={idx} href={item.url} target="_blank" rel="noreferrer" initial="initial" whileHover="hover"
className={`lava-btn relative inline-flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-full overflow-hidden border p-0 ${item.toneClass}`}
>
<motion.span variants={lavaVariants} className="wave-fill absolute left-0 top-0 bottom-0" style={{ width: '0%', backgroundImage: item.waveGradient }} />
<item.icon className="relative z-10 block h-5 w-5 md:h-6 md:w-6 shrink-0" />
</motion.a>
<motion.a
key={idx} href={item.url} target="_blank" rel="noreferrer" initial="initial" whileHover="hover"
aria-label={item.label}
title={item.label}
className={`lava-btn relative inline-flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-full overflow-hidden border p-0 ${item.toneClass}`}
>
<motion.span variants={lavaVariants} className="wave-fill absolute left-0 top-0 bottom-0" style={{ width: '0%', backgroundImage: item.waveGradient }} />
<item.icon className="relative z-10 block h-5 w-5 md:h-6 md:w-6 shrink-0" />
</motion.a>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/components/Hero.jsx`
around lines 37 - 43, The icon-only social links rendered by motion.a lack
accessible names; update the anchor (motion.a) to include a clear accessible
name by adding an aria-label (e.g., aria-label={item.label || item.name ||
item.url}) and/or include a visually-hidden text node (screen-reader-only span)
inside the anchor alongside <item.icon> so assistive technologies can identify
the link; ensure you reference the same item object used for icon and
waveGradient and provide a sensible fallback if item.label is missing.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx`:
- Around line 51-65: The ember animation uses infinite motion on the motion.div
mapped from emberConfigs and should respect reduced-motion preferences: import
and call useReducedMotion (from framer-motion) in the GlobalBackground
component, store e.g. const shouldReduce = useReducedMotion(), and if
shouldReduce is true replace the motion animation with a static rendering
(either render a non-animated <div> or pass no animate/transition and only set
final opacity/transform) for the mapped ember elements (the motion.div created
inside the emberConfigs.map). Ensure the branch still applies the same
size/position/style (width/height/left/filter/boxShadow/opacity) but omits
repeat: Infinity and keyframe arrays when reduced motion is enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1009188-b7cb-4e98-bc6d-a844c609b4b0

📥 Commits

Reviewing files that changed from the base of the PR and between f8b72a6 and ec8fe39.

📒 Files selected for processing (6)
  • frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Contact.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Experience.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Projects.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Projects.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Contact.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/About.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Experience.jsx
  • frontend/src/components/portfolio/templates/Volcanic_Forge/components/Skills.jsx

Comment on lines +51 to +65
{emberConfigs.map((config) => {
return (
<motion.div
key={config.key}
className="absolute bottom-0 rounded-full bg-orange-500"
style={{
width: config.size,
height: config.size,
left: `${config.startX}vw`,
filter: `blur(${config.blur}px) brightness(${PARTICLE_BRIGHTNESS})`,
boxShadow: `0 0 ${config.glowSpread}px ${config.glowBlur}px rgba(249, 115, 22, ${0.5 * Math.min(PARTICLE_BRIGHTNESS, 2)})`
}}
animate={{ y: ['10vh', '-110vh'], x: [0, config.driftX], opacity: [0, config.opacityPeak, 0] }}
transition={{ duration: config.duration, repeat: Infinity, delay: config.delay, ease: 'linear' }}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Framer Motion v12 documentation for useReducedMotion and recommended pattern to disable or simplify infinite animations for prefers-reduced-motion users.

💡 Result:

Framer Motion v12 (now Motion for React) “reduced motion” recommendations for infinite animations 1) Per-component pattern: useReducedMotion Use the useReducedMotion hook to detect whether the user has enabled “Reduced Motion”. It returns true if reduced motion is enabled, and it updates in response to changes. [1] [2] Recommended pattern: when reduced motion is true, switch an infinite/looping animation to a non-moving alternative (e.g., keep x/y fixed, use opacity-only, or use a finite transition). Example (turn an infinite horizontal marquee into a static position): import { motion, useReducedMotion } from "motion/react" export function AccessibleMarquee { const shouldReduceMotion = useReducedMotion return ( <motion.div animate={{ x: shouldReduceMotion? 0: ["0%", "-100%"], }} transition={{ repeat: shouldReduceMotion? 0: Infinity, duration: 10, ease: "linear", }} />) } This matches the hook’s documented guidance that you can replace motion-sickness inducing x/y animations with simpler ones (like opacity) or otherwise change behavior based on reduced motion. [1] [2] 2) Site-wide pattern: MotionConfig with reducedMotion="user" If you want a site-wide policy, Motion provides a MotionConfig component with a reducedMotion option. MotionConfig reducedMotion accepts: - "user" (default): respect the user’s device setting [3] - "always" / "never": force reduced motion on or off (useful for debugging) [3] When reduced motion is on, Motion disables transform and layout animations, while other animations (e.g., opacity, backgroundColor) persist. [3] Example: import { MotionConfig, motion } from "motion/react" export function App { return ( <motion.div animate={{ x: 100 }} /> ) } This is the recommended “global” approach when you have many components and want consistent reduced-motion handling without sprinkling per-component conditionals. [3] How this maps to prefers-reduced-motion The library-level behavior is intended to reflect the device’s “Reduced Motion” setting (prefers-reduced-motion). The CSS prefers-reduced-motion media feature is explicitly designed to detect that preference. [4] Bottom line - For “disable/simplify infinite animations” specifically: gate your infinite repeat (e.g., repeat: Infinity and keyframed x/y loops) behind useReducedMotion, and use a static or opacity-only alternative when shouldReduceMotion is true. [1] - For broader coverage: wrap your app in MotionConfig reducedMotion="user" so transform/layout animations are disabled automatically for users who prefer reduced motion. [3]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "---- File: $FILE (relevant area) ----"
nl -ba "$FILE" | sed -n '1,140p'

echo "---- framer-motion imports in the file ----"
rg -n "from 'framer-motion'|from \"framer-motion\"|from 'motion/react'|useReducedMotion|MotionConfig|reducedMotion" "$FILE" || true

echo "---- Package/dependency versions (best-effort) ----"
ls -la package.json frontend/package.json 2>/dev/null || true
if [ -f "package.json" ]; then
  rg -n "\"framer-motion\"|\"motion\"" package.json || true
fi
if [ -f "frontend/package.json" ]; then
  rg -n "\"framer-motion\"|\"motion\"" frontend/package.json || true
fi

echo "---- Search for existing reduced motion usage patterns ----"
rg -n "useReducedMotion|MotionConfig|reducedMotion" frontend/src || true

Repository: anurag3407/career-pilot

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "---- File: $FILE (first ~140 lines with numbers) ----"
cat -n "$FILE" | sed -n '1,160p'

echo "---- framer-motion / reduced-motion imports & usage in the file ----"
rg -n "from 'framer-motion'|from \"framer-motion\"|from 'motion/react'|useReducedMotion|MotionConfig|reducedMotion|prefers-reduced-motion" "$FILE" || true

echo "---- package.json (dependency versions best-effort) ----"
if [ -f "package.json" ]; then
  echo "== root package.json =="
  cat -n package.json | sed -n '1,140p'
  rg -n "\"framer-motion\"|\"motion\"" package.json || true
fi
if [ -f "frontend/package.json" ]; then
  echo "== frontend/package.json =="
  cat -n frontend/package.json | sed -n '1,220p'
  rg -n "\"framer-motion\"|\"motion\"" frontend/package.json || true
fi

echo "---- Search for existing reduced motion usage patterns ----"
rg -n "useReducedMotion|MotionConfig|reducedMotion" frontend/src || true

Repository: anurag3407/career-pilot

Length of output: 6171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file that already uses useReducedMotion
FILE="frontend/src/components/portfolio/templates/Mosaic_Assemble/ScatterText.jsx"
if [ -f "$FILE" ]; then
  echo "---- $FILE (first 120 lines) ----"
  cat -n "$FILE" | sed -n '1,160p'
fi

echo "---- Any other reduced-motion related usage ----"
rg -n "useReducedMotion|MotionConfig|reducedMotion|prefers-reduced-motion" frontend/src || true

Repository: anurag3407/career-pilot

Length of output: 2169


Respect prefers-reduced-motion for infinite ember animation.

frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx animates ember keyframes with transition repeat: Infinity (lines 63-64) but doesn’t gate the animation for reduced-motion users. Apply useReducedMotion (already used in Mosaic_Assemble/ScatterText.jsx) to switch to a static/opacity-only rendering when reduced motion is enabled.

♿ Proposed refactor
-import { motion } from 'framer-motion';
+import { motion, useReducedMotion } from 'framer-motion';

 export default function GlobalBackground() {
+  const shouldReduceMotion = useReducedMotion();
   const emberConfigs = useMemo(() => (
     Array.from({ length: 40 }, (_, index) => {
       const size = Math.random() * 4 + 1;
@@
             <motion.div
               key={config.key}
               className="absolute bottom-0 rounded-full bg-orange-500"
               style={{
                 width: config.size,
                 height: config.size,
                 left: `${config.startX}vw`,
                 filter: `blur(${config.blur}px) brightness(${PARTICLE_BRIGHTNESS})`,
                 boxShadow: `0 0 ${config.glowSpread}px ${config.glowBlur}px rgba(249, 115, 22, ${0.5 * Math.min(PARTICLE_BRIGHTNESS, 2)})`
               }}
-              animate={{ y: ['10vh', '-110vh'], x: [0, config.driftX], opacity: [0, config.opacityPeak, 0] }}
-              transition={{ duration: config.duration, repeat: Infinity, delay: config.delay, ease: 'linear' }}
+              animate={
+                shouldReduceMotion
+                  ? { opacity: config.opacityPeak * 0.35 }
+                  : { y: ['10vh', '-110vh'], x: [0, config.driftX], opacity: [0, config.opacityPeak, 0] }
+              }
+              transition={
+                shouldReduceMotion
+                  ? { duration: 0 }
+                  : { duration: config.duration, repeat: Infinity, delay: config.delay, ease: 'linear' }
+              }
             />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{emberConfigs.map((config) => {
return (
<motion.div
key={config.key}
className="absolute bottom-0 rounded-full bg-orange-500"
style={{
width: config.size,
height: config.size,
left: `${config.startX}vw`,
filter: `blur(${config.blur}px) brightness(${PARTICLE_BRIGHTNESS})`,
boxShadow: `0 0 ${config.glowSpread}px ${config.glowBlur}px rgba(249, 115, 22, ${0.5 * Math.min(PARTICLE_BRIGHTNESS, 2)})`
}}
animate={{ y: ['10vh', '-110vh'], x: [0, config.driftX], opacity: [0, config.opacityPeak, 0] }}
transition={{ duration: config.duration, repeat: Infinity, delay: config.delay, ease: 'linear' }}
/>
{emberConfigs.map((config) => {
return (
<motion.div
key={config.key}
className="absolute bottom-0 rounded-full bg-orange-500"
style={{
width: config.size,
height: config.size,
left: `${config.startX}vw`,
filter: `blur(${config.blur}px) brightness(${PARTICLE_BRIGHTNESS})`,
boxShadow: `0 0 ${config.glowSpread}px ${config.glowBlur}px rgba(249, 115, 22, ${0.5 * Math.min(PARTICLE_BRIGHTNESS, 2)})`
}}
animate={
shouldReduceMotion
? { opacity: config.opacityPeak * 0.35 }
: { y: ['10vh', '-110vh'], x: [0, config.driftX], opacity: [0, config.opacityPeak, 0] }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: config.duration, repeat: Infinity, delay: config.delay, ease: 'linear' }
}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/components/portfolio/templates/Volcanic_Forge/GlobalBackground.jsx`
around lines 51 - 65, The ember animation uses infinite motion on the motion.div
mapped from emberConfigs and should respect reduced-motion preferences: import
and call useReducedMotion (from framer-motion) in the GlobalBackground
component, store e.g. const shouldReduce = useReducedMotion(), and if
shouldReduce is true replace the motion animation with a static rendering
(either render a non-animated <div> or pass no animate/transition and only set
final opacity/transform) for the mapped ember elements (the motion.div created
inside the emberConfigs.map). Ensure the branch still applies the same
size/position/style (width/height/left/filter/boxShadow/opacity) but omits
repeat: Infinity and keyframe arrays when reduced motion is enabled.

@anurag3407 anurag3407 added level:advanced Advanced level task quality:clean High quality code gssoc:approved Verified contribution portfolio-template Portfolio Template Task labels May 28, 2026
@anurag3407 anurag3407 merged commit 4b8a55f into anurag3407:main May 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved Verified contribution level:advanced Advanced level task portfolio-template Portfolio Template Task quality:clean High quality code size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Portfolio Template] Build Complete "Volcanic Forge" Portfolio (Nature / Organic)

3 participants