Add login page with Microsoft SSO button

Dark heatmap-inspired design with animated concentric rings,
OliVAS wordmark, and a clean Microsoft sign-in button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-11 14:12:52 +00:00
parent 5e7b7a1fab
commit 1f83ed8c82
2 changed files with 282 additions and 17 deletions

View file

@ -1,22 +1,13 @@
import { useEffect } from "react";
import { useIsAuthenticated, useMsal } from "@azure/msal-react";
import { InteractionStatus } from "@azure/msal-browser";
import { loginRequest } from "./msalConfig";
import LoadingSpinner from "../components/common/LoadingSpinner";
import Login from "../pages/Login";
export default function RequireAuth({ children }: { children: React.ReactNode }) {
const isAuthenticated = useIsAuthenticated();
const { inProgress, instance } = useMsal();
const { inProgress } = useMsal();
useEffect(() => {
if (!isAuthenticated && inProgress === InteractionStatus.None) {
instance.loginRedirect(loginRequest).catch((e) => {
console.error("[Auth] Login redirect failed:", e);
});
}
}, [isAuthenticated, inProgress, instance]);
if (inProgress !== InteractionStatus.None) {
if (inProgress === InteractionStatus.HandleRedirect) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" message="Authenticating..." />
@ -25,11 +16,7 @@ export default function RequireAuth({ children }: { children: React.ReactNode })
}
if (!isAuthenticated) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" message="Redirecting to login..." />
</div>
);
return <Login />;
}
return <>{children}</>;

View file

@ -0,0 +1,278 @@
import { useState } from "react";
import { useMsal } from "@azure/msal-react";
import { InteractionStatus } from "@azure/msal-browser";
import { loginRequest } from "../auth/msalConfig";
export default function Login() {
const { instance, inProgress } = useMsal();
const [error, setError] = useState<string | null>(null);
const isLoading = inProgress !== InteractionStatus.None;
const handleLogin = async () => {
setError(null);
try {
await instance.loginRedirect(loginRequest);
} catch (e) {
setError("Sign-in failed. Please try again.");
console.error("[Auth] Login redirect failed:", e);
}
};
return (
<div style={styles.root}>
{/* Ambient heatmap glow */}
<div style={styles.glow} />
{/* Concentric rings — focal point motif */}
<div style={styles.rings} aria-hidden="true">
{[0, 1, 2, 3, 4].map((i) => (
<div key={i} style={{ ...styles.ring, ...ringVariant(i) }} />
))}
<div style={styles.ringDot} />
</div>
{/* Card */}
<div style={styles.card}>
{/* Wordmark */}
<div style={styles.wordmark}>
<span style={styles.wordmarkOl}>Ol</span>
<span style={styles.wordmarkIvas}>i</span>
<span style={styles.wordmarkVas}>VAS</span>
</div>
<p style={styles.tagline}>Visual Attention Analysis</p>
<div style={styles.divider} />
{/* Sign in button */}
<button
onClick={handleLogin}
disabled={isLoading}
style={{
...styles.msButton,
...(isLoading ? styles.msButtonDisabled : {}),
}}
onMouseEnter={(e) =>
!isLoading &&
Object.assign((e.target as HTMLElement).style, styles.msButtonHover)
}
onMouseLeave={(e) =>
!isLoading &&
Object.assign(
(e.target as HTMLElement).style,
styles.msButton,
isLoading ? styles.msButtonDisabled : {},
)
}
>
{isLoading ? (
<>
<Spinner />
<span>Redirecting</span>
</>
) : (
<>
<MicrosoftLogo />
<span>Sign in with Microsoft</span>
</>
)}
</button>
{error && <p style={styles.error}>{error}</p>}
<p style={styles.hint}>
Access is restricted to Oliver agency accounts.
</p>
</div>
<style>{keyframes}</style>
</div>
);
}
/* ── Sub-components ─────────────────────────────────────────────── */
function MicrosoftLogo() {
return (
<svg width="18" height="18" viewBox="0 0 21 21" fill="none">
<rect x="1" y="1" width="9" height="9" fill="#F25022" />
<rect x="11" y="1" width="9" height="9" fill="#7FBA00" />
<rect x="1" y="11" width="9" height="9" fill="#00A4EF" />
<rect x="11" y="11" width="9" height="9" fill="#FFB900" />
</svg>
);
}
function Spinner() {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
style={{ animation: "spin 0.8s linear infinite" }}
>
<circle cx="12" cy="12" r="10" stroke="#666" strokeWidth="3" />
<path d="M12 2a10 10 0 0 1 10 10" stroke="#ffc407" strokeWidth="3" strokeLinecap="round" />
</svg>
);
}
/* ── Ring variants ──────────────────────────────────────────────── */
function ringVariant(i: number): React.CSSProperties {
const size = 80 + i * 72;
const delay = i * 0.4;
return {
width: size,
height: size,
opacity: 0.12 - i * 0.018,
animationDelay: `${delay}s`,
};
}
/* ── Styles ─────────────────────────────────────────────────────── */
const styles: Record<string, React.CSSProperties> = {
root: {
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0d0e10",
fontFamily: "'DM Sans', system-ui, sans-serif",
WebkitFontSmoothing: "antialiased",
position: "relative",
overflow: "hidden",
},
glow: {
position: "absolute",
width: 600,
height: 600,
borderRadius: "50%",
background:
"radial-gradient(circle, rgba(255,196,7,0.08) 0%, transparent 70%)",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
},
rings: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
pointerEvents: "none",
},
ring: {
position: "absolute",
borderRadius: "50%",
border: "1px solid #ffc407",
animation: "pulse 4s ease-in-out infinite",
},
ringDot: {
width: 8,
height: 8,
borderRadius: "50%",
background: "#ffc407",
opacity: 0.6,
position: "absolute",
},
card: {
position: "relative",
zIndex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "52px 48px 40px",
borderRadius: 20,
background: "rgba(255,255,255,0.03)",
border: "1px solid rgba(255,255,255,0.07)",
backdropFilter: "blur(12px)",
boxShadow: "0 32px 64px rgba(0,0,0,0.4)",
minWidth: 340,
},
wordmark: {
fontSize: 38,
fontWeight: 700,
letterSpacing: "-0.03em",
lineHeight: 1,
marginBottom: 8,
fontFamily: "'DM Sans', system-ui, sans-serif",
},
wordmarkOl: {
color: "#ffffff",
},
wordmarkIvas: {
color: "#ffffff",
},
wordmarkVas: {
color: "#ffc407",
},
tagline: {
fontSize: 12,
color: "rgba(255,255,255,0.35)",
letterSpacing: "0.12em",
textTransform: "uppercase",
margin: 0,
fontWeight: 500,
},
divider: {
width: "100%",
height: 1,
background: "rgba(255,255,255,0.07)",
margin: "32px 0",
},
msButton: {
display: "flex",
alignItems: "center",
gap: 10,
padding: "11px 22px",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.12)",
background: "rgba(255,255,255,0.05)",
color: "rgba(255,255,255,0.85)",
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
transition: "all 0.15s ease",
width: "100%",
justifyContent: "center",
fontFamily: "inherit",
letterSpacing: "0.01em",
},
msButtonHover: {
background: "rgba(255,255,255,0.09)",
borderColor: "rgba(255,196,7,0.3)",
color: "#ffffff",
},
msButtonDisabled: {
opacity: 0.5,
cursor: "not-allowed",
},
error: {
marginTop: 16,
fontSize: 13,
color: "#f87171",
textAlign: "center",
},
hint: {
marginTop: 20,
fontSize: 11.5,
color: "rgba(255,255,255,0.2)",
textAlign: "center",
lineHeight: 1.5,
},
};
const keyframes = `
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: inherit; }
50% { transform: scale(1.04); opacity: 0.05; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
`;