Add prefers reduced motion handling

This commit is contained in:
Ategon 2025-01-20 00:57:59 -05:00
parent feb80916cc
commit b11ad9354b
7 changed files with 156 additions and 16 deletions

View file

@ -1,5 +1,7 @@
"use client";
import { Button } from "@nextui-org/react";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
interface ButtonActionProps {
icon?: ReactNode;
@ -12,10 +14,28 @@ export default function ButtonAction({
onPress,
name,
}: ButtonActionProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return (
<Button
endContent={icon}
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
className={`text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform duration-500 ease-in-out ${
!reduceMotion ? "hover:scale-110" : ""
}`}
variant="bordered"
onPress={onPress}
>

View file

@ -1,5 +1,7 @@
"use client";
import { Button, Link } from "@nextui-org/react";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
interface ButtonLinkProps {
icon?: ReactNode;
@ -8,14 +10,34 @@ interface ButtonLinkProps {
}
export default function ButtonLink({ icon, href, name }: ButtonLinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return (
<Link
href={href}
className="flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
className={`flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-110" : ""
}`}
>
<Button
endContent={icon}
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out"
className={`text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out ${
!reduceMotion ? "hover:scale-110" : ""
}`}
variant="bordered"
>
{name}

View file

@ -1,5 +1,7 @@
"use client";
import { Link } from "@nextui-org/react";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
interface IconLinkProps {
icon: ReactNode;
@ -7,10 +9,28 @@ interface IconLinkProps {
}
export default function IconLink({ icon, href }: IconLinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return (
<Link
href={href}
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 duration-500 ease-in-out transition-color"
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-125" : ""
} transition-color`}
>
{icon}
</Link>

View file

@ -1,4 +1,7 @@
"use client";
import { Link as BaseLink } from "@nextui-org/react";
import { useEffect, useState } from "react";
interface LinkProps {
name: string;
@ -6,10 +9,28 @@ interface LinkProps {
}
export default function Link({ name, href }: LinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return (
<BaseLink
href={href}
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110 duration-500 ease-in-out transition-color"
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-110" : ""
} transition-color`}
>
{name}
</BaseLink>

View file

@ -38,6 +38,21 @@ export default function PCNavbar() {
const [jam, setJam] = useState<JamType | null>();
const [isInJam, setIsInJam] = useState<boolean>();
const [user, setUser] = useState<UserType>();
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
useEffect(() => {
loadUser();
@ -91,7 +106,9 @@ export default function PCNavbar() {
<NavbarBrand className="flex-grow-0">
<Link
href="/"
className="duration-500 ease-in-out transition-all transform hover:scale-110"
className={`duration-500 ease-in-out transition-all transform ${
reduceMotion ? "" : "hover:scale-110"
}`}
>
<Image
as={NextImage}

View file

@ -6,7 +6,7 @@ import { Heart, LoaderCircle } from "lucide-react";
import { toast } from "react-toastify";
import { getCookie } from "@/helpers/cookie";
import { redirect } from "next/navigation";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
export default function LikeButton({ post }: { post: PostType }) {
@ -14,6 +14,21 @@ export default function LikeButton({ post }: { post: PostType }) {
const [loading, setLoading] = useState<boolean>(false);
const [liked, setLiked] = useState<boolean>(false);
const { theme } = useTheme();
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return (
<Button
@ -56,7 +71,7 @@ export default function LikeButton({ post }: { post: PostType }) {
if (response.status == 401) {
redirect("/login");
} else {
toast.error("An error occured");
toast.error("An error occurred");
return;
}
} else {
@ -79,7 +94,11 @@ export default function LikeButton({ post }: { post: PostType }) {
<Heart size={16} />
<Heart
size={16}
className={liked ? "animate-ping absolute top-0 left-0" : ""}
className={
liked && !reduceMotion
? "animate-ping absolute top-0 left-0"
: "absolute top-0 left-0"
}
style={{
position: "absolute",
top: "0",

View file

@ -8,16 +8,31 @@ export default function ThemeToggle() {
const [isSpinning, setIsSpinning] = useState<boolean>(false);
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState<boolean>(false);
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
const handleToggle = () => {
if (isSpinning) return;
setIsSpinning(true);
setTimeout(() => setIsSpinning(false), 500);
if (!reduceMotion) {
setIsSpinning(true);
setTimeout(() => setIsSpinning(false), 500);
}
setTheme(resolvedTheme === "dark" ? "light" : "dark");
};
@ -30,9 +45,15 @@ export default function ThemeToggle() {
<div
onClick={handleToggle}
style={{ cursor: "pointer" }}
className={`${isSpinning && "animate-[spin_0.5s_ease-out]"} `}
className={`${
isSpinning && !reduceMotion ? "animate-[spin_0.5s_ease-out]" : ""
}`}
>
<div className="!duration-250 !ease-linear !transition-all transform text-[#333] dark:text-white hover:scale-125">
<div
className={`!duration-250 !ease-linear !transition-all transform text-[#333] dark:text-white ${
!reduceMotion ? "hover:scale-125" : ""
}`}
>
{resolvedTheme === "dark" && <Moon />}
{resolvedTheme === "light" && <Sun />}
</div>