diff --git a/frontend/src/auth/RequireAuth.tsx b/frontend/src/auth/RequireAuth.tsx
index 6fe1402..ba824f3 100644
--- a/frontend/src/auth/RequireAuth.tsx
+++ b/frontend/src/auth/RequireAuth.tsx
@@ -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 (
@@ -25,11 +16,7 @@ export default function RequireAuth({ children }: { children: React.ReactNode })
}
if (!isAuthenticated) {
- return (
-
-
-
- );
+ return
;
}
return <>{children}>;
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
new file mode 100644
index 0000000..a29638f
--- /dev/null
+++ b/frontend/src/pages/Login.tsx
@@ -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
(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 (
+
+ {/* Ambient heatmap glow */}
+
+
+ {/* Concentric rings — focal point motif */}
+
+ {[0, 1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+ {/* Card */}
+
+ {/* Wordmark */}
+
+ Ol
+ i
+ VAS
+
+
Visual Attention Analysis
+
+
+
+ {/* Sign in button */}
+
+
+ {error &&
{error}
}
+
+
+ Access is restricted to Oliver agency accounts.
+
+
+
+
+
+ );
+}
+
+/* ── Sub-components ─────────────────────────────────────────────── */
+
+function MicrosoftLogo() {
+ return (
+
+ );
+}
+
+function Spinner() {
+ return (
+
+ );
+}
+
+/* ── 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 = {
+ 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); }
+ }
+`;