Skip to content

Commit 7684762

Browse files
committed
feat: rework LoginPage UI with enhanced design and functionality
1 parent 53795d4 commit 7684762

1 file changed

Lines changed: 262 additions & 98 deletions

File tree

frontend/pages/LoginPage.tsx

Lines changed: 262 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,289 @@
1-
21
import React, { useEffect } from 'react';
32
import { useNavigate, useLocation } from 'react-router-dom';
4-
import { useMsal, useIsAuthenticated } from "@azure/msal-react";
5-
import { loginRequest } from "../authConfig";
6-
import MongoIcon from '../components/icons/MongoIcon';
3+
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
4+
import { loginRequest } from '../authConfig';
75
import { USE_MSAL_AUTH } from '../app.config';
86
import { useAuth } from '../contexts/AuthContext';
9-
import { UserIcon, MicrosoftIcon } from '../components/icons/material-icons-imports';
107

11-
// --- UI Component (Shared) ---
8+
// ── Icons ────────────────────────────────────────────────────────────────────
9+
10+
const MicrosoftLogo = () => (
11+
<svg width="16" height="16" viewBox="0 0 21 21" style={{ flexShrink: 0 }}>
12+
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
13+
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
14+
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
15+
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
16+
</svg>
17+
);
18+
19+
const DevIcon = () => (
20+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" style={{ flexShrink: 0 }}>
21+
<circle cx="8" cy="5.5" r="2.5" />
22+
<path d="M2.5 13.5c0-3.038 2.462-5.5 5.5-5.5s5.5 2.462 5.5 5.5" />
23+
</svg>
24+
);
25+
26+
// ── Feature list ─────────────────────────────────────────────────────────────
27+
28+
const FEATURES = [
29+
{
30+
icon: (
31+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
32+
<path d="M2 4h12M2 8h8M2 12h5" />
33+
</svg>
34+
),
35+
label: 'AI query generation',
36+
},
37+
{
38+
icon: (
39+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
40+
<path d="M2 3h12v3H2zM2 7h12v3H2zM2 11h12v3H2z" />
41+
</svg>
42+
),
43+
label: 'Data explorer',
44+
},
45+
{
46+
icon: (
47+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
48+
<path d="M3 2h7l3 3v9H3zM6 7h5M6 9h5M6 11h3" />
49+
</svg>
50+
),
51+
label: 'Audit log',
52+
},
53+
{
54+
icon: (
55+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
56+
<path d="M2 13h12M4 11V6M7 11V3M10 11V8M13 11V5" />
57+
</svg>
58+
),
59+
label: 'Analytics',
60+
},
61+
];
62+
63+
// ── Shared UI ─────────────────────────────────────────────────────────────────
64+
1265
interface LoginUIProps {
13-
onLogin: () => void;
14-
buttonText: string;
15-
ButtonIcon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
66+
onLogin: () => void;
67+
buttonText: string;
68+
buttonIcon: React.ReactNode;
69+
note: string;
1670
}
1771

18-
const LoginUI: React.FC<LoginUIProps> = ({ onLogin, buttonText, ButtonIcon }) => (
19-
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col justify-center items-center p-4 text-slate-800 dark:text-slate-200">
20-
<div className="max-w-md w-full mx-auto bg-white dark:bg-slate-800/50 dark:ring-1 dark:ring-slate-700 rounded-xl shadow-lg dark:shadow-black/20 p-6 sm:p-8 text-center">
21-
<header className="flex flex-col items-center justify-center space-y-4 mb-8">
22-
<MongoIcon className="w-16 h-16 text-blue-500" />
23-
<div>
24-
<h1 className="text-3xl sm:text-4xl font-bold bg-gradient-to-r from-violet-600 to-blue-600 bg-clip-text text-transparent">QueryPal</h1>
25-
<p className="text-slate-500 dark:text-slate-400 mt-2">Your AI-Powered Database Assistant</p>
26-
</div>
27-
</header>
28-
29-
<main className="space-y-6">
30-
<button
31-
onClick={onLogin}
32-
className="w-full flex justify-center items-center gap-3 px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-violet-600 hover:bg-violet-700 disabled:bg-slate-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 transition-all duration-200 transform hover:scale-[1.02] active:scale-[0.99]"
33-
title={buttonText}
34-
>
35-
<ButtonIcon className="w-6 h-6" />
36-
{buttonText}
37-
</button>
38-
<p className="text-xs text-slate-500 dark:text-slate-400">
39-
{USE_MSAL_AUTH
40-
? "You will be redirected to the Microsoft login page for authentication."
41-
: "Using developer sign-in. No credentials required."
42-
}
43-
</p>
44-
</main>
72+
const LoginUI: React.FC<LoginUIProps> = ({ onLogin, buttonText, buttonIcon, note }) => (
73+
<div style={{
74+
minHeight: '100vh',
75+
background: 'var(--bg)',
76+
display: 'flex',
77+
flexDirection: 'column',
78+
alignItems: 'center',
79+
justifyContent: 'center',
80+
padding: '32px 20px',
81+
fontFamily: 'var(--font-body)',
82+
color: 'var(--fg)',
83+
}}>
84+
<div style={{ width: '100%', maxWidth: 380, display: 'flex', flexDirection: 'column', gap: 28 }}>
85+
86+
{/* Brand */}
87+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
88+
<div style={{
89+
width: 48, height: 48, borderRadius: 14,
90+
background: 'var(--fg)', color: 'var(--bg)',
91+
display: 'grid', placeItems: 'center',
92+
fontFamily: 'var(--font-display)', fontWeight: 700,
93+
fontSize: 26, letterSpacing: '-0.05em',
94+
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
95+
}}>Q</div>
96+
<div style={{ textAlign: 'center' }}>
97+
<div style={{
98+
fontFamily: 'var(--font-display)', fontWeight: 600,
99+
fontSize: 26, letterSpacing: '-0.02em', color: 'var(--fg)',
100+
lineHeight: 1.1,
101+
}}>QueryPal</div>
102+
<div style={{
103+
fontSize: 13, color: 'var(--muted)', marginTop: 5,
104+
fontFamily: 'var(--font-body)',
105+
}}>
106+
AI-powered queries for Azure Cosmos DB
107+
</div>
45108
</div>
46-
<footer className="text-center mt-8 text-slate-500 dark:text-slate-400 text-sm">
47-
<p>Powered by Microsoft Azure and Google Gemini. For internal use only.</p>
48-
<p className="text-xs max-w-md mx-auto">
49-
AI features use the Google Gemini API. Your data is not used to train their models. See the <a href="https://ai.google.dev/gemini-api/terms" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700 dark:hover:text-slate-200">Terms of Service</a>.
50-
</p>
51-
</footer>
109+
</div>
110+
111+
{/* Card */}
112+
<div style={{
113+
background: 'var(--panel)',
114+
border: '1px solid var(--border)',
115+
borderRadius: 'var(--radius-lg)',
116+
padding: '28px 28px 24px',
117+
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
118+
display: 'flex', flexDirection: 'column', gap: 20,
119+
}}>
120+
<div>
121+
<div style={{
122+
fontFamily: 'var(--font-display)', fontWeight: 600,
123+
fontSize: 18, color: 'var(--fg)', letterSpacing: '-0.01em',
124+
}}>
125+
Welcome back
126+
</div>
127+
<div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 5, lineHeight: 1.5 }}>
128+
Sign in to access your workspace and databases.
129+
</div>
130+
</div>
131+
132+
{/* Sign in button */}
133+
<button
134+
onClick={onLogin}
135+
style={{
136+
width: '100%', height: 44,
137+
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
138+
background: 'var(--accent)', color: '#fff',
139+
border: 'none', borderRadius: 9,
140+
fontSize: 13.5, fontWeight: 600, fontFamily: 'var(--font-body)',
141+
cursor: 'pointer',
142+
transition: 'opacity 0.15s, transform 0.1s',
143+
letterSpacing: '-0.01em',
144+
}}
145+
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.opacity = '0.88'; }}
146+
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.opacity = '1'; }}
147+
onMouseDown={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(0.987)'; }}
148+
onMouseUp={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; }}
149+
>
150+
{buttonIcon}
151+
{buttonText}
152+
</button>
153+
154+
{/* Note */}
155+
<div style={{
156+
fontSize: 11.5, color: 'var(--muted)',
157+
textAlign: 'center', lineHeight: 1.55,
158+
borderTop: '1px solid var(--border)',
159+
paddingTop: 16,
160+
}}>
161+
{note}
162+
</div>
163+
</div>
164+
165+
{/* Feature chips */}
166+
<div style={{
167+
display: 'flex', flexWrap: 'wrap',
168+
gap: 8, justifyContent: 'center',
169+
}}>
170+
{FEATURES.map(f => (
171+
<span key={f.label} style={{
172+
display: 'inline-flex', alignItems: 'center', gap: 5,
173+
padding: '4px 10px',
174+
background: 'var(--panel)',
175+
border: '1px solid var(--border)',
176+
borderRadius: 99,
177+
fontSize: 11.5, color: 'var(--muted)',
178+
fontFamily: 'var(--font-body)',
179+
}}>
180+
<span style={{ color: 'var(--accent)', display: 'flex' }}>{f.icon}</span>
181+
{f.label}
182+
</span>
183+
))}
184+
</div>
185+
186+
{/* Footer */}
187+
<div style={{
188+
textAlign: 'center',
189+
fontSize: 11, color: 'var(--muted)',
190+
lineHeight: 1.6,
191+
}}>
192+
<div>Powered by Microsoft Azure &amp; Google Gemini</div>
193+
<div>
194+
AI features are subject to the{' '}
195+
<a
196+
href="https://ai.google.dev/gemini-api/terms"
197+
target="_blank"
198+
rel="noopener noreferrer"
199+
style={{ color: 'var(--accent)', textDecoration: 'none' }}
200+
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.textDecoration = 'underline'; }}
201+
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.textDecoration = 'none'; }}
202+
>
203+
Gemini API Terms of Service
204+
</a>
205+
. For internal use only.
206+
</div>
207+
</div>
52208
</div>
209+
</div>
53210
);
54211

212+
// ── Redirecting state ─────────────────────────────────────────────────────────
213+
214+
const RedirectingScreen = () => (
215+
<div style={{
216+
minHeight: '100vh', background: 'var(--bg)',
217+
display: 'flex', alignItems: 'center', justifyContent: 'center',
218+
fontFamily: 'var(--font-body)', color: 'var(--muted)', fontSize: 13,
219+
gap: 10,
220+
}}>
221+
<style>{`@keyframes qp-spin { to { transform: rotate(360deg); } }`}</style>
222+
<div style={{
223+
width: 14, height: 14, borderRadius: '50%',
224+
border: '2px solid var(--border)', borderTopColor: 'var(--accent)',
225+
animation: 'qp-spin 0.7s linear infinite',
226+
}} />
227+
Redirecting…
228+
</div>
229+
);
55230

56-
// --- Container Components ---
231+
// ── Container components ──────────────────────────────────────────────────────
57232

58233
const MsalLoginPage: React.FC = () => {
59-
const { instance, accounts } = useMsal();
60-
const isAuthenticated = useIsAuthenticated();
61-
const navigate = useNavigate();
62-
const location = useLocation();
63-
64-
useEffect(() => {
65-
console.log('MSAL Login Page - isAuthenticated:', isAuthenticated, 'accounts:', accounts);
66-
if (isAuthenticated && accounts.length > 0) {
67-
// Check if there's a saved location to redirect to
68-
const from = location.state?.from?.pathname || '/hub';
69-
const search = location.state?.from?.search || '';
70-
const hash = location.state?.from?.hash || '';
71-
const redirectPath = from + search + hash;
72-
73-
console.log('User is authenticated, navigating to:', redirectPath);
74-
navigate(redirectPath, { replace: true });
75-
}
76-
}, [isAuthenticated, accounts, navigate, location.state]);
77-
78-
const handleLogin = () => {
79-
console.log('Starting MSAL login redirect');
80-
instance.loginRedirect(loginRequest).catch(e => {
81-
console.error('MSAL login error:', e);
82-
});
83-
}
234+
const { instance, accounts } = useMsal();
235+
const isAuthenticated = useIsAuthenticated();
236+
const navigate = useNavigate();
237+
const location = useLocation();
84238

85-
// Show loading state if we're in the middle of processing authentication
239+
useEffect(() => {
86240
if (isAuthenticated && accounts.length > 0) {
87-
return (
88-
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col justify-center items-center p-4 text-slate-800 dark:text-slate-200">
89-
<div>Redirecting…</div>
90-
</div>
91-
);
241+
const from = location.state?.from?.pathname || '/hub';
242+
const search = location.state?.from?.search || '';
243+
const hash = location.state?.from?.hash || '';
244+
navigate(from + search + hash, { replace: true });
92245
}
246+
}, [isAuthenticated, accounts, navigate, location.state]);
247+
248+
if (isAuthenticated && accounts.length > 0) return <RedirectingScreen />;
93249

94-
return <LoginUI onLogin={handleLogin} buttonText="Sign In with Microsoft Entra ID" ButtonIcon={MicrosoftIcon} />;
250+
return (
251+
<LoginUI
252+
onLogin={() => instance.loginRedirect(loginRequest).catch(console.error)}
253+
buttonText="Continue with Microsoft Entra ID"
254+
buttonIcon={<MicrosoftLogo />}
255+
note="You'll be redirected to Microsoft's secure sign-in page to authenticate."
256+
/>
257+
);
95258
};
96259

97260
const BypassLoginPage: React.FC = () => {
98-
const { login } = useAuth();
99-
const navigate = useNavigate();
100-
const location = useLocation();
101-
102-
const handleLogin = () => {
103-
login();
104-
// Navigate using React Router instead of window.location
105-
setTimeout(() => {
106-
// Check if there's a saved location to redirect to
107-
const from = location.state?.from?.pathname || '/hub';
108-
const search = location.state?.from?.search || '';
109-
const hash = location.state?.from?.hash || '';
110-
const redirectPath = from + search + hash;
111-
112-
navigate(redirectPath, { replace: true });
113-
}, 100);
114-
};
115-
116-
return <LoginUI onLogin={handleLogin} buttonText="Sign In as Developer" ButtonIcon={UserIcon} />;
117-
}
261+
const { login } = useAuth();
262+
const navigate = useNavigate();
263+
const location = useLocation();
118264

119-
// --- Main Exported Component (Router) ---
265+
const handleLogin = () => {
266+
login();
267+
setTimeout(() => {
268+
const from = location.state?.from?.pathname || '/hub';
269+
const search = location.state?.from?.search || '';
270+
const hash = location.state?.from?.hash || '';
271+
navigate(from + search + hash, { replace: true });
272+
}, 100);
273+
};
120274

121-
const LoginPage: React.FC = () => {
122-
return USE_MSAL_AUTH ? <MsalLoginPage /> : <BypassLoginPage />;
275+
return (
276+
<LoginUI
277+
onLogin={handleLogin}
278+
buttonText="Continue as Developer"
279+
buttonIcon={<DevIcon />}
280+
note="Developer sign-in bypass is active. No credentials required."
281+
/>
282+
);
123283
};
124284

285+
// ── Export ────────────────────────────────────────────────────────────────────
286+
287+
const LoginPage: React.FC = () => USE_MSAL_AUTH ? <MsalLoginPage /> : <BypassLoginPage />;
288+
125289
export default LoginPage;

0 commit comments

Comments
 (0)