diff options
Diffstat (limited to 'frontend/src/components/shared')
-rw-r--r-- | frontend/src/components/shared/PageTransition.tsx | 55 | ||||
-rw-r--r-- | frontend/src/components/shared/ThemeToggle.tsx | 53 |
2 files changed, 98 insertions, 10 deletions
diff --git a/frontend/src/components/shared/PageTransition.tsx b/frontend/src/components/shared/PageTransition.tsx new file mode 100644 index 0000000..fc95865 --- /dev/null +++ b/frontend/src/components/shared/PageTransition.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { usePathname } from 'next/navigation'; +import { ReactNode, useEffect, useState } from 'react'; + +interface PageTransitionProps { + children: ReactNode; +} + +export function PageTransition({ children }: PageTransitionProps) { + const pathname = usePathname(); + const [isFirstRender, setIsFirstRender] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => { + setIsFirstRender(false); + }, 500); + + return () => clearTimeout(timeout); + }, []); + + const variants = { + hidden: { opacity: 0, y: 20 }, + enter: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + }; + + // Only apply softer animation on initial render + const initialAnimation = isFirstRender ? { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + transition: { duration: 0.5 } + } : { + initial: "hidden", + animate: "enter", + exit: "exit", + variants, + transition: { + type: "tween", + ease: "easeInOut", + duration: 0.3 + } + }; + + return ( + <motion.div + key={pathname} + {...initialAnimation} + className="w-full h-full" + > + {children} + </motion.div> + ); +}
\ No newline at end of file diff --git a/frontend/src/components/shared/ThemeToggle.tsx b/frontend/src/components/shared/ThemeToggle.tsx index 679bbc5..b54bda9 100644 --- a/frontend/src/components/shared/ThemeToggle.tsx +++ b/frontend/src/components/shared/ThemeToggle.tsx @@ -1,36 +1,69 @@ 'use client'; +import { useState, useEffect } from 'react'; import { Moon, Sun } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useTheme } from './ThemeProvider'; export function ThemeToggle() { const { theme, setTheme } = useTheme(); + const [isAnimating, setIsAnimating] = useState(false); + + // Track initial mount to prevent animation on first render + const [isMounted, setIsMounted] = useState(false); + useEffect(() => { + setIsMounted(true); + }, []); const toggleTheme = () => { - if (theme === 'dark') { - setTheme('light'); - } else { - setTheme('dark'); - } + if (isAnimating) return; + + setIsAnimating(true); + + // Set new theme after a small delay for animation + const newTheme = theme === 'dark' ? 'light' : 'dark'; + setTimeout(() => { + setTheme(newTheme); + setTimeout(() => { + setIsAnimating(false); + }, 300); + }, 200); }; + if (!isMounted) { + return ( + <Button + variant="ghost" + size="icon" + className="opacity-0" + > + <Sun className="h-[1.2rem] w-[1.2rem]" /> + </Button> + ); + } + return ( <Button variant="ghost" size="icon" onClick={toggleTheme} + className={`relative overflow-hidden hover:bg-muted/80 hover:text-foreground transition-all duration-300 ${isAnimating ? 'cursor-wait' : ''}`} aria-label={ theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme' } + disabled={isAnimating} > - {theme === 'dark' ? ( - <Sun className="h-[1.2rem] w-[1.2rem]" /> - ) : ( - <Moon className="h-[1.2rem] w-[1.2rem]" /> - )} + <div className={`absolute inset-0 bg-primary/5 rounded-full transition-all duration-500 ${isAnimating ? 'scale-[5] opacity-0' : 'scale-0 opacity-0'}`}></div> + + <div className="relative"> + {theme === 'dark' ? ( + <Sun className={`h-5 w-5 transition-all duration-300 ${isAnimating ? 'rotate-90 scale-50 opacity-0' : 'rotate-0 scale-100 opacity-100'}`} /> + ) : ( + <Moon className={`h-5 w-5 transition-all duration-300 ${isAnimating ? 'rotate-90 scale-50 opacity-0' : 'rotate-0 scale-100 opacity-100'}`} /> + )} + </div> </Button> ); }
\ No newline at end of file |