diff --git a/app/alerts-settings/page.tsx b/app/alerts-settings/page.tsx index 5a42c70..14ce7f5 100644 --- a/app/alerts-settings/page.tsx +++ b/app/alerts-settings/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import { apiFetch } from "@/lib/api"; +import { apiFetch, apiPost } from "@/lib/api"; import type { AlertListResponse, AlertItem } from "@/lib/types/alert"; import { alertItemAsNarrativeHandoff, stashNarrativeHandoff } from "@/lib/narrativeHandoff"; import { useNotifications } from "@/components/layout/NotificationContext"; @@ -88,6 +88,16 @@ export default function AlertsSettingsPage() { const { compactMode, setCompactMode } = useSidebar(); const { isDark, toggle: toggleTheme } = useTheme(); + const [subEmail, setSubEmail] = useState(""); + const [subLoading, setSubLoading] = useState(false); + const [subMessage, setSubMessage] = useState(""); + const [subError, setSubError] = useState(false); + + const [unsubEmail, setUnsubEmail] = useState(""); + const [unsubLoading, setUnsubLoading] = useState(false); + const [unsubMessage, setUnsubMessage] = useState(""); + const [unsubError, setUnsubError] = useState(false); + useEffect(() => { let cancelled = false; (async () => { @@ -114,6 +124,47 @@ export default function AlertsSettingsPage() { return () => { cancelled = true; }; }, [sortFilter]); + async function handleSubscribe() { + if (!notifHighRisk && !notifMediumRisk) { + setSubMessage("Select at least one risk level above."); + setSubError(true); + return; + } + setSubLoading(true); + setSubMessage(""); + try { + const res = await apiPost<{ ok: boolean; detail: string }>("/alerts/subscribe", { + email: subEmail, + notify_high_risk: notifHighRisk, + notify_medium_risk: notifMediumRisk, + }); + setSubMessage(res.detail); + setSubError(false); + } catch { + setSubMessage("Failed to subscribe. Please try again."); + setSubError(true); + } finally { + setSubLoading(false); + } + } + + async function handleUnsubscribe() { + setUnsubLoading(true); + setUnsubMessage(""); + try { + const res = await apiPost<{ ok: boolean; detail: string }>("/alerts/unsubscribe", { + email: unsubEmail, + }); + setUnsubMessage(res.detail); + setUnsubError(!res.ok); + } catch { + setUnsubMessage("This email is not currently subscribed."); + setUnsubError(true); + } finally { + setUnsubLoading(false); + } + } + const filterOptions: SortFilter[] = ["All", "High", "Medium", "Low"]; const filtered = useMemo(() => { @@ -280,6 +331,70 @@ export default function AlertsSettingsPage() { > + + {/* Subscribe */} +
+

+ Subscribe to Email Alerts +

+

+ Receive narrative alerts at the selected risk levels above +

+
+ { setSubEmail(e.target.value); setSubMessage(""); }} + className="flex-1 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none focus:ring-2 focus:ring-zinc-900/20 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:ring-zinc-400/30" + /> + +
+ {subMessage && ( +

+ {subMessage} +

+ )} +
+ + {/* Unsubscribe */} +
+

+ Unsubscribe from Email Alerts +

+

+ Stop receiving narrative alert emails +

+
+ { setUnsubEmail(e.target.value); setUnsubMessage(""); }} + className="flex-1 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none focus:ring-2 focus:ring-zinc-900/20 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:ring-zinc-400/30" + /> + +
+ {unsubMessage && ( +

+ {unsubMessage} +

+ )} +
diff --git a/app/layout.tsx b/app/layout.tsx index eb6bb05..d841d1a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { SidebarProvider } from "@/components/layout/SidebarContext"; import { NotificationProvider } from "@/components/layout/NotificationContext"; -import { AuthProvider } from "@/components/layout/AuthContext"; import { ThemeProvider } from "@/components/layout/ThemeContext"; import { ClientShell } from "@/components/layout/ClientShell"; import "./globals.css"; @@ -29,13 +28,11 @@ export default function RootLayout({ - - - - {children} - - - + + + {children} + + diff --git a/app/page.tsx b/app/page.tsx index 48ab455..25196a3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,75 +1,5 @@ -'use client'; +import { redirect } from "next/navigation"; -import React from 'react'; -import { useRouter } from 'next/navigation'; -import { useAuth } from '@/components/layout/AuthContext'; -import { Lock, Mail, ShieldCheck } from 'lucide-react'; - -export default function LoginPage() { - const router = useRouter(); - const { login } = useAuth(); - - const handleLogin = (e: React.FormEvent) => { - e.preventDefault(); - login(); - router.push('/executive-overview'); - }; - - return ( -
-
- -
-
- -
-

YouTube Intelligence

-

- Senior Design Team 10 | Security Portal -

-
- -
-
- -
- - -
-
- -
- -
- - -
-
- - -
- -
-

- Authorized Personnel Only -

-
-
-
- ); -} \ No newline at end of file +export default function RootPage() { + redirect("/executive-overview"); +} diff --git a/components/layout/AuthContext.tsx b/components/layout/AuthContext.tsx deleted file mode 100644 index 9e875d6..0000000 --- a/components/layout/AuthContext.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; - -type AuthCtx = { - isAuthenticated: boolean; - login: () => void; - logout: () => void; -}; - -const AuthContext = createContext(null); - -export function AuthProvider({ children }: { children: ReactNode }) { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const login = useCallback(() => setIsAuthenticated(true), []); - const logout = useCallback(() => setIsAuthenticated(false), []); - - const value = useMemo(() => ({ isAuthenticated, login, logout }), [isAuthenticated, login, logout]); - - return {children}; -} - -export function useAuth() { - const ctx = useContext(AuthContext); - if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); - return ctx; -} diff --git a/components/layout/ClientShell.tsx b/components/layout/ClientShell.tsx index 12b8cf6..ca0a181 100644 --- a/components/layout/ClientShell.tsx +++ b/components/layout/ClientShell.tsx @@ -1,35 +1,16 @@ "use client"; -import { useEffect } from "react"; -import { usePathname, useRouter } from "next/navigation"; import { Sidebar } from "./Sidebar"; import { Header } from "./Header"; import { PageTransition } from "./PageTransition"; -import { useAuth } from "./AuthContext"; import type { ReactNode } from "react"; export function ClientShell({ children }: { children: ReactNode }) { - const pathname = usePathname(); - const router = useRouter(); - const { isAuthenticated } = useAuth(); - - const isLoginPage = pathname === "/"; - - // Redirect to login whenever not authenticated and not already there - useEffect(() => { - if (!isAuthenticated && !isLoginPage) { - router.replace("/"); - } - }, [isAuthenticated, isLoginPage, router]); - - // While redirecting, render nothing to avoid a flash of protected content - if (!isAuthenticated && !isLoginPage) return null; - return (
- {!isLoginPage &&
} +
- {!isLoginPage && } +
{/* ── Site Footer ───────────────────────────────────────────────── */} - {!isLoginPage &&
+
{/* Top row: brand + columns */} @@ -122,7 +103,7 @@ export function ClientShell({ children }: { children: ReactNode }) {
- } +
diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 1dfcb3f..2d77d90 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -7,13 +7,11 @@ import { apiFetch } from "@/lib/api"; import type { AlertListResponse, AlertItem } from "@/lib/types/alert"; import { useSidebar } from "./SidebarContext"; import { useNotifications } from "./NotificationContext"; -import { useAuth } from "./AuthContext"; import { useTheme } from "./ThemeContext"; -import { User, Settings, LogOut, ChevronDown, Bell } from "lucide-react"; +import { User, Settings, Info, ChevronDown, Bell } from "lucide-react"; export function Header() { const router = useRouter(); - const { logout } = useAuth(); const { compactMode, toggle } = useSidebar(); const { notifHighRisk, notifMediumRisk } = useNotifications(); const { isDark, toggle: toggleTheme } = useTheme(); @@ -189,12 +187,11 @@ export function Header() {
diff --git a/lib/api.ts b/lib/api.ts index 18e8c3f..2d9a34b 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -15,3 +15,16 @@ export async function apiFetch( if (!res.ok) throw new Error(`API ${path}: ${res.status}`); return res.json() as Promise; } + +export async function apiPost( + path: string, + body: Record, +): Promise { + const res = await fetch(`${API_BASE}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`API ${path}: ${res.status}`); + return res.json() as Promise; +} diff --git a/next.config.ts b/next.config.ts index c2792f4..5036a48 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,11 @@ const nextConfig: NextConfig = { experimental: { viewTransition: true, }, + async redirects() { + return [ + { source: "/", destination: "/executive-overview", permanent: false }, + ]; + }, }; export default nextConfig;