diff --git a/frontend/.gitignore b/frontend/.gitignore
index 7b8da95..fb1b8a7 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -34,6 +34,7 @@ yarn-error.log*
.env*
!.env.example
+
# vercel
.vercel
diff --git a/frontend/app/components/AppShell.tsx b/frontend/app/components/AppShell.tsx
index e7f9b39..69bb3b2 100644
--- a/frontend/app/components/AppShell.tsx
+++ b/frontend/app/components/AppShell.tsx
@@ -1,5 +1,6 @@
"use client";
import CommandPalette from "./CommandPalette";
+import NotificationsPanel from "./NotificationsPanel";
import { usePathname } from "next/navigation";
import Sidebar from "./Sidebar";
@@ -19,14 +20,31 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
: "min-w-0 flex-1 overflow-x-hidden p-6";
return (
-
+
{!hideSidebar &&
}
-
- {children}
-
+
+ {!isPublicRoute && (
+
+
+
+ )}
+
+
+ {children}
+
+
);
}
diff --git a/frontend/app/components/NotificationsPanel.tsx b/frontend/app/components/NotificationsPanel.tsx
new file mode 100644
index 0000000..dbb489d
--- /dev/null
+++ b/frontend/app/components/NotificationsPanel.tsx
@@ -0,0 +1,229 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+
+export default function NotificationsPanel() {
+ const [open, setOpen] = useState(false);
+ const router = useRouter();
+
+ const [notifications, setNotifications] = useState([
+ {
+ id: 1,
+ title: "Task moved to Review",
+ description: "Landing page redesign moved to review stage.",
+ time: "2m ago",
+ unread: true,
+ href: "/projects",
+ },
+ {
+ id: 2,
+ title: "New chat message",
+ description: "Alex sent a new team message.",
+ time: "10m ago",
+ unread: true,
+ href: "/chat",
+ },
+ {
+ id: 3,
+ title: "Project created",
+ description: "FlowForge Mobile App project was created.",
+ time: "1h ago",
+ unread: false,
+ href: "/projects",
+ },
+ ]);
+ const panelRef = useRef
(null);
+
+ useEffect(() => {
+ function handleOutsideClick(event: MouseEvent) {
+ if (
+ panelRef.current &&
+ !panelRef.current.contains(event.target as Node)
+ ) {
+ setOpen(false);
+ }
+ }
+ function handleEscape(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ setOpen(false);
+ }
+ }
+
+ document.addEventListener("mousedown", handleOutsideClick);
+ window.addEventListener("keydown", handleEscape);
+
+ return () => {
+ document.removeEventListener(
+ "mousedown",
+ handleOutsideClick
+ );
+ window.removeEventListener(
+ "keydown",
+ handleEscape
+ );
+ };
+ }, []);
+
+ const unreadCount = notifications.filter(
+ (notification) => notification.unread
+ ).length;
+
+ function markAllAsRead() {
+ setNotifications((prev) =>
+ prev.map((notification) => ({
+ ...notification,
+ unread: false,
+ }))
+ );
+ }
+
+ return (
+
+
setOpen((prev) => !prev)}
+ className="
+ relative rounded-full p-2
+ text-on-surface-variant
+ hover:bg-surface-container-low
+ transition-all duration-200
+ active:scale-90
+ "
+ type="button"
+ >
+
+ notifications
+
+
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
+
+ {open && (
+
+
+
+
+ Notifications
+
+
+
+ Recent activity and updates
+
+
+
+
+ Mark all as read
+
+
+
+
+ {notifications.length === 0 ? (
+
+
+ notifications_off
+
+
+
+ No notifications yet
+
+
+
+ You're all caught up.
+
+
+ ) : (
+ notifications.map((notification) => (
+
{
+ setNotifications((prev) =>
+ prev.map((item) =>
+ item.id === notification.id
+ ? {
+ ...item,
+ unread: false,
+ }
+ : item
+ )
+ );
+
+ setOpen(false);
+
+ router.push(notification.href);
+ }}
+ className={`
+ flex w-full items-start gap-3
+ border-b border-slate-200
+ px-5 py-4 text-left
+ transition-all duration-200
+ hover:bg-slate-100
+ ${
+ notification.unread
+ ? "bg-slate-50"
+ : "bg-white"
+ }
+ `}
+ >
+
+
+
+
+
+ {notification.title}
+
+
+
+ {notification.time}
+
+
+
+
+ {notification.description}
+
+
+
+ ))
+ )}
+
+
+ )}
+
+);
+}
\ No newline at end of file
diff --git a/frontend/app/components/analytics-comp/AnalyticsHeader.tsx b/frontend/app/components/analytics-comp/AnalyticsHeader.tsx
index bbb6014..d897491 100644
--- a/frontend/app/components/analytics-comp/AnalyticsHeader.tsx
+++ b/frontend/app/components/analytics-comp/AnalyticsHeader.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import NotificationsPanel from "@/app/components/NotificationsPanel";
type AnalyticsHeaderProps = {
sprint: string;
@@ -60,9 +61,8 @@ export default function AnalyticsHeader({
-
- notifications
-
+
+
settings
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx
index 1300cb3..b0f4fab 100644
--- a/frontend/app/dashboard/page.tsx
+++ b/frontend/app/dashboard/page.tsx
@@ -80,9 +80,16 @@ export default function Dashboard() {
if (!session) { setTeamLoading(false); return; }
const { data, error } = await supabase
.from("profiles")
- .select("name, email")
+ .select("full_name, email")
.limit(10);
- if (!error && data) setTeam(data);
+ if (!error && data) {
+ setTeam(
+ data.map((member) => ({
+ name: member.full_name || "User",
+ email: member.email,
+ }))
+ );
+ }
} catch (err) {
console.error("Failed to fetch team members:", err);
} finally {
diff --git a/frontend/app/lib/supabase.ts b/frontend/app/lib/supabase.ts
index b45c420..59841d8 100644
--- a/frontend/app/lib/supabase.ts
+++ b/frontend/app/lib/supabase.ts
@@ -1,12 +1,18 @@
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
-const supabaseKey =
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
- process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
-export const isSupabaseConfigured = !!supabaseUrl && !!supabaseKey;
+export const isSupabaseConfigured =
+ !!supabaseUrl && !!supabaseKey;
-export const supabase = isSupabaseConfigured
- ? createClient(supabaseUrl!, supabaseKey!)
- : null;
\ No newline at end of file
+export const supabase =
+ isSupabaseConfigured
+ ? createClient(supabaseUrl!, supabaseKey!, {
+ auth: {
+ persistSession: true,
+ autoRefreshToken: true,
+ detectSessionInUrl: true,
+ },
+ })
+ : null;
\ No newline at end of file
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
index d6ecb6e..041d21d 100644
--- a/frontend/app/login/page.tsx
+++ b/frontend/app/login/page.tsx
@@ -32,29 +32,51 @@ export default function Login() {
return;
}
- setLoading(true);
- const { error } = await supabase.auth.signInWithPassword({
- email,
- password,
- });
- setLoading(false);
+ setLoading(true);
- if (error) showToast(error.message, "error");
- else router.push(redirectPath);
- };
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ console.log("LOGIN DATA:", data);
+ console.log("LOGIN ERROR:", error);
+
+ setLoading(false);
+
+ if (error) {
+ console.error(error);
+ alert(error.message);
+ showToast(error.message, "error");
+ return;
+ }
+
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ console.log("SESSION:", session);
+
+ if (session) {
+ router.push("/dashboard");
+ router.refresh();
+ }
+};
return (
);
}
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
index 038f5fd..7e1ce27 100644
--- a/frontend/middleware.ts
+++ b/frontend/middleware.ts
@@ -1,35 +1,5 @@
-import { NextRequest, NextResponse } from "next/server";
-
-const protectedRoutes = [
- "/dashboard",
- "/projects",
- "/workspace",
- "/chat",
- "/insights",
-];
-
-export function middleware(req: NextRequest) {
- const token =
- req.cookies.get("sb-access-token")?.value ||
- req.headers.get("authorization");
-
- const isProtectedRoute = protectedRoutes.some((route) =>
- req.nextUrl.pathname.startsWith(route)
- );
-
- if (isProtectedRoute && !token) {
- return NextResponse.redirect(new URL("/login", req.url));
- }
+import { NextResponse } from "next/server";
+export function middleware() {
return NextResponse.next();
-}
-
-export const config = {
- matcher: [
- "/dashboard/:path*",
- "/projects/:path*",
- "/workspace/:path*",
- "/chat/:path*",
- "/insights/:path*",
- ],
-};
\ No newline at end of file
+}
\ No newline at end of file