diff --git a/src/frontend/package.json b/src/frontend/package.json index 75ebc8f..48e8371 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@atyrode/excalidraw": "^0.18.0-1", + "@atyrode/excalidraw": "^0.18.0-2", "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.74.3", "@tanstack/react-query-devtools": "^5.74.3", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 6fa94fb..216b873 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -125,6 +125,8 @@ export default function App({ setExcalidrawAPI={setExcalidrawAPI} onChange={debouncedLogChange} MainMenu={MainMenu} + isAuthenticated={isAuthenticated} + isAuthLoading={isAuthLoading} > {children} diff --git a/src/frontend/src/AuthGate.tsx b/src/frontend/src/AuthGate.tsx index 2c22846..e3958e8 100644 --- a/src/frontend/src/AuthGate.tsx +++ b/src/frontend/src/AuthGate.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; import { useAuthCheck } from "./api/hooks"; -import AuthModal from "./ui/AuthModal"; /** * If unauthenticated, it shows the AuthModal as an overlay, but still renders the app behind it. @@ -50,37 +49,6 @@ export default function AuthGate({ children }: { children: React.ReactNode }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated, coderAuthDone]); - // State to control modal visibility and exit animation - const [showAuthModal, setShowAuthModal] = useState(false); - const [isExiting, setIsExiting] = useState(false); - - // Update showAuthModal when authentication status changes - useEffect(() => { - if (isAuthenticated === false) { - setShowAuthModal(true); - setIsExiting(false); - } else if (isAuthenticated === true && showAuthModal) { - // Start exit animation when user becomes authenticated - setIsExiting(true); - // Modal will be removed after animation completes via onExitComplete - } - }, [isAuthenticated, showAuthModal]); - - // Handle exit animation completion - const handleExitComplete = () => { - setShowAuthModal(false); - }; - - // Always render children; overlay AuthModal if not authenticated - return ( - <> - {children} - {showAuthModal && ( - - )} - > - ); + // Just render children - AuthModal is now handled by ExcalidrawWrapper + return <>{children}>; } diff --git a/src/frontend/src/ExcalidrawWrapper.tsx b/src/frontend/src/ExcalidrawWrapper.tsx index 68bb1c5..5bc7692 100644 --- a/src/frontend/src/ExcalidrawWrapper.tsx +++ b/src/frontend/src/ExcalidrawWrapper.tsx @@ -1,4 +1,4 @@ -import React, { Children, cloneElement } from 'react'; +import React, { Children, cloneElement, useState, useEffect } from 'react'; import DiscordButton from './ui/DiscordButton'; import FeedbackButton from './ui/FeedbackButton'; import type { ExcalidrawImperativeAPI } from '@atyrode/excalidraw/types'; @@ -6,6 +6,7 @@ import type { NonDeletedExcalidrawElement } from '@atyrode/excalidraw/element/ty import type { AppState } from '@atyrode/excalidraw/types'; import { MainMenuConfig } from './ui/MainMenu'; import { renderCustomEmbeddable } from './CustomEmbeddableRenderer'; +import AuthDialog from './ui/AuthDialog'; const defaultInitialData = { elements: [], @@ -25,6 +26,8 @@ interface ExcalidrawWrapperProps { onChange: (elements: NonDeletedExcalidrawElement[], state: AppState) => void; MainMenu: any; renderTopRightUI?: () => React.ReactNode; + isAuthenticated?: boolean | null; + isAuthLoading?: boolean; } export const ExcalidrawWrapper: React.FC = ({ @@ -35,7 +38,19 @@ export const ExcalidrawWrapper: React.FC = ({ onChange, MainMenu, renderTopRightUI, + isAuthenticated = null, + isAuthLoading = false, }) => { + // Add state for modal animation + const [isExiting, setIsExiting] = useState(false); + + // Handle auth state changes + useEffect(() => { + if (isAuthenticated === true) { + setIsExiting(true); + } + }, [isAuthenticated]); + const renderExcalidraw = (children: React.ReactNode) => { const Excalidraw = Children.toArray(children).find( (child: any) => @@ -65,7 +80,14 @@ export const ExcalidrawWrapper: React.FC = ({ )), }, - + <> + + {!isAuthLoading && isAuthenticated === false && ( + {}} + /> + )} + > ); }; diff --git a/src/frontend/src/icons/DiscordIcon.tsx b/src/frontend/src/icons/DiscordIcon.tsx new file mode 100644 index 0000000..344b90d --- /dev/null +++ b/src/frontend/src/icons/DiscordIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +interface DiscordIconProps { + className?: string; + width?: number; + height?: number; + fill?: string; +} + +export const DiscordIcon: React.FC = ({ + className = "", + width = 20, + height = 20, + fill = "currentColor", +}) => { + return ( + + + + ); +}; + +export default DiscordIcon; diff --git a/src/frontend/src/icons/GithubIcon.tsx b/src/frontend/src/icons/GithubIcon.tsx new file mode 100644 index 0000000..dd94550 --- /dev/null +++ b/src/frontend/src/icons/GithubIcon.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +interface GithubIconProps { + className?: string; + width?: number; + height?: number; + stroke?: string; + strokeWidth?: number; +} + +export const GithubIcon: React.FC = ({ + className = "", + width = 20, + height = 20, + stroke = "currentColor", + strokeWidth = 2, +}) => { + return ( + + + + + ); +}; + +export default GithubIcon; diff --git a/src/frontend/src/icons/GoogleIcon.tsx b/src/frontend/src/icons/GoogleIcon.tsx new file mode 100644 index 0000000..bac7d73 --- /dev/null +++ b/src/frontend/src/icons/GoogleIcon.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +interface GoogleIconProps { + className?: string; + width?: number; + height?: number; +} + +export const GoogleIcon: React.FC = ({ + className = "", + width = 20, + height = 20, +}) => { + return ( + + + + + + + + ); +}; + +export default GoogleIcon; diff --git a/src/frontend/src/icons/index.ts b/src/frontend/src/icons/index.ts new file mode 100644 index 0000000..05d4ba1 --- /dev/null +++ b/src/frontend/src/icons/index.ts @@ -0,0 +1,3 @@ +export { default as GoogleIcon } from './GoogleIcon'; +export { default as GithubIcon } from './GithubIcon'; +export { default as DiscordIcon } from './DiscordIcon'; diff --git a/src/frontend/src/styles/AuthDialog.scss b/src/frontend/src/styles/AuthDialog.scss new file mode 100644 index 0000000..1aab2b1 --- /dev/null +++ b/src/frontend/src/styles/AuthDialog.scss @@ -0,0 +1,215 @@ +/* Auth Modal Styles */ + +.excalidraw .Dialog--fullscreen { + .auth-modal { + &__logo-container { + display: none; + } + &__content { + height: 100%; + margin-bottom: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + flex: 1; + } + &__buttons { + margin-bottom: auto; + } + } + + .Island { + height: 100%; + display: flex; + flex-direction: column; + } +} + +.auth-modal { + + .Island { + padding-top: 15px !important; + padding-bottom: 20px !important; + } + + &__wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 5; + background-color: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(1px); + } + + &__logo-container { + position: relative; + width: fit-content; + width: 100%; + height: 100%; + top: 50%; + left: 50%; + opacity: 0; + animation: logo-fade-in 2s cubic-bezier(0.00, 1.26, 0.64, 0.95) forwards; + animation-delay: 0.5s; + } + + @keyframes logo-fade-in { + from { + transform: translate(-270px, -318px); + opacity: 0; + } + to { + transform: translate(-230px, -318px); + opacity: 1; + } + } + + &__logo { + width: 60px; + height: 60px; + object-fit: contain; + } + + &__logo-speech-bubble { + position: absolute; + background-color: rgb(232, 232, 232); + color: #333; + padding: 8px 12px; + border-radius: 12px; + font-size: 14px; + max-width: 180px; + left: 73px; + top: 0px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + animation: bubble-appear 0.3s ease-out forwards; + animation-delay: 1.5s; + opacity: 0; + transform: translateY(5px); + + &::before { + content: ''; + position: absolute; + left: -10px; + top: 10px; + border-width: 5px 10px 5px 0; + border-style: solid; + border-color: transparent rgb(232, 232, 232) transparent transparent; + } + } + + @keyframes bubble-appear { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + &__title-container { + display: flex; + align-items: center; + } + + &__title { + margin: 0 auto; + font-size: 65px; + font-weight: 700; + color: white; + text-align: center; + + &-dot { + color: #fa8933; + } + } + + &__content { + display: flex; + flex-direction: column; + align-items: center; + } + + &__description { + color: #a0a0a9; + font-size: 18px; + line-height: 1.5; + text-align: center; + white-space: pre-line; + margin-bottom: 40px; + margin-top: 10px; + min-width: 300px; + max-width: 65%; + + .highlight { + color: #cfcfcf; + font-weight: bold; + } + } + + &__buttons { + display: flex; + align-items: center; + flex-direction: column; + gap: 13px; + width: 100%; + padding-bottom: 50px; + + button { + display: flex; + align-items: center; + justify-content: center; + width: 65%; + min-width: 250px; + gap: 8px; + height: 44px; + border-radius: 6px; + border: 2px solid #727279; + font-size: 15px; + font-weight: 500; + transition: all 0.2s ease; + cursor: pointer; + background-color: #464652; + color: white; + + &:hover { + border: 2px solid #cc6d24; + } + } + } + + &__footer { + + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + color: #a0a0a0; + margin-bottom: 10px; + padding-top: 10px; + border-top: 1px solid var(--dialog-border-color); + width: 100%; + + &-link { + display: flex; + align-items: center; + color: #b8723c !important; + font-size: 14px; + transition: color 0.15s; + + &:hover { + color: #a0a0a0 !important; + } + } + } + + &__warning { + text-align: right; + color: #828282bd; + font-size: 14px; + font-weight: 400; + } +} diff --git a/src/frontend/src/styles/AuthModal.scss b/src/frontend/src/styles/AuthModal.scss deleted file mode 100644 index f6a6427..0000000 --- a/src/frontend/src/styles/AuthModal.scss +++ /dev/null @@ -1,123 +0,0 @@ -/* Auth Modal Styles */ -@import './Modal.scss'; - -.auth-modal { - &__content { - position: relative; - z-index: 1; - padding: 20px; - margin: 12px; - padding-top: 0px; - padding-bottom: 0px; - } - - &__title-container { - display: flex; - align-items: center; - gap: 0.5rem; - } - - &__separator { - width: 100%; - height: 1.5px; - background: rgba(120, 120, 120, 0.25); - margin: 0.75rem auto 1.5rem auto; - border: none; - border-radius: 1px; - } - - &__title { - margin: 0 auto; - font-size: 72px; - font-weight: 700; - color: white; - text-align: center; - display: inline-block; - margin-bottom: 10px; - - &-dot { - color: #fa8933; - } - } - - &__description { - margin-top: 35px; - margin-bottom: 35px; - color: #a0a0a9; - font-size: 16px; - line-height: 1.5; - text-align: center; - white-space: pre-line; - max-width: 270px; - - .highlight { - color: #cfcfcf; - font-weight: bold; - } - } - - &__buttons { - display: flex; - align-items: center; - flex-direction: column; - gap: 13px; - margin-top: 30px; - margin-bottom: 40px; - } - - &__button { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 85%; - height: 44px; - border-radius: 6px; - font-size: 15px; - font-weight: 500; - transition: all 0.2s ease; - cursor: pointer; - - &--outline { - border: 1px solid #3a3a43; - background-color: #2a2a33; - color: white; - } - - &--primary { - border: none; - background-color: white; - color: #232329; - } - } - - &__footer { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - font-size: 14px; - color: #6b7280; - - &-link { - display: flex; - align-items: center; - color: #6b7280; - text-decoration: none; - font-size: 14px; - transition: color 0.15s; - - &:hover { - color: #a0a0a0; - } - } - } - - &__warning { - margin-top: 15px; - text-align: center; - color: #606060bd; - font-size: 12px; - font-weight: 400; - } -} diff --git a/src/frontend/src/styles/DiscordButton.scss b/src/frontend/src/styles/DiscordButton.scss index aa5e57a..9bdba12 100644 --- a/src/frontend/src/styles/DiscordButton.scss +++ b/src/frontend/src/styles/DiscordButton.scss @@ -1,4 +1,7 @@ .discord-button { + display: flex; + align-items: center; + justify-content: center; height: 2.75rem; min-width: 2.75rem; margin-left: 0.275rem; diff --git a/src/frontend/src/styles/Modal.scss b/src/frontend/src/styles/Modal.scss deleted file mode 100644 index 3f466c8..0000000 --- a/src/frontend/src/styles/Modal.scss +++ /dev/null @@ -1,141 +0,0 @@ -/* Generic Modal Styles */ -.modal { - &__overlay { - position: absolute; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - /* Animation is now applied via inline style */ - } - - &__wrapper { - position: relative; - display: flex; - align-items: center; - justify-content: center; - } - - &__backdrop { - position: absolute; - inset: 0; - background-color: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(0.6px); - z-index: -1; - } - - &__container { - padding: 18px; - position: relative; - z-index: 10; - width: 100%; - border-radius: 12px; - background-color: #232329; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2); - /* Animation is now applied via inline style */ - font-family: 'Roboto', sans-serif; - } - - &__favicon { - position: absolute; - top: -11.5%; - left: 8%; - width: 65px; - height: 65px; - z-index: 0; /* Behind modal */ - pointer-events: none; - user-select: none; - opacity: 0; /* Initial state, animation will control visibility */ - /* Animation is now applied via inline style */ - } -} - -/* Animation for favicon to slide up and fade in */ -@keyframes fadeInSlideUp { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Animation for favicon to slide down and fade out */ -@keyframes fadeOutSlideDown { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(5px); - } -} - -/* Animation keyframes */ -@keyframes modalFadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes modalFadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@keyframes modalZoomIn { - from { - transform: scale(0.95); - opacity: 0; - } - to { - transform: scale(1); - opacity: 1; - } -} - -@keyframes modalZoomOut { - from { - transform: scale(1); - opacity: 1; - } - to { - transform: scale(0.95); - opacity: 0; - } -} - -/* Media query for screens under 700x700 */ -@media (max-width: 734px), (max-height: 700px) { - .modal { - &__wrapper { - width: 100vw; - height: 100vh; - } - - &__container { - width: 100vw; - height: 100vh; - max-width: 100vw !important; - max-height: 100vh; - border-radius: 0; - margin: 0; - padding: 0; - box-shadow: none; - display: flex; - align-items: center; - justify-content: center; - } - } -} diff --git a/src/frontend/src/styles/index.scss b/src/frontend/src/styles/index.scss index 3d51eef..8fa96ed 100644 --- a/src/frontend/src/styles/index.scss +++ b/src/frontend/src/styles/index.scss @@ -1,7 +1,7 @@ @import './fonts.scss'; @import './HtmlEditor.scss'; @import './Editor.scss'; -@import './AuthModal.scss'; +@import './AuthDialog.scss'; @import './FeedbackButton.scss'; @import './DiscordButton.scss'; @import './MainMenuLabel.scss'; @@ -54,3 +54,23 @@ body { margin-left: 11px !important; font-size: 15px !important; } + +.excalidraw .Dialog--fullscreen { + .Dialog__close { + display: none; + } +} + +.excalidraw .Modal__background { + background-color: rgba(0, 0, 0, 0); + backdrop-filter: blur(0px); + animation: Modal__background__fade-in 0.3s ease-out forwards !important; + + &.animations-disabled { + animation: none !important; + } +} + +.excalidraw .Modal__content { + animation: Modal__content_fade-in 0.3s ease-out forwards !important; +} \ No newline at end of file diff --git a/src/frontend/src/ui/AuthDialog.tsx b/src/frontend/src/ui/AuthDialog.tsx new file mode 100644 index 0000000..b7e0dfe --- /dev/null +++ b/src/frontend/src/ui/AuthDialog.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { capture } from "../utils/posthog"; +import { Mail } from "lucide-react"; +import { queryClient } from "../api/queryClient"; +import { GoogleIcon, GithubIcon, DiscordIcon } from "../icons"; + +import { Dialog } from "@atyrode/excalidraw"; + +interface AuthDialogProps { + description?: React.ReactNode; + warningText?: string; + onClose?: () => void; + children?: React.ReactNode; +} + +export const AuthDialog = ({ + description = <>Welcome to your whiteboard IDE. Open terminals, VSCode, or Cursor in your pad, and start coding right away.>, + warningText = <>This is an open-source project in beta. Back up your work!>, + onClose, + children, +}: AuthDialogProps) => { + const [modalIsShown, setModalIsShown] = useState(true); + + // Array of random messages that the logo can "say" + const logoMessages = [ + "Hello there!", + "Welcome to pad.ws!", + "Ready to code?", + "Let's build something cool!", + "Code, collaborate, create!", + "Happy coding!", + "Ideas become reality here!", + "Let's get productive!", + "Let's turn ideas into code!" + ]; + + // Select a random message when component mounts + const randomMessage = useMemo(() => { + const randomIndex = Math.floor(Math.random() * logoMessages.length); + return logoMessages[randomIndex]; + }, []); + + useEffect(() => { + capture("auth_modal_shown"); + + // Load GitHub buttons script + const script = document.createElement('script'); + script.src = 'https://buttons.github.io/buttons.js'; + script.async = true; + script.defer = true; + document.body.appendChild(script); + + return () => { + // Clean up script when component unmounts + if (document.body.contains(script)) { + document.body.removeChild(script); + } + }; + }, []); + + useEffect(() => { + const checkLocalStorage = () => { + const authCompleted = localStorage.getItem('auth_completed'); + if (authCompleted) { + localStorage.removeItem('auth_completed'); + queryClient.invalidateQueries({ queryKey: ['auth'] }); + clearInterval(intervalId); + handleClose(); + } + }; + + const intervalId = setInterval(checkLocalStorage, 500); + + return () => { + clearInterval(intervalId); + }; + }, []); + + const handleClose = React.useCallback(() => { + setModalIsShown(false); + + if (onClose) { + onClose(); + } + }, [onClose]); + + // Prepare the content for the Dialog + const dialogContent = ( + + {description} + + {/* Sign-in buttons */} + + { + window.open( + "/auth/login?kc_idp_hint=google&popup=1", + "authPopup", + "width=500,height=700,noopener,noreferrer" + ); + }} + > + + Continue with Google + + + { + window.open( + "/auth/login?kc_idp_hint=github&popup=1", + "authPopup", + "width=500,height=700,noopener,noreferrer" + ); + }} + > + + Continue with GitHub + + + + {/* Footer */} + + {/* Warning message */} + + {warningText} + + + {/* GitHub Star button */} + + Star + + + + + + + ); + + return ( + <> + {modalIsShown && ( + + + + + {randomMessage} + + + + pad.ws + + } + closeOnClickOutside={false} + children={children || dialogContent} + /> + + )} + > + ); +}; + +export default AuthDialog; diff --git a/src/frontend/src/ui/AuthModal.tsx b/src/frontend/src/ui/AuthModal.tsx deleted file mode 100644 index 0d76291..0000000 --- a/src/frontend/src/ui/AuthModal.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { capture } from "../utils/posthog"; -import { Mail } from "lucide-react"; -import { queryClient } from "../api/queryClient"; -import Modal from "./Modal"; -import "../styles/AuthModal.scss"; - -interface AuthModalProps { - description?: React.ReactNode; - warningText?: string; - onExitComplete?: () => void; - isExiting?: boolean; -} - -const AuthModal: React.FC = ({ - description = <>Welcome to your whiteboard IDE. Open terminals and start coding right away in your own Ubuntu VM!>, - warningText = "🚧 This is a beta. Make backups! 🚧", - onExitComplete, - isExiting = false, -}) => { - const [isMounted, setIsMounted] = useState(false); - - useEffect(() => { - setIsMounted(true); - capture("auth_modal_shown"); - }, []); - - useEffect(() => { - const checkLocalStorage = () => { - const authCompleted = localStorage.getItem('auth_completed'); - if (authCompleted) { - localStorage.removeItem('auth_completed'); - queryClient.invalidateQueries({ queryKey: ['auth'] }); - clearInterval(intervalId); - } - }; - - const intervalId = setInterval(checkLocalStorage, 500); - - return () => { - clearInterval(intervalId); - }; - }, []); - - if (!isMounted) return null; - - return ( - - - - pad.ws - - - - {description} - - {/* Sign-in buttons */} - - { - window.open( - "/auth/login?kc_idp_hint=google&popup=1", - "authPopup", - "width=500,height=700,noopener,noreferrer" - ); - }} - > - - - - - - - - Continue with Google - - - { - window.open( - "/auth/login?kc_idp_hint=github&popup=1", - "authPopup", - "width=500,height=700,noopener,noreferrer" - ); - }} - > - - - - - Continue with GitHub - - - - {/* Footer */} - - - - - - - | - - - - - - - | - - - - - - {/* Warning message */} - - {warningText} - - - - ); -}; - -export default AuthModal; diff --git a/src/frontend/src/ui/DiscordButton.tsx b/src/frontend/src/ui/DiscordButton.tsx index 0cee6d0..e9cf00e 100644 --- a/src/frontend/src/ui/DiscordButton.tsx +++ b/src/frontend/src/ui/DiscordButton.tsx @@ -1,4 +1,5 @@ import React from "react"; +import DiscordIcon from "../icons/DiscordIcon"; const DISCORD_URL = "https://discord.gg/NnXSESxWpA"; @@ -15,24 +16,12 @@ const DiscordButton: React.FC = () => { type="button" style={{ padding: 0, width: "2.5rem" }} > - - - - - - + ); }; diff --git a/src/frontend/src/ui/Modal.tsx b/src/frontend/src/ui/Modal.tsx deleted file mode 100644 index 57a9f04..0000000 --- a/src/frontend/src/ui/Modal.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useEffect } from "react"; -import ReactDOM from "react-dom"; -import "../styles/Modal.scss"; - -interface ModalProps { - children: React.ReactNode; - showLogo?: boolean; - logoSrc?: string; - logoAlt?: string; - maxWidth?: string | number; - className?: string; - isExiting?: boolean; - onExitComplete?: () => void; -} - -const Modal: React.FC = ({ - children, - showLogo = true, - logoSrc = "/assets/images/favicon.png", - logoAlt = "Logo", - maxWidth = "500px", - className = "", - isExiting = false, - onExitComplete, -}) => { - // For entrance: Modal appears first, then logo with delay - // For exit: Logo disappears first, then modal with delay - const overlayAnimation = isExiting ? "modalFadeOut" : "modalFadeIn"; - const containerAnimation = isExiting ? "modalZoomOut" : "modalZoomIn"; - const faviconAnimation = isExiting ? "fadeOutSlideDown" : "fadeInSlideUp"; - - // Animation delays - const overlayDelay = isExiting ? "0.3s" : "0s"; // Delay modal exit until logo animation completes - const containerDelay = isExiting ? "0.3s" : "0s"; // Delay container exit until logo animation completes - const faviconDelay = isExiting ? "0s" : "0.3s"; // Logo appears with delay on entrance, but exits immediately - - // Handle exit animation completion - useEffect(() => { - if (isExiting && onExitComplete) { - // Wait for all animations to complete before calling onExitComplete - // Logo animation (0.3s) + delay (0.3s) + modal animation (0.3s) - const timer = setTimeout(() => { - onExitComplete(); - }, 600); // Total animation duration with delay - - return () => clearTimeout(timer); - } - }, [isExiting, onExitComplete]); - - const modalContent = ( - - {/* Backdrop with blur effect */} - - - {/* Wrapper for logo and modal, to position logo behind modal */} - - {/* Logo behind modal */} - {showLogo && ( - - )} - {/* Modal container with animation */} - - {children} - - - - ); - - // Use createPortal to render the modal at the end of the document body - return ReactDOM.createPortal(modalContent, document.body); -}; - -export default Modal; diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index dd45438..b6b25d0 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@atyrode/excalidraw@^0.18.0-1": - version "0.18.0-1" - resolved "https://registry.yarnpkg.com/@atyrode/excalidraw/-/excalidraw-0.18.0-1.tgz#1ba7c5e6a2d5c2b994069eb45fe41c7cfc198364" - integrity sha512-0Hqnj1wqfPuc6RaI/l2dCXAvw9wM6TZ9wfbZwi1ySx3dVJ+Hs79+2p1Gtx/dcCbkG8L3XiqtC1bFh3Cmls3LmQ== +"@atyrode/excalidraw@^0.18.0-2": + version "0.18.0-2" + resolved "https://registry.yarnpkg.com/@atyrode/excalidraw/-/excalidraw-0.18.0-2.tgz#66be05d5b8a2458dd65fb58baa104fd803a41c5d" + integrity sha512-z3scC5BzVnu8ddS1TJTNxX+wPa3bj72j9RVleSV7iDZsClLDqgcrW+Z/cVHEphWpvBKlMzRo31vDdg+dqE6dDg== dependencies: "@braintree/sanitize-url" "6.0.2" "@excalidraw/laser-pointer" "1.3.1" @@ -537,29 +537,29 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76" integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ== -"@tanstack/query-core@5.74.3": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.74.3.tgz#1fc97bd9a47f2acdf9f49737b1e6969e7bbcb7d7" - integrity sha512-Mqk+5o3qTuAiZML248XpNH8r2cOzl15+LTbUsZQEwvSvn1GU4VQhvqzAbil36p+MBxpr/58oBSnRzhrBevDhfg== +"@tanstack/query-core@5.74.4": + version "5.74.4" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.74.4.tgz#08c4f88f336738d822d9242c5e7d2be50f5c25b3" + integrity sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A== -"@tanstack/query-devtools@5.73.3": - version "5.73.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.73.3.tgz#8fc9872ed4408964b2c560d811caee491cfbdc3c" - integrity sha512-hBQyYwsOuO7QOprK75NzfrWs/EQYjgFA0yykmcvsV62q0t6Ua97CU3sYgjHx0ZvxkXSOMkY24VRJ5uv9f5Ik4w== +"@tanstack/query-devtools@5.74.6": + version "5.74.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.74.6.tgz#88a29fac2ba3db6c9bda8f40808f808d7a88df0b" + integrity sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw== "@tanstack/react-query-devtools@^5.74.3": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.74.3.tgz#3b079cd938b4f00d779e233e7fbb96c90264bf0f" - integrity sha512-H7TsOBB1fRCuuawrBzKMoIszqqILr2IN5oGLYMl7QG7ERJpMdc4hH8OwzBhVxJnmKeGwgtTQgcdKepfoJCWvFg== + version "5.74.6" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.74.6.tgz#169a3e2beab0c87ce3ebbda909bbd2c6590239f8" + integrity sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q== dependencies: - "@tanstack/query-devtools" "5.73.3" + "@tanstack/query-devtools" "5.74.6" "@tanstack/react-query@^5.74.3": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.74.3.tgz#f7acd825abaea091f009d1c3f115212e45c4ee74" - integrity sha512-QrycUn0wxjVPzITvQvOxFRdhlAwIoOQSuav7qWD4SWCoKCdLbyRZ2vji2GuBq/glaxbF4wBx3fqcYRDOt8KDTA== + version "5.74.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.74.4.tgz#d73ee1899c08a227519cbf53b9a0e0b1e67cd3fe" + integrity sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q== dependencies: - "@tanstack/query-core" "5.74.3" + "@tanstack/query-core" "5.74.4" "@types/d3-scale-chromatic@^3.0.0": version "3.1.0" @@ -603,9 +603,9 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@^22.14.0": - version "22.14.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f" - integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw== + version "22.15.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.1.tgz#4cd2c8717a61ae2979c6a0624b4d1b67415bf2c0" + integrity sha512-gSZyd0Qmv7qvbd2fJ9HGdYmv1yhNdelIA4YOtN6vkcmSwFhthxSEsBgU/JYZcXjWT6DFzoATcHrc52Ckh8SeRA== dependencies: undici-types "~6.21.0" @@ -1646,9 +1646,9 @@ postcss@^8.4.36: source-map-js "^1.2.1" posthog-js@^1.236.0: - version "1.236.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.236.0.tgz#26a37491b102e0bed53b7cc924ae9bc121394fba" - integrity sha512-tTKBu/ZHVfBHAvpHhLQX+8WfNWr1q4r2fZmySBYJPR+pK+HQtSwvjHCibaH6ukK38HphLCqc48YTbQGUbZn7Nw== + version "1.236.6" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.236.6.tgz#f4d73d25773f4e4bafd7632717c97b27bd9f1b7c" + integrity sha512-IX4fkn3HCK+ObdHr/AuWd+Ks7bgMpRpOQB93b5rDJAWkG4if4xFVUn5pgEjyCNeOO2GM1ECnp08q9tYNYEfwbA== dependencies: core-js "^3.38.1" fflate "^0.4.8"
{description}