From ea61bd54751847d560fd541ea0722fa6762dc34d Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 6 Mar 2026 21:50:35 -0300 Subject: [PATCH] feat: user nav dropdown in landing page + redirect to editor after OAuth login - Google OAuth callback now redirects to /editor instead of landing page - Landing page nav shows avatar, username and dropdown when logged in (My Projects, Open Editor, Sign out) - Skeleton placeholder during checkSession so login button never flickers Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/routes/auth.py | 3 +- frontend/src/pages/LandingPage.css | 143 +++++++++++++++++++++++++++++ frontend/src/pages/LandingPage.tsx | 82 ++++++++++++++++- 3 files changed, 224 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 77f30c6..cad7b10 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -168,6 +168,7 @@ async def google_callback(code: str, response: Response, db: AsyncSession = Depe await db.refresh(user) jwt_token = create_access_token({"sub": user.id}, expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) - redirect = RedirectResponse(url=settings.FRONTEND_URL) + # Send the user straight to the editor after OAuth login + redirect = RedirectResponse(url=f"{settings.FRONTEND_URL}/editor") _set_auth_cookie(redirect, jwt_token) return redirect diff --git a/frontend/src/pages/LandingPage.css b/frontend/src/pages/LandingPage.css index d9997e7..2f616b3 100644 --- a/frontend/src/pages/LandingPage.css +++ b/frontend/src/pages/LandingPage.css @@ -97,6 +97,149 @@ background: #1a8fd1; } +/* ── Nav auth skeleton ────────────────────────────────── */ +.nav-auth-skeleton { + width: 80px; + height: 28px; + background: var(--border); + border-radius: var(--radius); + animation: skeleton-pulse 1.4s ease-in-out infinite; +} + +@keyframes skeleton-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +/* ── User menu ────────────────────────────────────────── */ +.user-menu { + position: relative; +} + +.user-menu-trigger { + display: flex; + align-items: center; + gap: 8px; + background: transparent; + border: 1px solid var(--border-hi); + border-radius: 20px; + padding: 3px 10px 3px 4px; + cursor: pointer; + color: var(--text); + font-size: 13px; + transition: border-color 0.15s, background 0.15s; +} + +.user-menu-trigger:hover { + border-color: #333; + background: rgba(255,255,255,0.04); +} + +.user-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.user-avatar-initials { + display: flex; + align-items: center; + justify-content: center; + background: var(--accent); + color: #fff; + font-size: 11px; + font-weight: 700; +} + +.user-avatar-lg { + width: 36px; + height: 36px; + font-size: 15px; +} + +.user-menu-name { + font-size: 13px; + font-weight: 500; + color: var(--text); + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-menu-dropdown { + position: absolute; + right: 0; + top: calc(100% + 6px); + background: #131318; + border: 1px solid var(--border-hi); + border-radius: 6px; + min-width: 200px; + z-index: 200; + box-shadow: 0 8px 24px rgba(0,0,0,0.5); + overflow: hidden; +} + +.user-menu-header { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 14px 12px; +} + +.user-menu-uname { + font-size: 13.5px; + font-weight: 600; + color: var(--text); +} + +.user-menu-email { + font-size: 11.5px; + color: var(--text-muted); + margin-top: 1px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 130px; +} + +.user-menu-divider { + height: 1px; + background: var(--border); +} + +.user-menu-item { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: 9px 14px; + color: var(--text-muted); + font-size: 13px; + text-decoration: none; + background: none; + border: none; + cursor: pointer; + text-align: left; + transition: background 0.12s, color 0.12s; +} + +.user-menu-item:hover { + background: rgba(255,255,255,0.05); + color: var(--text); +} + +.user-menu-signout { + color: #e06c75; +} + +.user-menu-signout:hover { + background: rgba(224, 108, 117, 0.08); + color: #e06c75; +} + /* ── Hero ─────────────────────────────────────────────── */ .landing-hero { display: grid; diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 63dc6a1..6f1b6b8 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -1,4 +1,5 @@ -import { Link } from 'react-router-dom'; +import { useState, useRef, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; import { useAuthStore } from '../store/useAuthStore'; import './LandingPage.css'; @@ -330,9 +331,82 @@ const IcoSponsor = () => ( ); +/* ── User nav dropdown ────────────────────────────────── */ +const UserMenu: React.FC = () => { + const user = useAuthStore((s) => s.user); + const logout = useAuthStore((s) => s.logout); + const navigate = useNavigate(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + if (!user) return null; + + const initials = user.username[0].toUpperCase(); + + return ( +
+ + {open && ( +
+
+ {user.avatar_url ? ( + + ) : ( + {initials} + )} +
+
{user.username}
+
{user.email}
+
+
+
+ setOpen(false)}> + + + + Open Editor + + setOpen(false)}> + + + + My Projects + +
+ +
+ )} +
+ ); +}; + /* ── Component ────────────────────────────────────────── */ export const LandingPage: React.FC = () => { const user = useAuthStore((s) => s.user); + const isLoading = useAuthStore((s) => s.isLoading); return (
@@ -347,8 +421,10 @@ export const LandingPage: React.FC = () => { GitHub Examples - {user ? ( - Open Editor + {isLoading ? ( +
+ ) : user ? ( + ) : ( <> Sign in