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
26 changes: 18 additions & 8 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Footer from "../components/sections/Footer";

// src/app/layout.tsx : UI Layout, global context providers, and one off global components
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import { AuthProvider } from "@/providers/AuthProvider";
import { AuthDialogProvider } from "@/providers/AuthDialogProvider";
import AuthModal from "@/components/AuthModal";
import Footer from "@/components/sections/Footer";

const inter = Inter({
variable: "--font-inter",
Expand All @@ -12,19 +15,26 @@ const inter = Inter({

export const metadata: Metadata = {
title: "AI Agents Directory",
description: "Discover the best AI agents for your use case based on verified user reviews. Updated daily.",
description:
"Discover the best AI agents for your use case based on verified user reviews. Updated daily.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body className={`${inter.variable} antialiased`}>
{children}
<Footer/>
{/* Providers are mounted ONCE here and wrap the whole app */}
<AuthProvider>
<AuthDialogProvider>
{/* Your entire app can now use useAuth() and useAuthDialog() */}
{children}
{/* One global login modal instance controlled by useAuthDialog() */}
<AuthModal />
<Footer />
</AuthDialogProvider>
</AuthProvider>
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ export default function HomePage() {
</>
);
}
// force redeploy

97 changes: 97 additions & 0 deletions frontend/components/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import { useEffect, useRef } from "react";
import { useAuth } from "@/providers/AuthProvider";
import { useAuthDialog } from "@/providers/AuthDialogProvider";
import { X } from "lucide-react";

export default function AuthModal() {
const { isOpen, close, resolveSignedIn } = useAuthDialog();
const { user, loading, signInWithGoogle } = useAuth();
const cardRef = useRef<HTMLDivElement | null>(null);

// ✅ Hooks always run in the same order — no early return before them.

// Auto-close once user is signed in
useEffect(() => {
if (!loading && user) {
resolveSignedIn();
}
}, [loading, user, resolveSignedIn]);

// Basic focus trap & escape key
useEffect(() => {
if (!isOpen) return; // ✅ condition INSIDE the effect
const prev = document.activeElement as HTMLElement | null;
cardRef.current?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
prev?.focus();
};
}, [isOpen, close]);

// Now it's safe to return early
if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onClick={close}
aria-modal="true"
role="dialog"
>
<div
ref={cardRef}
className="relative w-full max-w-md rounded-2xl bg-white p-6 shadow-xl outline-none"
tabIndex={-1}
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={close}
className="absolute right-3 top-3 inline-flex h-9 w-9 items-center justify-center rounded-xl border border-green-200 text-green-800 hover:bg-green-50"
aria-label="Close login"
>
<X className="h-5 w-5" />
</button>

{/* Logo + heading */}
<div className="mb-5">
<div className="mb-3 h-8 w-10 rounded-md bg-green-800" aria-hidden />
<h2 className="text-2xl font-semibold text-gray-900">
Sign in to unlock the best of AgentList.
</h2>
</div>

{/* Buttons */}
<div className="space-y-3">
<button
onClick={signInWithGoogle}
className="flex w-full items-center justify-center gap-3 rounded-full border-2 border-green-800 px-5 py-3 text-sm font-medium text-green-900 hover:bg-green-50"
>
<span className="h-5 w-5 rounded-sm bg-white shadow" aria-hidden />
Continue with Google
</button>

<button
disabled
className="flex w-full cursor-not-allowed items-center justify-center gap-3 rounded-full border-2 border-green-200 px-5 py-3 text-sm font-medium text-gray-500"
title="Coming soon"
>
<span className="h-5 w-5 rounded-sm bg-white shadow" aria-hidden />
Continue with email
</button>
</div>

{/* Legal */}
<p className="mt-6 text-center text-xs text-gray-500">
By proceeding, you agree to our Terms and confirm you’ve read our Privacy Policy.
</p>
</div>
</div>
);
}
75 changes: 53 additions & 22 deletions frontend/components/sections/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
// components/Navbar.jsx
import { Globe } from 'lucide-react'; // or swap with another icon if preferred
// components/Navbar.tsx

// ✅ Requires: useAuth (AuthProvider) + useAuthDialog (AuthDialogProvider) mounted in app/layout
"use client";

import Link from "next/link";
import { Globe } from "lucide-react";
import { useAuth } from "@/providers/AuthProvider";
import { useAuthDialog } from "@/providers/AuthDialogProvider";

export const Navbar = () => {
const { user, logout } = useAuth();
const { open } = useAuthDialog();

return (
<header className="w-full bg-white py-4">
{/* CONTENT WRAPPER: centers all navbar content and limits max width */}
<div className="max-w-7xl mx-auto px-8 flex justify-between items-center text-green-900 font-medium">

{/* LEFT: Logo + Brand Name */}
<div className="flex items-center gap-2">
<img src="/logo.png" alt="logo" className="w-12 h-12 object-contain" />
Expand All @@ -15,32 +23,55 @@ export const Navbar = () => {

{/* CENTER: Nav Links */}
<nav className="hidden md:flex gap-6 text-sm">
<a href="/Newsletter" className="font-semibold hover:text-green-700">Newsletter</a>
<a href="/AgentRequest" className="font-semibold hover:text-green-700"> Request</a>
<a href="/AgentSubmission" className="font-semibold hover:text-green-700"> Submit</a>
<a href="/About" className="font-semibold hover:text-green-700">About</a>
<Link href="/Newsletter" className="font-semibold hover:text-green-700">Newsletter</Link>
<Link href="/AgentRequest" className="font-semibold hover:text-green-700">Request</Link>
<Link href="/AgentSubmission" className="font-semibold hover:text-green-700">Submit</Link>
<Link href="/About" className="font-semibold hover:text-green-700">About</Link>
</nav>

{/* RIGHT: Currency + Auth Buttons */}
{/* RIGHT: Currency + Auth */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 pr-4 border-r border-gray-300 text-sm">
<Globe className="w-4 h-4" />
<span>USD</span>
</div>
<a
href="/SignUp"
className="bg-green-900 text-white px-5 py-2 rounded-full text-sm font-semibold hover:bg-green-800 transition"
>
Sign Up
</a>
<a
href="/login"
className="bg-green-900 text-white px-5 py-2 rounded-full text-sm font-semibold hover:bg-green-800 transition"
>
Log In
</a>
</div>

{!user ? (
<>
{/* both open the same auth modal */}
<button
onClick={() => open()}
className="bg-green-900 text-white px-5 py-2 rounded-full text-sm font-semibold hover:bg-green-800 transition"
>
Sign Up
</button>
<button
onClick={() => open()}
className="bg-green-900 text-white px-5 py-2 rounded-full text-sm font-semibold hover:bg-green-800 transition"
>
Log In
</button>
</>
) : (
<div className="flex items-center gap-3">
{/* tiny user chip */}
<div className="flex items-center gap-2 px-3 py-1 rounded-full border border-green-200">
<div className="h-6 w-6 rounded-full bg-green-900 text-white grid place-items-center text-xs">
{(user.displayName?.[0] || user.email?.[0] || "U").toUpperCase()}
</div>
<span className="hidden sm:inline text-sm">
{user.displayName || user.email}
</span>
</div>
<button
onClick={async () => { await logout(); }}
className="px-5 py-2 rounded-full text-sm font-semibold border border-green-900 text-green-900 hover:bg-green-50 transition"
>
Logout
</button>
</div>
)}
</div>
</div>
</header>
);
Expand Down
29 changes: 16 additions & 13 deletions frontend/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { initializeApp } from "firebase/app";
// firebase.ts
import { initializeApp, getApps, getApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
apiKey: "AIzaSyAHSh4dbIWUx2zF_becF2RcyxNKpeDMt6M",
authDomain: "agentlist-22c4d.firebaseapp.com",
databaseURL: "https://agentlist-22c4d-default-rtdb.firebaseio.com",
projectId: "agentlist-22c4d",
storageBucket: "agentlist-22c4d.firebasestorage.app",
messagingSenderId: "412385337270",
appId: "1:412385337270:web:238d2411ebb0cb7c2eab17",
measurementId: "G-WXWSDHNPVH"
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID!,
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db, firebaseConfig };
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const db = getFirestore(app);
export const auth = getAuth(app);
export default app;
34 changes: 16 additions & 18 deletions frontend/lib/getAgents.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import { collection, getDocs, getFirestore } from "firebase/firestore";
import { initializeApp, getApps } from "firebase/app";
// lib/getAgents.ts
import { collection, getDocs, Timestamp } from "firebase/firestore";
import { db } from "@/firebase"; // or "../firebase" if you don't use "@/"

import { firebaseConfig } from "../firebase"; // Adjust the import path as necessary



if (!getApps().length) {
initializeApp(firebaseConfig);
}
export async function getAgents() {
const snap = await getDocs(collection(db, "agents"));
return snap.docs.map((doc) => {
const data = doc.data() as any;

const db = getFirestore();
// Handle Firestore Timestamp or ISO/date string
let isoDate = "";
if (data.datePublished instanceof Timestamp) {
isoDate = data.datePublished.toDate().toISOString().split("T")[0];
} else if (typeof data.datePublished === "string" || typeof data.datePublished === "number") {
isoDate = new Date(data.datePublished).toISOString().split("T")[0];
}

export async function getAgents() {
const querySnapshot = await getDocs(collection(db, "agents"));
return querySnapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
name: data.name ?? "",
category: data.category ?? "",
datePublished: data.datePublished?.seconds
? new Date(data.datePublished.seconds * 1000).toISOString().split("T")[0]
: new Date(data.datePublished).toISOString().split("T")[0],
datePublished: isoDate,
publisher: data.publisher ?? "",
upvotes: data.upvotes ?? 0,
description: data.description ?? "",
website: data.website ?? "",
};
});
}
}
60 changes: 60 additions & 0 deletions frontend/providers/AuthDialogProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import {
createContext,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from "react";

type OpenOpts = { onSignedIn?: () => void };
type AuthDialogCtx = {
isOpen: boolean;
open: (opts?: OpenOpts) => void;
close: () => void;
/** Call this when sign-in succeeds; runs the stored callback (if any) and closes. */
resolveSignedIn: () => void;
};

const AuthDialogContext = createContext<AuthDialogCtx | null>(null);

export function AuthDialogProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const onSignedInRef = useRef<(() => void) | undefined>(undefined);

const open = useCallback((opts?: OpenOpts) => {
onSignedInRef.current = opts?.onSignedIn;
setIsOpen(true);
}, []);

const close = useCallback(() => {
setIsOpen(false);
onSignedInRef.current = undefined;
}, []);

const resolveSignedIn = useCallback(() => {
const cb = onSignedInRef.current;
onSignedInRef.current = undefined;
setIsOpen(false);
if (cb) cb();
}, []);

const value = useMemo(
() => ({ isOpen, open, close, resolveSignedIn }),
[isOpen, open, close, resolveSignedIn]
);

return (
<AuthDialogContext.Provider value={value}>
{children}
</AuthDialogContext.Provider>
);
}

export function useAuthDialog() {
const ctx = useContext(AuthDialogContext);
if (!ctx) throw new Error("useAuthDialog must be used within <AuthDialogProvider>");
return ctx;
}
Loading
Loading