- {/* Annonce pour les lecteurs d'écran */} -
- {currentPhase === AnimationPhase.FADING_OUT && - "Transition en cours vers la page À propos"} -
- - - - - - {currentPhase === AnimationPhase.BACKGROUND_ANIMATING && ( -
- )} -
- ); -}; - -export default HomePage; \ No newline at end of file diff --git a/src/components/HomePage/HomePageBackground.tsx b/src/components/HomePage/HomePageBackground.tsx deleted file mode 100644 index 0f8ce8c..0000000 --- a/src/components/HomePage/HomePageBackground.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { motion } from 'framer-motion'; -import { useState, useEffect } from 'react'; - -import HomePageBackgroundImage from './homepageBackground.png'; -import { HomePageBackgroundProps } from '../../types'; - -import '../../styles/HomePage.css'; - -const useImagePreloader = (src: string): boolean => { - const [isLoaded, setIsLoaded] = useState(false); - - useEffect((): (() => void) => { - const img = new Image(); - img.src = src; - img.onload = (): void => { setIsLoaded(true); }; - img.onerror = (): void => { - console.error('Erreur lors du chargement de l\'image de fond'); - setIsLoaded(true); - }; - - return (): void => { - img.onload = null; - img.onerror = null; - }; - }, [src]); - - return isLoaded; -}; - -const HomePageBackground: React.FC = ({ - animateExit -}) => { - const isImageLoaded = useImagePreloader(HomePageBackgroundImage); - const imageVariants = { - initial: { - x: '0vw', - opacity: 0 - }, - loaded: { - x: '0vw', - opacity: 1, - transition: { - opacity: { duration: 0.5 } - } - }, - exit: { - x: '-25vw', - opacity: 0, - transition: { - duration: 3, - ease: [0.11, 0, 0.5, 0] - } - } - }; - - const getAnimationState = (): string => { - if (animateExit) return 'exit'; - if (isImageLoaded) return 'loaded'; - return 'initial'; - }; - - return ( - - ); -}; - -export default HomePageBackground; \ No newline at end of file diff --git a/src/components/HomePage/HomePageFooter.tsx b/src/components/HomePage/HomePageFooter.tsx deleted file mode 100644 index e2b8c8b..0000000 --- a/src/components/HomePage/HomePageFooter.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { motion } from 'framer-motion'; - -import { HomePageFooterProps } from '../../types'; - -import '../../styles/HomePage.css'; - -const HomePageFooter: React.FC = ({ animateExit }) => { - const currentYear = new Date().getFullYear(); - - return ( - - - Le Dev Novice © {currentYear} - - - ); -}; - -export default HomePageFooter; \ No newline at end of file diff --git a/src/components/HomePage/HomePageMenu.tsx b/src/components/HomePage/HomePageMenu.tsx deleted file mode 100644 index c4b1d51..0000000 --- a/src/components/HomePage/HomePageMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Link } from '@tanstack/react-router'; -import { motion } from 'framer-motion'; - -import { HomePageMenuProps } from '../../types'; - -import '../../styles/HomePage.css'; - -const HomePageMenu: React.FC = ({ - animateExit, - onAboutClick -}) => { - const handleAboutKeyDown = (event: React.KeyboardEvent): void => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - onAboutClick(); - } - }; - - const menuVariants = { - visible: { opacity: 1 }, - hidden: { opacity: 0 } - } as const; - - return ( - - {/* A Propos */} -
- -
- - {/* Projets */} -
- - Projets - -
- - {/* Publications */} -
- - Publications - -
-
- ); -}; - -export default HomePageMenu; \ No newline at end of file diff --git a/src/components/HomePage/HomePageSocials.tsx b/src/components/HomePage/HomePageSocials.tsx deleted file mode 100644 index a90c39e..0000000 --- a/src/components/HomePage/HomePageSocials.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { motion } from 'framer-motion'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faGithub, - faLinkedin, - faTwitter, - IconDefinition, -} from '@fortawesome/free-brands-svg-icons'; - -import { HomePageSocialsProps } from '../../types'; - -import '../../styles/HomePage.css'; - -interface SocialLink { - name: string; - icon: IconDefinition; - url: string; - ariaLabel: string; -} - -const socialLinks: SocialLink[] = [ - { - name: 'GitHub', - icon: faGithub, - url: 'https://github.com/LeDevNovice', - ariaLabel: 'Visitez mon profil GitHub' - }, - { - name: 'LinkedIn', - icon: faLinkedin, - url: 'https://linkedin.com/in/gregory-saison', - ariaLabel: 'Visitez mon profil LinkedIn' - }, - { - name: 'Twitter', - icon: faTwitter, - url: 'https://x.com/ledevnovice', - ariaLabel: 'Visitez mon profil Twitter' - } -]; - -const HomePageSocials: React.FC = ({ animateExit }) => { - return ( - - {socialLinks.map((social) => ( - - - - ))} - - ); -}; - -export default HomePageSocials; \ No newline at end of file diff --git a/src/components/HomePage/HomePageTitle.tsx b/src/components/HomePage/HomePageTitle.tsx deleted file mode 100644 index d914c47..0000000 --- a/src/components/HomePage/HomePageTitle.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { motion } from 'framer-motion'; - -import { HomePageTitleProps } from '../../types'; - -import '../../styles/HomePage.css'; - -const HomePageTitle: React.FC = ({ animateExit }) => { - return ( - -
-

- GREG.GS -

-

- DEV BACK-END / DEVOPS.JS -

-
-
-
- ); -}; - -export default HomePageTitle; \ No newline at end of file diff --git a/src/components/HomePage/homepageBackground.png b/src/components/HomePage/homepageBackground.png deleted file mode 100644 index 344472f68389934314b21893337cf6460b892387..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54949 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 RYLNf{002ovPDHLkV1jwbfS3RP diff --git a/src/components/IntroOverlay/IntroDot.tsx b/src/components/IntroOverlay/IntroDot.tsx index 232eed1..2093f13 100644 --- a/src/components/IntroOverlay/IntroDot.tsx +++ b/src/components/IntroOverlay/IntroDot.tsx @@ -2,17 +2,19 @@ import { motion } from "framer-motion"; import { IntroDotProps } from '../../types'; import '../../styles/Overlay.css'; -const IntroDot: React.FC = ({ - variant, - animationState, - handleDotAnimationComplete +const IntroDot: React.FC = ({ + variant, + animationState, + handleDotAnimationComplete, }) => { const onAnimationComplete = (): void => { handleDotAnimationComplete(animationState); }; + const needsHighZ = animationState === 'expand' || animationState === 'contract'; + return ( - = ({ onAnimationComplete={onAnimationComplete} style={{ willChange: 'transform, opacity', - zIndex: animationState === 'expand' || animationState === 'contract' ? 9999 : 1, + zIndex: needsHighZ ? 9999 : 1, }} role="presentation" aria-hidden="true" diff --git a/src/components/IntroOverlay/IntroOverlay.tsx b/src/components/IntroOverlay/IntroOverlay.tsx index 329466b..e0336b7 100644 --- a/src/components/IntroOverlay/IntroOverlay.tsx +++ b/src/components/IntroOverlay/IntroOverlay.tsx @@ -4,14 +4,17 @@ import React from 'react'; import IntroTitle from './IntroTitle'; import IntroDot from './IntroDot'; import IntroEnterMessage from './IntroEnterMessage'; -import { - IntroOverlayProps, - DotAnimationState, - DotVariants +import OverlayMenu from './OverlayMenu'; +import { + IntroOverlayProps, + DotAnimationState, + DotVariants, } from '../../types'; import '../../styles/Overlay.css'; +// ─── State ────────────────────────────────────────────── + interface IntroOverlayState { dotAnimationState: DotAnimationState; showTitle: boolean; @@ -19,74 +22,89 @@ interface IntroOverlayState { hasStartedWiggle: boolean; hasOverlayBackground: boolean; isTouchDevice: boolean; - isFullyComplete: boolean; + menuOpen: boolean; + hasCompletedFirstOpen: boolean; } +// ─── Actions ──────────────────────────────────────────── + type IntroOverlayAction = | { type: 'INIT_TOUCH_DEVICE'; isTouchDevice: boolean } | { type: 'SHOW_ENTER_MESSAGE' } | { type: 'START_WIGGLE_SEQUENCE' } | { type: 'TRANSITION_TO'; nextState: DotAnimationState } | { type: 'START_EXPANSION' } - | { type: 'START_CONTRACTION' } - | { type: 'HIDE_ELEMENTS' } - | { type: 'COMPLETE_ANIMATION' }; + | { type: 'SHOW_MENU' } + | { type: 'START_CLOSE' } + | { type: 'RESET_TO_IDLE' }; + +// ─── Reducer ──────────────────────────────────────────── const introOverlayReducer = ( - state: IntroOverlayState, + state: IntroOverlayState, action: IntroOverlayAction ): IntroOverlayState => { switch (action.type) { case 'INIT_TOUCH_DEVICE': return { ...state, isTouchDevice: action.isTouchDevice }; - + case 'SHOW_ENTER_MESSAGE': return { ...state, showEnterMessage: true }; - + case 'START_WIGGLE_SEQUENCE': if (!state.hasStartedWiggle) { return { ...state, hasStartedWiggle: true, - dotAnimationState: 'wiggle1' + showEnterMessage: false, + dotAnimationState: 'wiggle1', }; } return state; - + case 'TRANSITION_TO': return { ...state, dotAnimationState: action.nextState }; - + case 'START_EXPANSION': return { ...state, - dotAnimationState: 'expand' + dotAnimationState: 'expand', + showTitle: false, + showEnterMessage: false, }; - - case 'HIDE_ELEMENTS': + + case 'SHOW_MENU': return { ...state, - showTitle: false, - hasOverlayBackground: true + menuOpen: true, + hasOverlayBackground: true, + hasCompletedFirstOpen: true, }; - - case 'START_CONTRACTION': + + case 'START_CLOSE': return { ...state, + menuOpen: false, dotAnimationState: 'contract', - hasOverlayBackground: false }; - - case 'COMPLETE_ANIMATION': + + case 'RESET_TO_IDLE': return { ...state, - isFullyComplete: true + dotAnimationState: 'idle', + showTitle: true, + showEnterMessage: true, + hasOverlayBackground: true, + hasStartedWiggle: false, }; - + default: return state; } }; +// ─── Dot Variants ─────────────────────────────────────── + const dotVariants: DotVariants = { hidden: { scale: 1, @@ -101,6 +119,14 @@ const dotVariants: DotVariants = { delay: 0.5, }, }, + idle: { + scale: 1, + x: 0, + opacity: 1, + transition: { + duration: 0.01, + }, + }, wiggle1: { x: [0, 5, -5, 5, -5, 0], opacity: 1, @@ -139,20 +165,22 @@ const dotVariants: DotVariants = { transition: { duration: 2, ease: [0.4, 0, 0.2, 1], - times: [0, 0.5, 1] + times: [0, 0.5, 1], }, }, contract: { - scale: 0, + scale: 1, opacity: 1, transition: { - duration: 2, - ease: 'easeOut', + duration: 1.5, + ease: [0.4, 0, 0.2, 1], }, }, } as const; -const IntroOverlay: React.FC = ({ onFinish }) => { +// ─── Component ────────────────────────────────────────── + +const IntroOverlay: React.FC = () => { const initialState: IntroOverlayState = { dotAnimationState: 'fadeIn', showTitle: true, @@ -160,11 +188,14 @@ const IntroOverlay: React.FC = ({ onFinish }) => { hasStartedWiggle: false, hasOverlayBackground: true, isTouchDevice: false, - isFullyComplete: false, + menuOpen: false, + hasCompletedFirstOpen: false, }; const [state, dispatch] = useReducer(introOverlayReducer, initialState); + // ── Init ── + useEffect(() => { const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; dispatch({ type: 'INIT_TOUCH_DEVICE', isTouchDevice: isTouch }); @@ -174,114 +205,130 @@ const IntroOverlay: React.FC = ({ onFinish }) => { dispatch({ type: 'SHOW_ENTER_MESSAGE' }); }, []); + // ── User click on the overlay (Greg.GS screen) ── + const handleUserInteraction = useCallback(() => { - if (!state.hasStartedWiggle) { - console.log('Starting wiggle sequence'); - dispatch({ type: 'START_WIGGLE_SEQUENCE' }); + if (state.menuOpen) return; + + if (!state.hasCompletedFirstOpen) { + // First time: run the full wiggle → expand sequence + if (!state.hasStartedWiggle) { + dispatch({ type: 'START_WIGGLE_SEQUENCE' }); + } + } else { + // Subsequent times: expand directly (skip wiggle) + dispatch({ type: 'START_EXPANSION' }); } - }, [state.hasStartedWiggle]); + }, [state.menuOpen, state.hasCompletedFirstOpen, state.hasStartedWiggle]); + + // ── Close menu (✕ button) ── + + const handleMenuClose = useCallback(() => { + dispatch({ type: 'START_CLOSE' }); + }, []); + + // ── Animation state machine ── const handleDotAnimationComplete = useCallback(( previousState: DotAnimationState ) => { - console.log(`✅ Animation terminée: ${previousState}`); - - const transitions: Record void> = { + const transitions: Partial void>> = { fadeIn: () => { - console.log('FadeIn complete - waiting for user interaction'); + // Waiting for user interaction + }, + idle: () => { + // Stable state, waiting for user interaction }, wiggle1: () => { - console.log('Wiggle1 complete -> pause'); dispatch({ type: 'TRANSITION_TO', nextState: 'pause' }); }, pause: () => { - console.log('Pause complete -> wiggle2'); setTimeout(() => { dispatch({ type: 'TRANSITION_TO', nextState: 'wiggle2' }); }, 1000); }, wiggle2: () => { - console.log('Wiggle2 complete -> secondPause'); setTimeout(() => { dispatch({ type: 'TRANSITION_TO', nextState: 'secondPause' }); }, 1000); }, secondPause: () => { - console.log('SecondPause complete -> expand'); dispatch({ type: 'START_EXPANSION' }); }, expand: () => { - console.log('🔵 EXPAND COMPLETE - Starting contraction'); - - setTimeout(() => { - console.log('🔴 Starting contraction and hiding elements'); - dispatch({ type: 'HIDE_ELEMENTS' }); - dispatch({ type: 'START_CONTRACTION' }); - }, 2500); + // Expansion complete → show the menu + dispatch({ type: 'SHOW_MENU' }); }, contract: () => { - console.log('✅ CONTRACT COMPLETE - Animation fully finished'); - dispatch({ type: 'COMPLETE_ANIMATION' }); - - setTimeout(() => { - console.log('🎯 Calling onFinish'); - onFinish?.(); - }, 500); + // Contraction complete → reset to idle (Greg.GS visible) + dispatch({ type: 'RESET_TO_IDLE' }); }, }; const transition = transitions[previousState]; - transition(); - }, [onFinish]); + if (transition) transition(); + }, []); - useEffect(() => { - console.log('Current animation state:', state.dotAnimationState); - }, [state.dotAnimationState]); + // ── Render ── const containerStyle = { backgroundColor: state.hasOverlayBackground ? '#FEFEFE' : 'transparent', transition: 'background-color 0.5s ease-out', }; + const isInteractive = !state.menuOpen + && state.dotAnimationState !== 'expand' + && state.dotAnimationState !== 'contract'; + return (
{ - if (e.key === 'Enter' || e.key === ' ') { + if (isInteractive && (e.key === 'Enter' || e.key === ' ')) { handleUserInteraction(); } }} - aria-label="Cliquez pour entrer dans le site" + aria-label="Cliquez pour ouvrir le menu" > -
+ {/* ── Greg . GS title ── */} +
{state.showTitle && ( - )} - + - + {state.showTitle && ( - )}
- - {state.showEnterMessage && state.dotAnimationState === 'fadeIn' && ( - - )} + + {/* ── Enter message ── */} + {state.showEnterMessage + && (state.dotAnimationState === 'fadeIn' || state.dotAnimationState === 'idle') + && ( + + )} + + {/* ── Fullscreen menu (visible when expanded) ── */} +
); }; diff --git a/src/components/IntroOverlay/OverlayMenu.tsx b/src/components/IntroOverlay/OverlayMenu.tsx new file mode 100644 index 0000000..88b85ca --- /dev/null +++ b/src/components/IntroOverlay/OverlayMenu.tsx @@ -0,0 +1,149 @@ +import React, { useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useNavigate } from '@tanstack/react-router'; + +import { OverlayMenuProps } from '../../types'; + +import '../../styles/Overlay.css'; + +interface MenuItem { + label: string; + path: '/about' | '/publications' | '/projects'; +} + +const menuItems: MenuItem[] = [ + { label: 'À Propos', path: '/about' }, + { label: 'Publications', path: '/publications' }, + { label: 'Projets', path: '/projects' }, +]; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + delay: 0.2, + duration: 0.4, + ease: 'easeOut', + staggerChildren: 0.1, + delayChildren: 0.3, + }, + }, + exit: { + opacity: 0, + transition: { + duration: 0.25, + ease: 'easeIn', + }, + }, +} as const; + +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: 'easeOut', + }, + }, + exit: { + opacity: 0, + y: -10, + transition: { + duration: 0.2, + }, + }, +} as const; + +const OverlayMenu: React.FC = ({ onClose, isVisible }) => { + const navigate = useNavigate(); + const closeButtonRef = useRef(null); + + // Focus trap: focus the close button when the menu opens + useEffect(() => { + if (isVisible && closeButtonRef.current) { + closeButtonRef.current.focus(); + } + }, [isVisible]); + + // Close on Escape key + useEffect(() => { + if (!isVisible) return; + + const handleKeyDown = (e: KeyboardEvent): void => { + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isVisible, onClose]); + + const handleNavigate = (path: MenuItem['path']): void => { + void navigate({ to: path }); + }; + + return ( + + {isVisible && ( + + {/* Close button */} + { + e.stopPropagation(); + onClose(); + }} + variants={itemVariants} + aria-label="Fermer le menu" + type="button" + > + ✕ + + + {/* Navigation items */} + + + )} + + ); +}; + +export default OverlayMenu; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index acafa59..a087bed 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,147 +1,7 @@ -import { useState, useEffect, useCallback } from 'react'; +import IntroOverlay from '../components/IntroOverlay/IntroOverlay'; -import IntroOverlay from './../components/IntroOverlay/IntroOverlay'; -import HomePage from '../components/HomePage/HomePage'; -import { HomeProps } from '../types'; - -interface HomeState { - showIntro: boolean; - isFirstVisit: boolean; - introCompletedAt: number | null; - isAnimationFinalizing: boolean; -} - -interface IntroState extends HomeState { - startIntroFinalization: () => void; - completeIntro: () => void; - resetIntro: () => void; -} - -const useIntroState = (): IntroState => { - const STORAGE_KEY = 'portfolio_intro_shown'; - - const [introState, setIntroState] = useState(() => { - let hasShownIntro = false; - try { - hasShownIntro = sessionStorage.getItem(STORAGE_KEY) === 'true'; - } catch (error) { - console.warn('SessionStorage not available:', error); - hasShownIntro = false; - } - - console.log('🔍 Initial intro state - hasShownIntro:', hasShownIntro); - - return { - showIntro: !hasShownIntro, - isFirstVisit: !hasShownIntro, - introCompletedAt: null, - isAnimationFinalizing: false, - }; - }); - - const startIntroFinalization = useCallback(() => { - console.log('🎬 Starting intro finalization'); - setIntroState(prev => ({ - ...prev, - isAnimationFinalizing: true, - })); - }, []); - - const completeIntro = useCallback(() => { - console.log('✅ Completing intro - unmounting overlay'); - try { - sessionStorage.setItem(STORAGE_KEY, 'true'); - console.log('💾 SessionStorage set successfully'); - } catch (error) { - console.warn('Could not set sessionStorage:', error); - } - - setIntroState(prev => ({ - ...prev, - showIntro: false, - introCompletedAt: Date.now(), - isAnimationFinalizing: false, - })); - }, []); - - const resetIntro = useCallback(() => { - console.log('🔄 Resetting intro state'); - try { - sessionStorage.removeItem(STORAGE_KEY); - } catch (error) { - console.warn('Could not remove from sessionStorage:', error); - } - - setIntroState({ - showIntro: true, - isFirstVisit: false, - introCompletedAt: null, - isAnimationFinalizing: false, - }); - }, []); - - useEffect(() => { - console.log('📊 Intro state changed:', introState); - }, [introState]); - - return { - ...introState, - startIntroFinalization, - completeIntro, - resetIntro, - }; -}; - -const Home: React.FC = () => { - const { - showIntro, - isFirstVisit, - isAnimationFinalizing, - startIntroFinalization, - completeIntro, - } = useIntroState(); - - const handleIntroFinish = useCallback(() => { - console.log('🎯 Intro finish triggered'); - startIntroFinalization(); - - setTimeout(() => { - console.log('⏰ Timeout reached - completing intro'); - completeIntro(); - }, 2000); - }, [startIntroFinalization, completeIntro]); - - useEffect(() => { - if (!showIntro && isFirstVisit && !isAnimationFinalizing) { - console.log('🎉 Intro completed for the first time'); - } - }, [showIntro, isFirstVisit, isAnimationFinalizing]); - - useEffect(() => { - console.log('🏠 Home component render - showIntro:', showIntro, 'isFirstVisit:', isFirstVisit); - }); - - return ( - <> - - {showIntro && ( -
- -
- )} - - ); +const Home: React.FC = () => { + return ; }; export default Home; \ No newline at end of file diff --git a/src/styles/HomePage.css b/src/styles/HomePage.css deleted file mode 100644 index 194e18a..0000000 --- a/src/styles/HomePage.css +++ /dev/null @@ -1,220 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@600&display=swap'); - -.homepage { - width: 100%; - height: 100vh; -} -.homepage__title { - width: 90vw; - min-height: 65vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} -.homepage__title-purple { - color: #792262; -} -.homepage__title-border { - width: 10vw; - border-bottom: 3px solid #792262; -} -.homepage__title-name { - font-family: 'Montserrat', sans-serif; - font-size: 5vw; - padding-bottom: 8px; - margin: 0; - letter-spacing: 4px; -} -.homepage__title-job { - font-family: 'Montserrat', sans-serif; - font-size: 3vw; - margin: 0; - padding-bottom: 64px; - letter-spacing: 4px; -} -.homepage__menu { - position: fixed; - display: flex; - flex-direction: column; - margin-left: 10vw; -} -.homepage__menu-item { - padding-bottom: 24px; -} -.homepage__menu-item--button { - background: none; - border: none; - cursor: pointer; - font: inherit; - text-align: left; - padding: 0; -} -.homepage__menu-item--button:focus-visible { - outline: 2px solid #792262; - outline-offset: 4px; -} -.homepage__menu-item--text { - font-family: 'Montserrat', sans-serif; - font-size: 2.5vw; - letter-spacing: 4px; - text-decoration: none; - color: inherit; -} -.homepage__menu-item--text:hover { - color: #792262; -} -.homepage__menu-item:nth-child(2) { - transform: translateX(5vw); -} -.homepage__menu-item:nth-child(3) { - transform: translateX(10vw); -} -.homepage__background { - position: fixed; - bottom: 0; - right: 0; - min-height: 100vh; - opacity: 1; -} -.homepage__socials { - position: fixed; - top: 0; - padding: 8px 0px 0px 8px; -} -.homepage__socials-icon { - color: #792262; - font-size: 2vw; - padding-right: 16px; -} -.homepage__footer { - position: absolute; - bottom: 10px; - width: 100%; - text-align: center; -} -.homepage__footer-content { - font-size: 1vw; - font-style: italic; -} -.homepage__socials-link { - display: inline-block; - transition: transform 0.2s ease, opacity 0.2s ease; - text-decoration: none; -} -.homepage__socials-link:hover { - transform: translateY(-2px); -} -.homepage__socials-link:focus-visible { - outline: 2px solid #792262; - outline-offset: 4px; - border-radius: 4px; -} -.homepage__socials-link:active { - transform: translateY(0); -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -@media only screen and (max-width: 500px) { - .homepage__title { - width: 100vw; - min-height: 40vh; - } - .homepage__title-border { - width: 20vw; - border-bottom: 3px solid #792262; - } - .homepage__menu { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin: 0; - } - .homepage__menu-item--text { - font-family: 'Montserrat', sans-serif; - font-size: 6vw; - letter-spacing: 4px; - text-decoration: none; - color: inherit; - } - .homepage__menu-item { - padding-bottom: 48px; - } - .homepage__menu-item:nth-child(2) { - transform: translateX(0); - } - .homepage__menu-item:nth-child(3) { - transform: translateX(0); - } - .homepage__background { - display: none; - } - .homepage__footer-content { - font-size: 3vw; - font-style: italic; - } - .homepage__socials-icon { - color: #792262; - font-size: 4vw; - padding-right: 16px; - } -} -@media only screen and (min-width: 501px) and (max-width: 650px) { - .homepage__title { - width: 100vw; - min-height: 60vh; - } - .homepage__background { - display: none; - } - .homepage__footer-content { - font-size: 3vw; - font-style: italic; - } - .homepage__menu-item--text { - font-family: 'Montserrat', sans-serif; - font-size: 4vw; - letter-spacing: 4px; - text-decoration: none; - color: inherit; - } -} -@media only screen and (min-width: 651px) and (max-width: 1200px) { - .homepage__title { - width: 100vw; - min-height: 60vh; - } - .homepage__background { - display: none; - } - .homepage__footer-content { - font-size: 1.5vw; - font-style: italic; - } - .homepage__menu-item--text { - font-family: 'Montserrat', sans-serif; - font-size: 2vw; - letter-spacing: 4px; - text-decoration: none; - color: inherit; - } -} \ No newline at end of file diff --git a/src/styles/Overlay.css b/src/styles/Overlay.css index f8f8672..70aacad 100644 --- a/src/styles/Overlay.css +++ b/src/styles/Overlay.css @@ -1,5 +1,7 @@ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); +/* ─── Overlay Container ─────────────────────────────────── */ + .overlay__container { position: fixed; inset: 0; @@ -16,13 +18,22 @@ transform: translateZ(0); backface-visibility: hidden; } + +.overlay__container:focus-visible { + outline: 2px solid #792262; + outline-offset: -2px; +} + +/* ─── Title (Greg . GS) ─────────────────────────────────── */ + .overlay__title-wrapper { - display: inline-block; - white-space: nowrap; + display: inline-block; + white-space: nowrap; text-align: center; position: relative; z-index: 1; } + .overlay__title { display: inline-block; font-family: 'Montserrat', sans-serif; @@ -36,9 +47,12 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +/* ─── Dot ────────────────────────────────────────────────── */ + .overlay__title-dot { - display: inline-block; - width: 2.5vw; + display: inline-block; + width: 2.5vw; height: 2.5vw; border-radius: 50%; background-color: #792262; @@ -49,10 +63,14 @@ backface-visibility: hidden; -webkit-backface-visibility: hidden; } + .overlay__title-dot[style*="scale"] { z-index: 9999 !important; transform-style: preserve-3d; } + +/* ─── Enter Message ──────────────────────────────────────── */ + .overlay__enter-message { position: absolute; top: 65vh; @@ -74,11 +92,80 @@ } } -.overlay__container:focus-visible { - outline: 2px solid #792262; - outline-offset: -2px; +/* ─── Overlay Menu (fullscreen, over expanded dot) ───────── */ + +.overlay__menu { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + pointer-events: auto; +} + +.overlay__menu-close { + position: absolute; + top: 2rem; + right: 2rem; + background: none; + border: none; + color: #FEFEFE; + font-size: 2.5rem; + cursor: pointer; + font-family: 'Montserrat', sans-serif; + font-weight: 300; + line-height: 1; + padding: 0.5rem; + transition: transform 0.2s ease, opacity 0.2s ease; + z-index: 10001; } +.overlay__menu-close:hover { + transform: scale(1.15); + opacity: 0.85; +} + +.overlay__menu-close:focus-visible { + outline: 2px solid #FEFEFE; + outline-offset: 4px; + border-radius: 2px; +} + +.overlay__menu-nav { + display: flex; + flex-direction: column; + align-items: center; + gap: 3rem; +} + +.overlay__menu-item { + background: none; + border: none; + color: #FEFEFE; + font-family: 'Montserrat', sans-serif; + font-size: 3.5vw; + font-weight: 700; + letter-spacing: 0.15em; + text-transform: uppercase; + cursor: pointer; + padding: 0.5rem 1rem; + transition: opacity 0.2s ease; +} + +.overlay__menu-item:hover { + opacity: 0.75; +} + +.overlay__menu-item:focus-visible { + outline: 2px solid #FEFEFE; + outline-offset: 6px; + border-radius: 2px; +} + +/* ─── Accessibility ──────────────────────────────────────── */ + @media (prefers-reduced-motion: reduce) { .overlay__title-dot, .overlay__title, @@ -87,60 +174,74 @@ animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } - + @keyframes pulse { 0%, 100% { opacity: 1; } } } + +/* ─── Responsive ─────────────────────────────────────────── */ + @media (max-width: 768px) { .overlay__title { font-size: 12vw; } - + .overlay__title-dot { width: 3.5vw; height: 3.5vw; } - + .overlay__enter-message { font-size: 3vw; top: 55vh; } + + .overlay__menu-item { + font-size: 7vw; + } + + .overlay__menu-nav { + gap: 2.5rem; + } + + .overlay__menu-close { + font-size: 2rem; + top: 1.5rem; + right: 1.5rem; + } } + @media (min-width: 769px) and (max-width: 1024px) { .overlay__enter-message { font-size: 2.5vw; top: 60vh; } + + .overlay__menu-item { + font-size: 5vw; + } } + @media (min-width: 1920px) { .overlay__title { font-size: 8vw; } - + .overlay__title-dot { width: 2vw; height: 2vw; max-width: 40px; max-height: 40px; } - + .overlay__enter-message { font-size: 1.2vw; } -} -.intro-overlay-wrapper { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 9999; - pointer-events: auto; -} -.intro-overlay-wrapper[style*="pointer-events: none"] { - pointer-events: none !important; + .overlay__menu-item { + font-size: 2.5vw; + } } \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 77ee6f0..5a86f30 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,13 +1,10 @@ import { Variant, Variants } from 'framer-motion'; -export interface AnimationProps { - animateExit: boolean; - duration?: number; - delay?: number; -} +// --- Dot Animation --- export type DotAnimationState = | 'fadeIn' + | 'idle' | 'wiggle1' | 'pause' | 'wiggle2' @@ -18,6 +15,7 @@ export type DotAnimationState = export interface DotVariants extends Variants { hidden: Variant; fadeIn: Variant; + idle: Variant; wiggle1: Variant; pause: Variant; wiggle2: Variant; @@ -26,35 +24,39 @@ export interface DotVariants extends Variants { contract: Variant; } -export interface HomePageMenuProps extends Pick { - onAboutClick: () => void; -} -export interface HomePageBackgroundProps { - animateExit: boolean; -} -export type HomePageTitleProps = Pick -export type HomePageFooterProps = Pick -export type HomePageSocialsProps = Pick +// --- IntroOverlay --- + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IntroOverlayProps {} -export interface IntroOverlayProps { - onFinish?: () => void; -} export interface IntroTitleProps { text: string; handleTitleAnimationComplete: () => void; } + export interface IntroDotProps { variant: DotVariants; animationState: DotAnimationState; handleDotAnimationComplete: (animationName: DotAnimationState) => void; } + export interface IntroEnterMessageProps { isTouchDevice: boolean; } -export type HomeProps = Record; +// --- Overlay Menu --- + +export interface OverlayMenuProps { + onClose: () => void; + isVisible: boolean; +} + +// --- Pages --- + export type AboutProps = Record; +// --- Utilities --- + export type NavigationHandler = () => void; export type AnimationCallback = () => void; export type ComponentProps = T extends React.FC ? P : never; \ No newline at end of file From e6680a4c194b21d2a3ae3a017577965c1fa17621 Mon Sep 17 00:00:00 2001 From: LeDevNovice Date: Wed, 18 Feb 2026 13:31:25 +0100 Subject: [PATCH 02/11] fix: intro animation error --- src/components/IntroOverlay/IntroOverlay.tsx | 60 ++++++++++---------- src/components/IntroOverlay/IntroTitle.tsx | 37 +++++++----- src/types/index.ts | 2 + 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/src/components/IntroOverlay/IntroOverlay.tsx b/src/components/IntroOverlay/IntroOverlay.tsx index e0336b7..342c5fa 100644 --- a/src/components/IntroOverlay/IntroOverlay.tsx +++ b/src/components/IntroOverlay/IntroOverlay.tsx @@ -17,7 +17,7 @@ import '../../styles/Overlay.css'; interface IntroOverlayState { dotAnimationState: DotAnimationState; - showTitle: boolean; + titlesVisible: boolean; showEnterMessage: boolean; hasStartedWiggle: boolean; hasOverlayBackground: boolean; @@ -69,7 +69,9 @@ const introOverlayReducer = ( return { ...state, dotAnimationState: 'expand', - showTitle: false, + // Titles stay in DOM (opacity → 0 via prop) so the dot + // expands from its real position between "Greg" and "GS" + titlesVisible: false, showEnterMessage: false, }; @@ -92,9 +94,10 @@ const introOverlayReducer = ( return { ...state, dotAnimationState: 'idle', - showTitle: true, + titlesVisible: true, showEnterMessage: true, hasOverlayBackground: true, + // Reset so wiggles can fire again on next click hasStartedWiggle: false, }; @@ -183,7 +186,7 @@ const dotVariants: DotVariants = { const IntroOverlay: React.FC = () => { const initialState: IntroOverlayState = { dotAnimationState: 'fadeIn', - showTitle: true, + titlesVisible: true, showEnterMessage: false, hasStartedWiggle: false, hasOverlayBackground: true, @@ -210,16 +213,11 @@ const IntroOverlay: React.FC = () => { const handleUserInteraction = useCallback(() => { if (state.menuOpen) return; - if (!state.hasCompletedFirstOpen) { - // First time: run the full wiggle → expand sequence - if (!state.hasStartedWiggle) { - dispatch({ type: 'START_WIGGLE_SEQUENCE' }); - } - } else { - // Subsequent times: expand directly (skip wiggle) - dispatch({ type: 'START_EXPANSION' }); + // Always run wiggle sequence (fires on every open) + if (!state.hasStartedWiggle) { + dispatch({ type: 'START_WIGGLE_SEQUENCE' }); } - }, [state.menuOpen, state.hasCompletedFirstOpen, state.hasStartedWiggle]); + }, [state.menuOpen, state.hasStartedWiggle]); // ── Close menu (✕ button) ── @@ -256,11 +254,9 @@ const IntroOverlay: React.FC = () => { dispatch({ type: 'START_EXPANSION' }); }, expand: () => { - // Expansion complete → show the menu dispatch({ type: 'SHOW_MENU' }); }, contract: () => { - // Contraction complete → reset to idle (Greg.GS visible) dispatch({ type: 'RESET_TO_IDLE' }); }, }; @@ -278,7 +274,11 @@ const IntroOverlay: React.FC = () => { const isInteractive = !state.menuOpen && state.dotAnimationState !== 'expand' - && state.dotAnimationState !== 'contract'; + && state.dotAnimationState !== 'contract' + && state.dotAnimationState !== 'wiggle1' + && state.dotAnimationState !== 'wiggle2' + && state.dotAnimationState !== 'pause' + && state.dotAnimationState !== 'secondPause'; return (
= () => { }} aria-label="Cliquez pour ouvrir le menu" > - {/* ── Greg . GS title ── */} + {/* ── Greg . GS ── + Titles are ALWAYS in the DOM so the dot stays in position + during expand/contract. Visibility is controlled via opacity. */}
- {state.showTitle && ( - - )} + = () => { handleDotAnimationComplete={handleDotAnimationComplete} /> - {state.showTitle && ( - - )} +
{/* ── Enter message ── */} diff --git a/src/components/IntroOverlay/IntroTitle.tsx b/src/components/IntroOverlay/IntroTitle.tsx index 1528e29..00bace8 100644 --- a/src/components/IntroOverlay/IntroTitle.tsx +++ b/src/components/IntroOverlay/IntroTitle.tsx @@ -4,27 +4,34 @@ import { IntroTitleProps } from '../../types'; import '../../styles/Overlay.css'; -const IntroTitle: React.FC = ({ - text, - handleTitleAnimationComplete +const IntroTitle: React.FC = ({ + text, + visible, + skipAnimation, + handleTitleAnimationComplete, }) => { - const animationConfig = { - initial: { opacity: 0 }, - animate: { opacity: 1 }, - transition: { - duration: 5, - delay: 1, - ease: "linear" as const + const getTransition = () => { + // Fading out during expansion → quick fade + if (!visible) { + return { duration: 0.3, ease: 'easeOut' as const }; } + // Reappearing after menu close → instant + if (skipAnimation) { + return { duration: 0 }; + } + // First load → slow cinematic fade-in + return { duration: 5, delay: 1, ease: 'linear' as const }; }; return ( - { + if (visible) handleTitleAnimationComplete(); + }} role="heading" aria-level={1} aria-label={`Partie du nom: ${text}`} diff --git a/src/types/index.ts b/src/types/index.ts index 5a86f30..25799c3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,6 +31,8 @@ export interface IntroOverlayProps {} export interface IntroTitleProps { text: string; + visible: boolean; + skipAnimation: boolean; handleTitleAnimationComplete: () => void; } From cb852e832a805cbbfd2ef01c85e16bcc440a80d5 Mon Sep 17 00:00:00 2001 From: LeDevNovice Date: Wed, 18 Feb 2026 13:51:14 +0100 Subject: [PATCH 03/11] fix: remove unecessary comments --- src/components/IntroOverlay/IntroDot.tsx | 1 + .../IntroOverlay/IntroEnterMessage.tsx | 15 +- src/components/IntroOverlay/IntroOverlay.tsx | 41 +---- src/components/IntroOverlay/IntroTitle.tsx | 4 - src/components/IntroOverlay/OverlayMenu.tsx | 9 +- src/styles/About.css | 174 +++++++++++++++--- src/styles/Overlay.css | 14 -- src/types/index.ts | 10 - 8 files changed, 160 insertions(+), 108 deletions(-) diff --git a/src/components/IntroOverlay/IntroDot.tsx b/src/components/IntroOverlay/IntroDot.tsx index 2093f13..f63ecba 100644 --- a/src/components/IntroOverlay/IntroDot.tsx +++ b/src/components/IntroOverlay/IntroDot.tsx @@ -1,4 +1,5 @@ import { motion } from "framer-motion"; + import { IntroDotProps } from '../../types'; import '../../styles/Overlay.css'; diff --git a/src/components/IntroOverlay/IntroEnterMessage.tsx b/src/components/IntroOverlay/IntroEnterMessage.tsx index 817066a..caab74e 100644 --- a/src/components/IntroOverlay/IntroEnterMessage.tsx +++ b/src/components/IntroOverlay/IntroEnterMessage.tsx @@ -1,23 +1,22 @@ import { motion, Variants } from "framer-motion"; import { IntroEnterMessageProps } from '../../types'; - import '../../styles/Overlay.css'; const messageVariants: Variants = { hidden: { opacity: 0 }, - visible: { + visible: { opacity: [0, 1, 0], - transition: { - duration: 2, - repeat: Infinity, - ease: 'easeInOut' + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut' } } }; -const IntroEnterMessage: React.FC = ({ - isTouchDevice +const IntroEnterMessage: React.FC = ({ + isTouchDevice }) => { const message = isTouchDevice ? "Touch the screen to enter" diff --git a/src/components/IntroOverlay/IntroOverlay.tsx b/src/components/IntroOverlay/IntroOverlay.tsx index 342c5fa..3be9ef5 100644 --- a/src/components/IntroOverlay/IntroOverlay.tsx +++ b/src/components/IntroOverlay/IntroOverlay.tsx @@ -10,11 +10,7 @@ import { DotAnimationState, DotVariants, } from '../../types'; - import '../../styles/Overlay.css'; - -// ─── State ────────────────────────────────────────────── - interface IntroOverlayState { dotAnimationState: DotAnimationState; titlesVisible: boolean; @@ -26,8 +22,6 @@ interface IntroOverlayState { hasCompletedFirstOpen: boolean; } -// ─── Actions ──────────────────────────────────────────── - type IntroOverlayAction = | { type: 'INIT_TOUCH_DEVICE'; isTouchDevice: boolean } | { type: 'SHOW_ENTER_MESSAGE' } @@ -38,8 +32,6 @@ type IntroOverlayAction = | { type: 'START_CLOSE' } | { type: 'RESET_TO_IDLE' }; -// ─── Reducer ──────────────────────────────────────────── - const introOverlayReducer = ( state: IntroOverlayState, action: IntroOverlayAction @@ -69,8 +61,6 @@ const introOverlayReducer = ( return { ...state, dotAnimationState: 'expand', - // Titles stay in DOM (opacity → 0 via prop) so the dot - // expands from its real position between "Greg" and "GS" titlesVisible: false, showEnterMessage: false, }; @@ -97,7 +87,6 @@ const introOverlayReducer = ( titlesVisible: true, showEnterMessage: true, hasOverlayBackground: true, - // Reset so wiggles can fire again on next click hasStartedWiggle: false, }; @@ -106,8 +95,6 @@ const introOverlayReducer = ( } }; -// ─── Dot Variants ─────────────────────────────────────── - const dotVariants: DotVariants = { hidden: { scale: 1, @@ -181,8 +168,6 @@ const dotVariants: DotVariants = { }, } as const; -// ─── Component ────────────────────────────────────────── - const IntroOverlay: React.FC = () => { const initialState: IntroOverlayState = { dotAnimationState: 'fadeIn', @@ -197,8 +182,6 @@ const IntroOverlay: React.FC = () => { const [state, dispatch] = useReducer(introOverlayReducer, initialState); - // ── Init ── - useEffect(() => { const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; dispatch({ type: 'INIT_TOUCH_DEVICE', isTouchDevice: isTouch }); @@ -208,35 +191,26 @@ const IntroOverlay: React.FC = () => { dispatch({ type: 'SHOW_ENTER_MESSAGE' }); }, []); - // ── User click on the overlay (Greg.GS screen) ── - const handleUserInteraction = useCallback(() => { if (state.menuOpen) return; - // Always run wiggle sequence (fires on every open) if (!state.hasStartedWiggle) { dispatch({ type: 'START_WIGGLE_SEQUENCE' }); } }, [state.menuOpen, state.hasStartedWiggle]); - // ── Close menu (✕ button) ── - const handleMenuClose = useCallback(() => { dispatch({ type: 'START_CLOSE' }); }, []); - // ── Animation state machine ── - const handleDotAnimationComplete = useCallback(( previousState: DotAnimationState ) => { const transitions: Partial void>> = { - fadeIn: () => { - // Waiting for user interaction - }, - idle: () => { - // Stable state, waiting for user interaction - }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + fadeIn: () => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + idle: () => { }, wiggle1: () => { dispatch({ type: 'TRANSITION_TO', nextState: 'pause' }); }, @@ -265,8 +239,6 @@ const IntroOverlay: React.FC = () => { if (transition) transition(); }, []); - // ── Render ── - const containerStyle = { backgroundColor: state.hasOverlayBackground ? '#FEFEFE' : 'transparent', transition: 'background-color 0.5s ease-out', @@ -294,9 +266,6 @@ const IntroOverlay: React.FC = () => { }} aria-label="Cliquez pour ouvrir le menu" > - {/* ── Greg . GS ── - Titles are ALWAYS in the DOM so the dot stays in position - during expand/contract. Visibility is controlled via opacity. */}
= () => { />
- {/* ── Enter message ── */} {state.showEnterMessage && (state.dotAnimationState === 'fadeIn' || state.dotAnimationState === 'idle') && ( )} - {/* ── Fullscreen menu (visible when expanded) ── */} = ({ @@ -11,15 +10,12 @@ const IntroTitle: React.FC = ({ handleTitleAnimationComplete, }) => { const getTransition = () => { - // Fading out during expansion → quick fade if (!visible) { return { duration: 0.3, ease: 'easeOut' as const }; } - // Reappearing after menu close → instant if (skipAnimation) { return { duration: 0 }; } - // First load → slow cinematic fade-in return { duration: 5, delay: 1, ease: 'linear' as const }; }; diff --git a/src/components/IntroOverlay/OverlayMenu.tsx b/src/components/IntroOverlay/OverlayMenu.tsx index 88b85ca..a6400bc 100644 --- a/src/components/IntroOverlay/OverlayMenu.tsx +++ b/src/components/IntroOverlay/OverlayMenu.tsx @@ -3,7 +3,6 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useNavigate } from '@tanstack/react-router'; import { OverlayMenuProps } from '../../types'; - import '../../styles/Overlay.css'; interface MenuItem { @@ -12,9 +11,9 @@ interface MenuItem { } const menuItems: MenuItem[] = [ - { label: 'À Propos', path: '/about' }, + { label: 'About', path: '/about' }, { label: 'Publications', path: '/publications' }, - { label: 'Projets', path: '/projects' }, + { label: 'Projects', path: '/projects' }, ]; const containerVariants = { @@ -61,14 +60,12 @@ const OverlayMenu: React.FC = ({ onClose, isVisible }) => { const navigate = useNavigate(); const closeButtonRef = useRef(null); - // Focus trap: focus the close button when the menu opens useEffect(() => { if (isVisible && closeButtonRef.current) { closeButtonRef.current.focus(); } }, [isVisible]); - // Close on Escape key useEffect(() => { if (!isVisible) return; @@ -101,7 +98,6 @@ const OverlayMenu: React.FC = ({ onClose, isVisible }) => { aria-modal="true" aria-label="Menu de navigation" > - {/* Close button */} = ({ onClose, isVisible }) => { ✕ - {/* Navigation items */}
); diff --git a/src/components/IntroOverlay/OverlayMenu.tsx b/src/components/IntroOverlay/OverlayMenu.tsx index a6400bc..427472a 100644 --- a/src/components/IntroOverlay/OverlayMenu.tsx +++ b/src/components/IntroOverlay/OverlayMenu.tsx @@ -1,21 +1,60 @@ import React, { useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { useNavigate } from '@tanstack/react-router'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faGithub, + faLinkedin, + faTwitter, +} from '@fortawesome/free-brands-svg-icons'; +import type { IconDefinition } from '@fortawesome/free-brands-svg-icons'; + +import { OverlayMenuProps, SectionId } from '../../types'; -import { OverlayMenuProps } from '../../types'; import '../../styles/Overlay.css'; +// ─── Data ─────────────────────────────────────────────── + interface MenuItem { label: string; - path: '/about' | '/publications' | '/projects'; + section: SectionId; +} + +interface SocialLink { + name: string; + icon: IconDefinition; + url: string; + ariaLabel: string; } const menuItems: MenuItem[] = [ - { label: 'About', path: '/about' }, - { label: 'Publications', path: '/publications' }, - { label: 'Projects', path: '/projects' }, + { label: 'À Propos', section: 'about' }, + { label: 'Publications', section: 'publications' }, + { label: 'Projets', section: 'projects' }, ]; +const socialLinks: SocialLink[] = [ + { + name: 'GitHub', + icon: faGithub, + url: 'https://github.com/LeDevNovice', + ariaLabel: 'Visitez mon profil GitHub', + }, + { + name: 'LinkedIn', + icon: faLinkedin, + url: 'https://linkedin.com/in/gregory-saison', + ariaLabel: 'Visitez mon profil LinkedIn', + }, + { + name: 'Twitter', + icon: faTwitter, + url: 'https://x.com/ledevnovice', + ariaLabel: 'Visitez mon profil Twitter', + }, +]; + +// ─── Variants ─────────────────────────────────────────── + const containerVariants = { hidden: { opacity: 0 }, visible: { @@ -56,16 +95,42 @@ const itemVariants = { }, } as const; -const OverlayMenu: React.FC = ({ onClose, isVisible }) => { - const navigate = useNavigate(); +const bottomVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.5, + ease: 'easeOut', + delay: 0.6, + }, + }, + exit: { + opacity: 0, + transition: { + duration: 0.15, + }, + }, +} as const; + +// ─── Component ────────────────────────────────────────── + +const OverlayMenu: React.FC = ({ + onClose, + onNavigate, + isVisible, +}) => { const closeButtonRef = useRef(null); + const currentYear = new Date().getFullYear(); + // Focus the close button when the menu opens useEffect(() => { if (isVisible && closeButtonRef.current) { closeButtonRef.current.focus(); } }, [isVisible]); + // Close on Escape key useEffect(() => { if (!isVisible) return; @@ -77,12 +142,18 @@ const OverlayMenu: React.FC = ({ onClose, isVisible }) => { }; window.addEventListener('keydown', handleKeyDown); - // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type - return () => window.removeEventListener('keydown', handleKeyDown); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [isVisible, onClose]); - const handleNavigate = (path: MenuItem['path']): void => { - void navigate({ to: path }); + const handleItemClick = (e: React.MouseEvent, section: SectionId): void => { + e.stopPropagation(); + onNavigate(section); + }; + + const handleCloseClick = (e: React.MouseEvent): void => { + e.stopPropagation(); + onClose(); }; return ( @@ -98,13 +169,11 @@ const OverlayMenu: React.FC = ({ onClose, isVisible }) => { aria-modal="true" aria-label="Menu de navigation" > + {/* ── Close button ── */} { - e.stopPropagation(); - onClose(); - }} + onClick={handleCloseClick} variants={itemVariants} aria-label="Fermer le menu" type="button" @@ -112,6 +181,7 @@ const OverlayMenu: React.FC = ({ onClose, isVisible }) => { ✕ + {/* ── Navigation items ── */} + + {/* ── Bottom section: socials + footer ── */} + +
+ {socialLinks.map((social) => ( + + + + ))} +
+ + + Le Dev Novice © {currentYear} + +
)} diff --git a/src/components/Panel/ContentPanel.tsx b/src/components/Panel/ContentPanel.tsx new file mode 100644 index 0000000..f3b088f --- /dev/null +++ b/src/components/Panel/ContentPanel.tsx @@ -0,0 +1,291 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +import PanelDot from './PanelDot'; +import { ContentPanelProps, SectionId } from '../../types'; + +import '../../styles/Panel.css'; + +// ─── Section titles ───────────────────────────────────── + +const sectionTitles: Record = { + about: 'À Propos', + publications: 'Publications', + projects: 'Projets', +}; + +// ─── Internal phase machine ───────────────────────────── + +type PanelPhase = + | 'idle' + | 'morphing-in' + | 'color-shift-in' + | 'open' + | 'closing-content' + | 'color-shift-out' + | 'morphing-out'; + +// ─── Animation config ─────────────────────────────────── + +const TIMING = { + MORPH_IN_DELAY: 300, // wait for menu exit + MORPH_DURATION: 800, // violet contracts to panel + COLOR_SHIFT: 500, // violet → white + CONTENT_FADE_IN: 400, // content appears + CONTENT_FADE_OUT: 300, // content disappears + COLOR_SHIFT_OUT: 500, // white → violet + MORPH_OUT_DURATION: 800, // panel expands to fullscreen +} as const; + +const morphEasing = [0.4, 0, 0.2, 1]; + +// ─── Content variants ─────────────────────────────────── + +const contentVariants = { + hidden: { + opacity: 0, + y: 15, + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: TIMING.CONTENT_FADE_IN / 1000, + ease: 'easeOut', + }, + }, + exit: { + opacity: 0, + y: -10, + transition: { + duration: TIMING.CONTENT_FADE_OUT / 1000, + ease: 'easeIn', + }, + }, +} as const; + +const dotVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: 0.3, delay: 0.1 }, + }, + exit: { + opacity: 0, + transition: { duration: 0.2 }, + }, +} as const; + +// ─── Component ────────────────────────────────────────── + +const ContentPanel: React.FC = ({ + isVisible, + section, + onClosed, +}) => { + const [phase, setPhase] = useState('idle'); + const [displaySection, setDisplaySection] = useState(null); + + // ── Open sequence ── + + useEffect(() => { + if (isVisible && section && phase === 'idle') { + setDisplaySection(section); + + // Delay before starting morph (let menu exit first) + const timer = setTimeout(() => { + setPhase('morphing-in'); + }, TIMING.MORPH_IN_DELAY); + + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type + return () => clearTimeout(timer); + } + }, [isVisible, section, phase]); + + // ── Phase transitions after morph-in ── + + useEffect(() => { + if (phase === 'color-shift-in') { + const timer = setTimeout(() => { + setPhase('open'); + }, TIMING.COLOR_SHIFT); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type + return () => clearTimeout(timer); + } + }, [phase]); + + // ── Phase transitions during close ── + + useEffect(() => { + if (phase === 'closing-content') { + const timer = setTimeout(() => { + setPhase('color-shift-out'); + }, TIMING.CONTENT_FADE_OUT + 100); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type + return () => clearTimeout(timer); + } + }, [phase]); + + useEffect(() => { + if (phase === 'color-shift-out') { + const timer = setTimeout(() => { + setPhase('morphing-out'); + }, TIMING.COLOR_SHIFT_OUT); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/explicit-function-return-type + return () => clearTimeout(timer); + } + }, [phase]); + + // ── Close handler (from PanelDot) ── + + const handleClose = useCallback(() => { + if (phase !== 'open') return; + setPhase('closing-content'); + }, [phase]); + + // ── Morph animation complete ── + + const handleMorphComplete = useCallback(() => { + if (phase === 'morphing-in') { + setPhase('color-shift-in'); + } else if (phase === 'morphing-out') { + setPhase('idle'); + setDisplaySection(null); + onClosed(); + } + }, [phase, onClosed]); + + // ── Don't render if idle ── + + if (phase === 'idle' && !isVisible) return null; + + // ── Compute animation targets ── + + const isExpanded = phase === 'morphing-out'; + const isWhite = + phase === 'color-shift-in' || + phase === 'open' || + phase === 'closing-content'; + const showContent = phase === 'open'; + const showDot = + phase === 'color-shift-in' || + phase === 'open' || + phase === 'closing-content'; + + return ( + + {/* ── PanelDot ── */} + + {showDot && ( + + + + )} + + + {/* ── Section content ── */} + + {showContent && displaySection && ( + + {/* ── Header ── */} +
+

+ {sectionTitles[displaySection]} +

+
+
+ + {/* ── Content area (placeholder — real content in phases 6-8) ── */} +
+ +
+ + {/* ── Footer ── */} +
+ + Le Dev Novice © {new Date().getFullYear()} + +
+
+ )} +
+
+ ); +}; + +// ─── Placeholder content (temporary) ──────────────────── + +const SectionPlaceholder: React.FC<{ section: SectionId }> = ({ section }) => { + const messages: Record = { + about: { + title: 'Biographie, succès & compétences', + description: + 'Le contenu complet de cette section sera intégré prochainement avec les tabs Biographie, Succès et Compétences.', + }, + publications: { + title: 'En préparation...', + description: + 'Les premières publications arrivent bientôt. Articles techniques, retours d\'expérience et vulgarisation.', + }, + projects: { + title: 'En construction...', + description: + 'Les projets seront présentés ici très bientôt. Contributions open-source, projets personnels et professionnels.', + }, + }; + + const { title, description } = messages[section]; + + return ( +
+
+

{title}

+

{description}

+
+ ); +}; + +export default ContentPanel; \ No newline at end of file diff --git a/src/components/Panel/PanelDot.tsx b/src/components/Panel/PanelDot.tsx new file mode 100644 index 0000000..4580f37 --- /dev/null +++ b/src/components/Panel/PanelDot.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; + +import { PanelDotProps } from '../../types'; + +import '../../styles/Panel.css'; + +const PanelDot: React.FC = ({ onClick }) => { + const [isHovered, setIsHovered] = useState(false); + + return ( +
setIsHovered(true)} + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + onMouseLeave={() => setIsHovered(false)} + > + + + {/* ── Tooltip ── */} + +
+ ); +}; + +export default PanelDot; \ No newline at end of file diff --git a/src/styles/Overlay.css b/src/styles/Overlay.css index ed1c4a1..8cdfc2a 100644 --- a/src/styles/Overlay.css +++ b/src/styles/Overlay.css @@ -1,5 +1,7 @@ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); +/* ─── Overlay Container ─────────────────────────────────── */ + .overlay__container { position: fixed; inset: 0; @@ -22,6 +24,8 @@ outline-offset: -2px; } +/* ─── Title (Greg . GS) ─────────────────────────────────── */ + .overlay__title-wrapper { display: inline-block; white-space: nowrap; @@ -44,6 +48,8 @@ -moz-osx-font-smoothing: grayscale; } +/* ─── Dot ────────────────────────────────────────────────── */ + .overlay__title-dot { display: inline-block; width: 2.5vw; @@ -63,6 +69,8 @@ transform-style: preserve-3d; } +/* ─── Enter Message ──────────────────────────────────────── */ + .overlay__enter-message { position: absolute; top: 65vh; @@ -76,14 +84,19 @@ } @keyframes pulse { - 0%, 100% { + + 0%, + 100% { opacity: 0; } + 50% { opacity: 1; } } +/* ─── Overlay Menu (fullscreen, over expanded dot) ───────── */ + .overlay__menu { position: fixed; inset: 0; @@ -95,6 +108,8 @@ pointer-events: auto; } +/* ── Close button ── */ + .overlay__menu-close { position: absolute; top: 2rem; @@ -123,6 +138,8 @@ border-radius: 2px; } +/* ── Nav items ── */ + .overlay__menu-nav { display: flex; flex-direction: column; @@ -154,7 +171,68 @@ border-radius: 2px; } +/* ── Bottom section (socials + footer) ── */ + +.overlay__menu-bottom { + position: absolute; + bottom: 0; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding-bottom: 2rem; +} + +/* ── Socials ── */ + +.overlay__menu-socials { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.overlay__menu-social-link { + display: inline-flex; + align-items: center; + justify-content: center; + color: #FEFEFE; + font-size: 1.5rem; + text-decoration: none; + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.overlay__menu-social-link:hover { + transform: translateY(-3px); + opacity: 0.8; +} + +.overlay__menu-social-link:focus-visible { + outline: 2px solid #FEFEFE; + outline-offset: 4px; + border-radius: 4px; +} + +.overlay__menu-social-link:active { + transform: translateY(0); +} + +/* ── Footer ── */ + +.overlay__menu-footer { + font-family: 'Montserrat', sans-serif; + font-size: 0.85rem; + font-style: italic; + color: #FEFEFE; + opacity: 0.5; + user-select: none; +} + +/* ─── Accessibility ──────────────────────────────────────── */ + @media (prefers-reduced-motion: reduce) { + .overlay__title-dot, .overlay__title, .overlay__enter-message { @@ -164,12 +242,16 @@ } @keyframes pulse { - 0%, 100% { + + 0%, + 100% { opacity: 1; } } } +/* ─── Responsive ─────────────────────────────────────────── */ + @media (max-width: 768px) { .overlay__title { font-size: 12vw; @@ -198,6 +280,23 @@ top: 1.5rem; right: 1.5rem; } + + .overlay__menu-social-link { + font-size: 1.3rem; + } + + .overlay__menu-socials { + gap: 1.8rem; + } + + .overlay__menu-bottom { + padding-bottom: 1.5rem; + gap: 0.8rem; + } + + .overlay__menu-footer { + font-size: 0.75rem; + } } @media (min-width: 769px) and (max-width: 1024px) { @@ -230,4 +329,8 @@ .overlay__menu-item { font-size: 2.5vw; } + + .overlay__menu-social-link { + font-size: 1.3rem; + } } \ No newline at end of file diff --git a/src/styles/Panel.css b/src/styles/Panel.css new file mode 100644 index 0000000..b8eac0b --- /dev/null +++ b/src/styles/Panel.css @@ -0,0 +1,298 @@ +/* ─── Content Panel ──────────────────────────────────────── */ + +.content-panel { + position: fixed; + z-index: 10000; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 1200px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + box-sizing: border-box; + + /* Default state: violet */ + background-color: #792262; + transition: background-color 0.5s ease-out; +} + +.content-panel[data-bg='white'] { + background-color: #FFFFFF; + box-shadow: 0 8px 60px rgba(121, 34, 98, 0.08); +} + +.content-panel[data-bg='purple'] { + background-color: #792262; + box-shadow: none; +} + +/* ─── Dot area (top-left of panel) ───────────────────────── */ + +.content-panel__dot-area { + position: absolute; + top: 1.5rem; + left: 1.5rem; + z-index: 10; +} + +/* ─── Panel body (content wrapper) ───────────────────────── */ + +.content-panel__body { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 3rem 4rem; + overflow: hidden; +} + +/* ─── Header ─────────────────────────────────────────────── */ + +.content-panel__header { + text-align: center; + margin-bottom: 2.5rem; + padding-top: 1rem; + flex-shrink: 0; +} + +.content-panel__title { + font-family: 'Montserrat', sans-serif; + font-size: 2.5rem; + font-weight: 700; + letter-spacing: 0.25em; + text-transform: uppercase; + color: #000000; + margin: 0 0 1rem 0; + line-height: 1; +} + +.content-panel__title-bar { + width: 8vw; + max-width: 100px; + height: 3px; + background-color: #792262; + margin: 0 auto; + border-radius: 2px; +} + +/* ─── Content area ───────────────────────────────────────── */ + +.content-panel__content { + flex: 1; + overflow-y: auto; + min-height: 0; + + /* Scrollbar */ + scrollbar-width: thin; + scrollbar-color: #792262 transparent; +} + +.content-panel__content::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.content-panel__content::-webkit-scrollbar-track { + background: transparent; +} + +.content-panel__content::-webkit-scrollbar-thumb { + background-color: #792262; + border-radius: 999px; + border: 2px solid transparent; + background-clip: padding-box; + min-height: 20px; +} + +/* ─── Footer ─────────────────────────────────────────────── */ + +.content-panel__footer { + text-align: center; + padding-top: 1.5rem; + flex-shrink: 0; +} + +.content-panel__footer-text { + font-family: 'Montserrat', sans-serif; + font-size: 0.85rem; + font-style: italic; + color: #888888; +} + +/* ─── Placeholder content ────────────────────────────────── */ + +.content-panel__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + flex: 1; +} + +.content-panel__placeholder-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #792262; + margin-bottom: 2rem; + animation: panel-placeholder-pulse 3s ease-in-out infinite; +} + +@keyframes panel-placeholder-pulse { + + 0%, + 100% { + transform: scale(1); + opacity: 0.6; + } + + 50% { + transform: scale(1.2); + opacity: 1; + } +} + +.content-panel__placeholder-title { + font-family: 'Montserrat', sans-serif; + font-size: 1.3rem; + font-weight: 600; + color: #792262; + margin: 0 0 1rem 0; + letter-spacing: 0.05em; +} + +.content-panel__placeholder-text { + font-family: 'Montserrat', sans-serif; + font-size: 0.95rem; + color: #888888; + line-height: 1.7; + max-width: 400px; + margin: 0; +} + +/* ─── PanelDot ───────────────────────────────────────────── */ + +.panel-dot__wrapper { + position: relative; + display: inline-flex; + align-items: center; +} + +.panel-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #792262; + border: none; + padding: 0; + cursor: pointer; + outline: none; + display: block; +} + +.panel-dot:focus-visible { + outline: 2px solid #792262; + outline-offset: 4px; + border-radius: 50%; +} + +/* ── Tooltip ── */ + +.panel-dot__tooltip { + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + white-space: nowrap; + + font-family: 'Montserrat', sans-serif; + font-size: 0.75rem; + font-weight: 400; + color: #792262; + letter-spacing: 0.03em; + + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease 0.5s; + /* 500ms delay before appearing */ +} + +.panel-dot__tooltip--visible { + opacity: 1; +} + +/* ─── Responsive ─────────────────────────────────────────── */ + +@media (max-width: 768px) { + .content-panel__body { + padding: 2rem 1.5rem; + } + + .content-panel__header { + margin-bottom: 1.5rem; + } + + .content-panel__title { + font-size: 1.8rem; + letter-spacing: 0.12em; + } + + .content-panel__title-bar { + width: 15vw; + } + + .content-panel__dot-area { + top: 1rem; + left: 1rem; + } + + .content-panel__placeholder-title { + font-size: 1.1rem; + } + + .content-panel__placeholder-text { + font-size: 0.85rem; + } + + .content-panel__footer-text { + font-size: 0.75rem; + } + + /* Tooltip goes below on mobile */ + .panel-dot__tooltip { + left: 50%; + top: calc(100% + 8px); + transform: translateX(-50%); + } +} + +@media (min-width: 769px) and (max-width: 1024px) { + .content-panel__body { + padding: 2.5rem 3rem; + } + + .content-panel__title { + font-size: 2.2rem; + } +} + +@media (min-width: 1920px) { + .content-panel__title { + font-size: 2.8rem; + } + + .content-panel__placeholder-text { + font-size: 1rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .content-panel__placeholder-dot { + animation: none; + } +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 66bfce2..13db516 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,11 @@ import { Variant, Variants } from 'framer-motion'; +// --- Sections --- + +export type SectionId = 'about' | 'publications' | 'projects'; + +// --- Dot Animation --- + export type DotAnimationState = | 'fadeIn' | 'idle' @@ -22,6 +28,8 @@ export interface DotVariants extends Variants { contract: Variant; } +// --- IntroOverlay --- + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface IntroOverlayProps {} @@ -42,13 +50,32 @@ export interface IntroEnterMessageProps { isTouchDevice: boolean; } +// --- Overlay Menu --- + export interface OverlayMenuProps { onClose: () => void; + onNavigate: (section: SectionId) => void; isVisible: boolean; } +// --- Content Panel --- + +export interface ContentPanelProps { + isVisible: boolean; + section: SectionId | null; + onClosed: () => void; +} + +export interface PanelDotProps { + onClick: () => void; +} + +// --- Pages --- + export type AboutProps = Record; +// --- Utilities --- + export type NavigationHandler = () => void; export type AnimationCallback = () => void; export type ComponentProps = T extends React.FC ? P : never; \ No newline at end of file From db98bd3d24e3064924046d6e9c47adb691cd42b6 Mon Sep 17 00:00:00 2001 From: LeDevNovice Date: Thu, 19 Feb 2026 13:06:51 +0100 Subject: [PATCH 05/11] feat: implement animation for transition in pages --- src/components/IntroOverlay/IntroDot.tsx | 6 +- src/components/IntroOverlay/IntroOverlay.tsx | 26 ++++- src/components/Panel/ContentPanel.tsx | 112 +++++++++++-------- src/components/Panel/PanelDot.tsx | 12 +- src/styles/Panel.css | 7 +- src/types/index.ts | 2 + 6 files changed, 97 insertions(+), 68 deletions(-) diff --git a/src/components/IntroOverlay/IntroDot.tsx b/src/components/IntroOverlay/IntroDot.tsx index f63ecba..8f62cc5 100644 --- a/src/components/IntroOverlay/IntroDot.tsx +++ b/src/components/IntroOverlay/IntroDot.tsx @@ -1,5 +1,4 @@ import { motion } from "framer-motion"; - import { IntroDotProps } from '../../types'; import '../../styles/Overlay.css'; @@ -12,7 +11,10 @@ const IntroDot: React.FC = ({ handleDotAnimationComplete(animationState); }; - const needsHighZ = animationState === 'expand' || animationState === 'contract'; + const needsHighZ = + animationState === 'expand' || + animationState === 'expanded' || + animationState === 'contract'; return (