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 &&
- }
+
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;