Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions src/components/shared/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client'

import { Fragment, useEffect, useRef } from 'react'
import { Fragment, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { motion } from 'framer-motion'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'

import { Container } from '@/components/shared/Container'

Expand Down Expand Up @@ -183,6 +183,29 @@ function DesktopNavigation(props) {
}

function ModeToggle() {
const [mounted, setMounted] = useState(false)
const [isDarkMode, setIsDarkMode] = useState(false)
const prefersReducedMotion = useReducedMotion()
const iconTransition = prefersReducedMotion
? { duration: 0 }
: { duration: 0.5, ease: 'easeInOut' }

useEffect(() => {
setMounted(true)
setIsDarkMode(document.documentElement.classList.contains('dark'))

const observer = new MutationObserver(() => {
setIsDarkMode(document.documentElement.classList.contains('dark'))
})

observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})

return () => observer.disconnect()
}, [])

function disableTransitionsTemporarily() {
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
Expand All @@ -204,15 +227,42 @@ function ModeToggle() {
}
}

if (!mounted) {
return (
<div className="h-10 w-10 rounded-full bg-white/90 ring-1 ring-black/10 dark:bg-zinc-800/90 dark:ring-white/10" />
)
}

return (
<button
type="button"
aria-label="Toggle dark mode"
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
className="group relative flex h-10 w-10 items-center justify-center rounded-full bg-white/90 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
Comment on lines 237 to +240
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Expose the current toggle state.

This is a two-state button, but it only exposes an action label right now. Adding aria-pressed makes the current mode understandable to assistive tech.

♿ Suggested change
 <button
   type="button"
   aria-label="Toggle dark mode"
+  aria-pressed={isDarkMode}
   className="group relative flex h-10 w-10 items-center justify-center rounded-full bg-white/90 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
   onClick={toggleMode}
 >
📝 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
<button
type="button"
aria-label="Toggle dark mode"
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
className="group relative flex h-10 w-10 items-center justify-center rounded-full bg-white/90 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
<button
type="button"
aria-label="Toggle dark mode"
aria-pressed={isDarkMode}
className="group relative flex h-10 w-10 items-center justify-center rounded-full bg-white/90 shadow-lg shadow-zinc-800/5 ring-1 ring-black/10 hover:ring-black/20 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/shared/Header.jsx` around lines 233 - 236, The toggle button
in Header.jsx currently only has an action label; update the button element (the
JSX <button> in the Header component) to expose its current state to assistive
tech by adding aria-pressed={isDarkMode} (or the boolean state variable that
represents dark mode in this component, e.g., isDark/isDarkTheme/darkMode) and
ensure that the boolean is derived from the same state used by the toggleTheme
handler so aria-pressed stays in sync with the visual mode.

onClick={toggleMode}
>
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-yellow-400 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-yellow-500" />
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
<AnimatePresence mode="wait" initial={false}>
{isDarkMode ? (
<motion.div
key="moon"
initial={{ opacity: 0, rotate: -90, scale: 0.5 }}
animate={{ opacity: 1, rotate: 0, scale: 1 }}
exit={{ opacity: 0, rotate: 90, scale: 0.5 }}
transition={iconTransition}
>
<MoonIcon className="h-6 w-6 fill-zinc-700 stroke-zinc-500 transition group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
</motion.div>
) : (
<motion.div
key="sun"
initial={{ opacity: 0, rotate: 90, scale: 0.5 }}
animate={{ opacity: 1, rotate: 0, scale: 1 }}
exit={{ opacity: 0, rotate: -90, scale: 0.5 }}
transition={iconTransition}
>
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-yellow-400 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-yellow-500" />
</motion.div>
)}
</AnimatePresence>
</button>
)
}
Expand Down
39 changes: 26 additions & 13 deletions src/components/shared/Pattern.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@ export function Pattern({
let height = pattern.length * size + (pattern.length - 1) * gapY

return (
<svg aria-hidden="true" width={width} height={height} {...props}>
<svg aria-hidden="true" width={width} height={height} className="overflow-visible" {...props}>
<defs>
<symbol id={`${id}-0`} width={size} height={size}>
<rect className="fill-green-200 dark:fill-yellow-200" width={size} height={size} />
<rect className="fill-green-200 dark:fill-yellow-200 transition-colors duration-500" width={size} height={size} />
<circle
className="fill-[#00843D] dark:fill-yellow-400"
className="fill-[#00843D] dark:fill-yellow-400 shadow-xl transition-colors duration-500"
cx={size / 2}
cy={size / 2}
r={size * (13 / 40)}
/>
</symbol>
<symbol id={`${id}-1`} width={size} height={size}>
<circle
className="fill-green-400 dark:fill-yellow-300 animate-pulse"
className="fill-green-300 dark:fill-yellow-300 animate-pulse transition-colors duration-500"
cx={size / 2}
cy={size / 2}
r={size / 2}
/>
<rect
className="fill-[#00843D] dark:fill-yellow-400"
className="fill-[#00843D] dark:fill-yellow-400 shadow-xl transition-colors duration-500"
Comment on lines +33 to +39
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "Pattern.jsx" --maxdepth 5

Repository: AOSSIE-Org/Website

Length of output: 138


🏁 Script executed:

git ls-files | grep -i pattern

Repository: AOSSIE-Org/Website

Length of output: 138


🏁 Script executed:

fd -t f "tailwind.config" -o "tailwind.js" | head -5

Repository: AOSSIE-Org/Website

Length of output: 203


🏁 Script executed:

wc -l src/components/shared/Pattern.jsx

Repository: AOSSIE-Org/Website

Length of output: 98


🏁 Script executed:

cat -n src/components/shared/Pattern.jsx

Repository: AOSSIE-Org/Website

Length of output: 3251


🏁 Script executed:

find . -maxdepth 2 -name "tailwind.config*" -o -name "tailwind.js"

Repository: AOSSIE-Org/Website

Length of output: 82


🏁 Script executed:

cat -n tailwind.config.js

Repository: AOSSIE-Org/Website

Length of output: 13659


🏁 Script executed:

grep -r "tailwindcss" package.json package-lock.json 2>/dev/null | head -10

Repository: AOSSIE-Org/Website

Length of output: 702


Wrap animations in motion-safe: and motion-reduce: variants to respect user motion preferences.

The animate-pulse effect on line 33, hover rotation effects on line 53, and hover scale/drop-shadow effects on line 64 run unconditionally. Users who disable motion in their operating system preferences will still see continuous animations. Use motion-safe: to gate these effects and motion-reduce: to optionally disable them explicitly, ensuring compliance with accessibility standards.

Suggested change
           <circle
-            className="fill-green-300 dark:fill-yellow-300 animate-pulse transition-colors duration-500"
+            className="fill-green-300 dark:fill-yellow-300 motion-safe:animate-pulse motion-reduce:animate-none transition-colors duration-500"
             cx={size / 2}
             cy={size / 2}
             r={size / 2}
           />
@@
-          const hoverRotation = (rowIndex + columnIndex) % 2 === 0 ? "group-hover:rotate-[15deg]" : "group-hover:-rotate-[15deg]"
+          const hoverRotation =
+            (rowIndex + columnIndex) % 2 === 0
+              ? 'motion-safe:group-hover:rotate-[15deg]'
+              : 'motion-safe:group-hover:-rotate-[15deg]'
@@
               <use
                 href={`#${id}-${shape}`}
                 x={x}
                 y={y}
                 style={{ transformOrigin: `${x + size / 2}px ${y + size / 2}px` }}
-                className={`transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] group-hover:scale-[1.4] ${hoverRotation} group-hover:drop-shadow-lg pointer-events-none`}
+                className={`transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] motion-safe:group-hover:scale-[1.4] ${hoverRotation} motion-safe:group-hover:drop-shadow-lg pointer-events-none`}
               />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/shared/Pattern.jsx` around lines 33 - 39, The Pattern
component is applying animations unconditionally (e.g., the circle/rect
classNames use animate-pulse and there are hover rotation/scale/drop-shadow
classes) which ignores users' reduced-motion preferences; update the Tailwind
className strings to prefix motion-safe: for the active animations (e.g.,
motion-safe:animate-pulse, motion-safe:hover:rotate-...,
motion-safe:hover:scale-..., motion-safe:hover:drop-shadow-...) and add
corresponding motion-reduce: fallbacks to disable them (e.g.,
motion-reduce:animate-none, motion-reduce:hover:rotate-0,
motion-reduce:hover:scale-100, motion-reduce:hover:drop-shadow-none) so the
Pattern component respects reduced-motion settings.

width={size / 2}
height={size / 2}
x={size / 4}
Expand All @@ -45,14 +45,27 @@ export function Pattern({
</symbol>
</defs>
{pattern.map((row, rowIndex) =>
row.map((shape, columnIndex) => (
<use
key={`${rowIndex}-${columnIndex}`}
href={`#${id}-${shape}`}
x={columnIndex * size + columnIndex * gapX}
y={rowIndex * size + rowIndex * gapY}
/>
))
row.map((shape, columnIndex) => {
const x = columnIndex * size + columnIndex * gapX;
const y = rowIndex * size + rowIndex * gapY;

// Determine a playful alternating rotation direction based on the grid index
const hoverRotation = (rowIndex + columnIndex) % 2 === 0 ? "group-hover:rotate-[15deg]" : "group-hover:-rotate-[15deg]";

return (
<g key={`${rowIndex}-${columnIndex}`} className="group cursor-pointer">
{/* Invisible, static hit area to handle hover without flickering */}
<rect x={x} y={y} width={size} height={size} fill="transparent" className="pointer-events-auto" />
<use
href={`#${id}-${shape}`}
x={x}
y={y}
style={{ transformOrigin: `${x + size / 2}px ${y + size / 2}px` }}
className={`transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] group-hover:scale-[1.4] ${hoverRotation} group-hover:drop-shadow-lg pointer-events-none`}
/>
</g>
);
})
)}
</svg>
)
Expand Down