Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 116 additions & 1 deletion app/alerts-settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -280,6 +331,70 @@ export default function AlertsSettingsPage() {
>
<Toggle enabled={notifMediumRisk} onChange={setNotifMediumRisk} />
</SettingRow>

{/* Subscribe */}
<div className="pt-4 mt-2 border-t border-zinc-100 dark:border-zinc-700/50">
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
Subscribe to Email Alerts
</p>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
Receive narrative alerts at the selected risk levels above
</p>
<div className="mt-3 flex gap-2">
<input
type="email"
placeholder="you@example.com"
value={subEmail}
onChange={(e) => { 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"
/>
<button
type="button"
onClick={handleSubscribe}
disabled={!subEmail || subLoading}
className="shrink-0 cursor-pointer rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-200 dark:text-zinc-900 dark:hover:bg-white"
>
{subLoading ? "..." : "Subscribe"}
</button>
</div>
{subMessage && (
<p className={`mt-2 text-xs ${subError ? "text-red-500" : "text-emerald-600 dark:text-emerald-400"}`}>
{subMessage}
</p>
)}
</div>

{/* Unsubscribe */}
<div className="pt-4 mt-2 border-t border-zinc-100 dark:border-zinc-700/50">
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
Unsubscribe from Email Alerts
</p>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
Stop receiving narrative alert emails
</p>
<div className="mt-3 flex gap-2">
<input
type="email"
placeholder="you@example.com"
value={unsubEmail}
onChange={(e) => { 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"
/>
<button
type="button"
onClick={handleUnsubscribe}
disabled={!unsubEmail || unsubLoading}
className="shrink-0 cursor-pointer rounded-lg border border-zinc-200 bg-white px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-red-400 dark:hover:bg-red-950/30"
>
{unsubLoading ? "..." : "Unsubscribe"}
</button>
</div>
{unsubMessage && (
<p className={`mt-2 text-xs ${unsubError ? "text-red-500" : "text-emerald-600 dark:text-emerald-400"}`}>
{unsubMessage}
</p>
)}
</div>
</SectionCard>
</div>
</div>
Expand Down
13 changes: 5 additions & 8 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -29,13 +28,11 @@ export default function RootLayout({
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}>
<ThemeProvider>
<AuthProvider>
<NotificationProvider>
<SidebarProvider>
<ClientShell>{children}</ClientShell>
</SidebarProvider>
</NotificationProvider>
</AuthProvider>
<NotificationProvider>
<SidebarProvider>
<ClientShell>{children}</ClientShell>
</SidebarProvider>
</NotificationProvider>
</ThemeProvider>
</body>
</html>
Expand Down
78 changes: 4 additions & 74 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="fixed inset-0 z-[9999] bg-zinc-50 dark:bg-zinc-950 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-zinc-200 dark:border-zinc-700 p-8 text-zinc-900 dark:text-zinc-100">

<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-zinc-900 dark:bg-zinc-700 rounded-xl mb-4 shadow-lg">
<ShieldCheck className="text-white w-8 h-8" />
</div>
<h1 className="text-2xl font-bold tracking-tight">YouTube Intelligence</h1>
<p className="text-zinc-500 dark:text-zinc-400 mt-2 text-sm font-medium text-balance">
Senior Design Team 10 | Security Portal
</p>
</div>

<form className="space-y-5" onSubmit={handleLogin}>
<div>
<label className="block text-sm font-semibold mb-1.5">Email Address</label>
<div className="relative group">
<Mail className="absolute left-3 top-3 w-5 h-5 text-zinc-400 group-focus-within:text-zinc-900 dark:group-focus-within:text-zinc-100 transition-colors" />
<input
type="email"
placeholder="admin@cs4485.com"
required
className="w-full pl-10 pr-4 py-2.5 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-lg outline-none focus:ring-2 focus:ring-zinc-900 dark:focus:ring-zinc-400 transition-all text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500"
/>
</div>
</div>

<div>
<label className="block text-sm font-semibold mb-1.5">Password</label>
<div className="relative group">
<Lock className="absolute left-3 top-3 w-5 h-5 text-zinc-400 group-focus-within:text-zinc-900 dark:group-focus-within:text-zinc-100 transition-colors" />
<input
type="password"
placeholder="••••••••"
required
className="w-full pl-10 pr-4 py-2.5 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-lg outline-none focus:ring-2 focus:ring-zinc-900 dark:focus:ring-zinc-400 transition-all text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500"
/>
</div>
</div>

<button
type="submit"
className="w-full bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 py-3 rounded-lg font-bold hover:bg-zinc-800 dark:hover:bg-white transition-all active:scale-[0.98] shadow-md shadow-zinc-200 dark:shadow-zinc-900"
>
Access Platform
</button>
</form>

<div className="mt-8 pt-6 border-t border-zinc-100 dark:border-zinc-700 text-center">
<p className="text-[10px] text-zinc-400 dark:text-zinc-500 uppercase tracking-[0.2em] font-bold">
Authorized Personnel Only
</p>
</div>
</div>
</div>
);
}
export default function RootPage() {
redirect("/executive-overview");
}
28 changes: 0 additions & 28 deletions components/layout/AuthContext.tsx

This file was deleted.

27 changes: 4 additions & 23 deletions components/layout/ClientShell.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-screen flex-col">
{!isLoginPage && <Header />}
<Header />
<div className="flex min-h-0 flex-1">
{!isLoginPage && <Sidebar />}
<Sidebar />
<main
className="min-w-0 flex-1 overflow-auto bg-[#F5F5F5] dark:bg-zinc-950 flex flex-col"
style={{ scrollbarGutter: "stable" }}
Expand All @@ -39,7 +20,7 @@ export function ClientShell({ children }: { children: ReactNode }) {
</div>

{/* ── Site Footer ───────────────────────────────────────────────── */}
{!isLoginPage && <footer className="border-t border-zinc-200 bg-white px-8 py-10 dark:border-zinc-800 dark:bg-zinc-900 font-sans">
<footer className="border-t border-zinc-200 bg-white px-8 py-10 dark:border-zinc-800 dark:bg-zinc-900 font-sans">
<div className="mx-auto max-w-6xl">

{/* Top row: brand + columns */}
Expand Down Expand Up @@ -122,7 +103,7 @@ export function ClientShell({ children }: { children: ReactNode }) {
</div>

</div>
</footer>}
</footer>
</main>
</div>
</div>
Expand Down
13 changes: 5 additions & 8 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -189,12 +187,11 @@ export function Header() {

<div className="py-1">
<button
type="button"
onClick={() => { logout(); router.push("/"); }}
className="flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm text-red-600 transition-colors hover:bg-red-50 dark:hover:bg-red-950/40"
onClick={() => { setIsProfileOpen(false); router.push('/help'); }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-700 hover:bg-zinc-50 transition-colors dark:text-zinc-300 dark:hover:bg-zinc-800"
>
<LogOut className="size-4 shrink-0" />
Sign Out
<Info className="w-4 h-4 text-zinc-500 dark:text-zinc-400" />
Help &amp; Support
</button>
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ export async function apiFetch<T>(
if (!res.ok) throw new Error(`API ${path}: ${res.status}`);
return res.json() as Promise<T>;
}

export async function apiPost<T>(
path: string,
body: Record<string, unknown>,
): Promise<T> {
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<T>;
}
5 changes: 5 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ const nextConfig: NextConfig = {
experimental: {
viewTransition: true,
},
async redirects() {
return [
{ source: "/", destination: "/executive-overview", permanent: false },
];
},
};

export default nextConfig;
Loading