From c0d8ceb334e4d64662526b71c6eb475887934b47 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Wed, 23 Sep 2026 23:04:11 +0300 Subject: [PATCH] feat(frontend): add dedicated Welcome page and persistent session (#32) --- frontend/src/App.tsx | 6 +- frontend/src/pages/Login/Login.tsx | 63 ++++----- .../src/pages/Welcome/Welcome.module.scss | 131 ++++++++++++++++++ frontend/src/pages/Welcome/Welcome.tsx | 82 +++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- 5 files changed, 243 insertions(+), 41 deletions(-) create mode 100644 frontend/src/pages/Welcome/Welcome.module.scss create mode 100644 frontend/src/pages/Welcome/Welcome.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0cc57ae..b3c0572 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'; import Login from './pages/Login/Login'; +import Welcome from './pages/Welcome/Welcome'; import ErrorPage from './pages/ErrorPage/ErrorPage'; import { pingServer, registerServerDownHandler } from './services/apiClient'; @@ -33,7 +34,10 @@ export default function App() { } /> } /> } /> - } /> + } /> + } /> + } /> + } /> ); } diff --git a/frontend/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx index f08b78d..30ccef3 100644 --- a/frontend/src/pages/Login/Login.tsx +++ b/frontend/src/pages/Login/Login.tsx @@ -1,15 +1,10 @@ -import { FormEvent, useState } from 'react'; +import { FormEvent, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import styles from './Login.module.scss'; import { apiFetch } from '../../services/apiClient'; type Mode = 'signin' | 'signup'; -interface LoggedInUser { - id: number; - name?: string; - email: string; -} - export default function Login() { const [mode, setMode] = useState('signin'); const [name, setName] = useState(''); @@ -18,8 +13,22 @@ export default function Login() { const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - const [loggedInUser, setLoggedInUser] = useState(null); - const [welcomeMessage, setWelcomeMessage] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + // If user is already authenticated, redirect to Welcome page + const stored = localStorage.getItem('zemo_user'); + if (stored) { + try { + const u = JSON.parse(stored); + if (u && u.id) { + navigate('/welcome', { replace: true }); + } + } catch { + localStorage.removeItem('zemo_user'); + } + } + }, [navigate]); function toggleMode() { setMode((m) => (m === 'signin' ? 'signup' : 'signin')); @@ -49,8 +58,9 @@ export default function Login() { setLoading(false); return; } - setLoggedInUser(data.user); - setWelcomeMessage(`Welcome to ZemoLibrary, ${data.user?.name || data.user?.email}!`); + // Save session and redirect to Welcome page + localStorage.setItem('zemo_user', JSON.stringify(data.user)); + navigate('/welcome', { replace: true }); } else { const res = await apiFetch('/auth/login', { method: 'POST', @@ -63,8 +73,9 @@ export default function Login() { setLoading(false); return; } - setLoggedInUser(data.user); - setWelcomeMessage(`Welcome back, ${data.user?.name || data.user?.email}!`); + // Save session and redirect to Welcome page + localStorage.setItem('zemo_user', JSON.stringify(data.user)); + navigate('/welcome', { replace: true }); } } catch { // apiFetch automatically triggers server-down navigation on network fault @@ -74,32 +85,6 @@ export default function Login() { } } - if (loggedInUser && welcomeMessage) { - return ( -
-
-
✓
-

{welcomeMessage}

-

- Logged in as {loggedInUser.email} -

- -
-
- ); - } - return (
diff --git a/frontend/src/pages/Welcome/Welcome.module.scss b/frontend/src/pages/Welcome/Welcome.module.scss new file mode 100644 index 0000000..ff834cc --- /dev/null +++ b/frontend/src/pages/Welcome/Welcome.module.scss @@ -0,0 +1,131 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + min-height: 80vh; + padding: 2rem 1rem; +} + +.welcomeCard { + display: flex; + flex-direction: column; + align-items: center; + max-width: 480px; + width: 100%; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 16px; + padding: 2.5rem 2rem; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.06); + text-align: center; +} + +.avatarCircle { + width: 72px; + height: 72px; + border-radius: 50%; + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 700; + margin-bottom: 1.25rem; + box-shadow: 0 4px 14px rgba(79, 70, 229, 0.35); +} + +.header { + margin-bottom: 1.5rem; + + h1 { + font-size: 1.75rem; + font-weight: 700; + margin: 0.5rem 0; + color: var(--text-color, #111827); + } + + .subtitle { + font-size: 0.95rem; + color: #6b7280; + margin: 0; + + strong { + color: var(--text-color, #111827); + } + } +} + +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + background: #ecfdf5; + color: #059669; + font-size: 0.75rem; + font-weight: 600; + border-radius: 9999px; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.detailsBox { + width: 100%; + background: var(--bg-color, #f9fafb); + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 12px; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.85rem; + margin-bottom: 1.75rem; + text-align: left; +} + +.detailRow { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; +} + +.detailLabel { + color: #6b7280; + font-weight: 500; +} + +.detailValue { + color: var(--text-color, #111827); + font-weight: 600; +} + +.onlineBadge { + color: #10b981; + font-weight: 600; + font-size: 0.85rem; +} + +.actions { + width: 100%; +} + +.signOutBtn { + width: 100%; + padding: 0.75rem; + background: var(--text-color, #111827); + color: var(--bg-color, #ffffff); + border: none; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s ease, transform 0.1s ease; + + &:hover { + opacity: 0.9; + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} diff --git a/frontend/src/pages/Welcome/Welcome.tsx b/frontend/src/pages/Welcome/Welcome.tsx new file mode 100644 index 0000000..2b636cf --- /dev/null +++ b/frontend/src/pages/Welcome/Welcome.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './Welcome.module.scss'; + +interface User { + id: number; + name?: string; + email: string; +} + +export default function Welcome() { + const [user, setUser] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + const stored = localStorage.getItem('zemo_user'); + if (!stored) { + navigate('/login', { replace: true }); + return; + } + try { + setUser(JSON.parse(stored)); + } catch { + localStorage.removeItem('zemo_user'); + navigate('/login', { replace: true }); + } + }, [navigate]); + + function handleSignOut() { + localStorage.removeItem('zemo_user'); + navigate('/login', { replace: true }); + } + + if (!user) { + return null; + } + + const displayName = user.name && user.name.trim().length > 0 ? user.name : user.email.split('@')[0]; + + return ( +
+
+
+ {displayName.charAt(0).toUpperCase()} +
+ +
+ Active Session +

Welcome, {displayName}!

+

+ You have successfully connected to ZemoLibrary. +

+
+ +
+
+ Account Name + {user.name || 'Not provided'} +
+
+ Email Address + {user.email} +
+
+ User ID + #{user.id} +
+
+ Server Status + ● Online (app.zemoserver.dev) +
+
+ +
+ +
+
+
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 98ed9a7..7019964 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/config/api.ts","./src/context/themecontext.tsx","./src/pages/errorpage/errorpage.tsx","./src/pages/login/login.tsx","./src/services/apiclient.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/config/api.ts","./src/context/themecontext.tsx","./src/pages/errorpage/errorpage.tsx","./src/pages/login/login.tsx","./src/pages/welcome/welcome.tsx","./src/services/apiclient.ts"],"version":"5.9.3"} \ No newline at end of file