diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4b7e1ea..6879cad 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -53,7 +53,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - platforms: linux/amd64,linux/arm64 + platforms: ${{ github.ref == 'refs/heads/main' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/src/frontend/public/auth/popup-close.html b/src/frontend/public/auth/popup-close.html index ef2b783..f86d8b2 100644 --- a/src/frontend/public/auth/popup-close.html +++ b/src/frontend/public/auth/popup-close.html @@ -3,32 +3,11 @@ Authentication Complete - -
Authentication complete! You may close this window.
diff --git a/src/frontend/src/AuthGate.tsx b/src/frontend/src/AuthGate.tsx index 6a1f274..2c22846 100644 --- a/src/frontend/src/AuthGate.tsx +++ b/src/frontend/src/AuthGate.tsx @@ -50,11 +50,37 @@ 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} - {isAuthenticated === false && } + {showAuthModal && ( + + )} ); } diff --git a/src/frontend/src/styles/AuthModal.scss b/src/frontend/src/styles/AuthModal.scss index f52c349..f6a6427 100644 --- a/src/frontend/src/styles/AuthModal.scss +++ b/src/frontend/src/styles/AuthModal.scss @@ -1,238 +1,123 @@ /* Auth Modal Styles */ -.auth-modal-overlay { - position: absolute; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - animation: modalFadeIn 0.3s ease-out forwards; -} - -/* Wrapper for modal and logo */ -.auth-modal-wrapper { - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -/* Backdrop with blur effect */ -.auth-modal-backdrop { - position: absolute; - inset: 0; - background-color: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(0.6px); - z-index: -1; -} - -/* Modal container with animation */ -.auth-modal-container { - padding: 18px; - position: relative; - z-index: 10; - width: 100%; - max-width: 500px; - 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: modalZoomIn 0.3s ease-out forwards; - font-family: 'Roboto', sans-serif; -} - -/* Favicon behind modal, relative to wrapper */ -.auth-modal-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; - animation: fadeInSlideUp 0.3s ease-out forwards; - animation-delay: 1s; /* Delay appearance by 1 second */ -} - -/* Animation for favicon to slide up and fade in */ -@keyframes fadeInSlideUp { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); +@import './Modal.scss'; + +.auth-modal { + &__content { + position: relative; + z-index: 1; + padding: 20px; + margin: 12px; + padding-top: 0px; + padding-bottom: 0px; } -} - -/* Modal content */ -.auth-modal-content { - position: relative; - z-index: 1; - padding: 20px; - margin: 12px; - padding-top: 0px; - padding-bottom: 0px; -} -/* Modal title container */ -.auth-modal-title-container { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.auth-modal-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; -} - -/* Modal title */ -.auth-modal-title { - margin: 0 auto; - font-size: 72px; - font-weight: 700; - color: white; - text-align: center; - display: inline-block; - margin-bottom: 10px; -} -.auth-modal-title-dot { - color: #fa8933; -} - -/* Modal description */ -.auth-modal-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; + &__title-container { + display: flex; + align-items: center; + gap: 0.5rem; } -} - -/* Sign-in buttons container */ -.auth-modal-buttons { - display: flex; - align-items: center; - flex-direction: column; - gap: 13px; - margin-top: 30px; - margin-bottom: 40px; -} - -/* Base button styles */ -.auth-modal-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 button style (GitHub) */ -.auth-modal-button-outline { - border: 1px solid #3a3a43; - background-color: #2a2a33; - color: white; -} - -/* Primary button style (Google) */ -.auth-modal-button-primary { - border: none; - background-color: white; - color: #232329; -} - -/* Footer styles */ -.auth-modal-footer { - display: flex; - justify-content: center; - align-items: center; - gap: 12px; - font-size: 14px; - color: #6b7280; -} -.auth-modal-footer-link { - display: flex; - align-items: center; - color: #6b7280; - text-decoration: none; - font-size: 14px; - transition: color 0.15s; -} - -.auth-modal-footer-link:hover { - color: #a0a0a0; -} - -/* Warning message */ -.auth-modal-warning { - margin-top: 15px; - text-align: center; - color: #606060bd; - font-size: 12px; - font-weight: 400; -} - -/* Animation keyframes */ -@keyframes modalFadeIn { - from { - opacity: 0; - } - to { - opacity: 1; + &__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; } -} -@keyframes modalZoomIn { - from { - transform: scale(0.95); - opacity: 0; + &__title { + margin: 0 auto; + font-size: 72px; + font-weight: 700; + color: white; + text-align: center; + display: inline-block; + margin-bottom: 10px; + + &-dot { + color: #fa8933; + } } - to { - transform: scale(1); - opacity: 1; + + &__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; + } } -} -/* Media query for screens under 700x700 */ -@media (max-width: 734px), (max-height: 700px) { - .auth-modal-wrapper { - width: 100vw; - height: 100vh; + &__buttons { + display: flex; + align-items: center; + flex-direction: column; + gap: 13px; + margin-top: 30px; + margin-bottom: 40px; } - .auth-modal-container { - width: 100vw; - height: 100vh; - max-width: 100vw; - max-height: 100vh; - border-radius: 0; - margin: 0; - padding: 0; - box-shadow: none; + + &__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/Modal.scss b/src/frontend/src/styles/Modal.scss new file mode 100644 index 0000000..3f466c8 --- /dev/null +++ b/src/frontend/src/styles/Modal.scss @@ -0,0 +1,141 @@ +/* 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/ui/AuthModal.tsx b/src/frontend/src/ui/AuthModal.tsx index 80d5f6d..0d76291 100644 --- a/src/frontend/src/ui/AuthModal.tsx +++ b/src/frontend/src/ui/AuthModal.tsx @@ -1,16 +1,22 @@ import React, { useState, useEffect } from "react"; -import ReactDOM from "react-dom"; 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); @@ -38,40 +44,26 @@ const AuthModal: React.FC = ({ if (!isMounted) return null; - const modalContent = ( -
- {/* Backdrop with blur effect */} -