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
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ yarn-error.log*
.env*
!.env.example


# vercel
.vercel

Expand Down
26 changes: 22 additions & 4 deletions frontend/app/components/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import CommandPalette from "./CommandPalette";
import NotificationsPanel from "./NotificationsPanel";

import { usePathname } from "next/navigation";
import Sidebar from "./Sidebar";
Expand All @@ -19,14 +20,31 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
: "min-w-0 flex-1 overflow-x-hidden p-6";

return (
<div className="flex min-h-screen relative">
<div className="flex min-h-screen relative w-full">
<CommandPalette />

{!hideSidebar && <Sidebar />}

<main className={mainClass}>
{children}
</main>
<div className="flex-1 flex flex-col">
{!isPublicRoute && (
<div
className="
sticky top-0 z-40
flex items-center justify-end
border-b border-outline-variant
bg-white/80
px-6 py-4
backdrop-blur-xl
"
>
<NotificationsPanel />
</div>
)}

<main className={mainClass}>
{children}
</main>
</div>
</div>
);
}
229 changes: 229 additions & 0 deletions frontend/app/components/NotificationsPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>(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 (
<div className="relative" ref={panelRef}>
<button
onClick={() => 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"
>
<span className="material-symbols-outlined">
notifications
</span>

{unreadCount > 0 && (
<span
className="
absolute -right-1 -top-1
flex h-5 min-w-5 items-center justify-center
rounded-full bg-primary px-1
text-[10px] font-bold text-white
"
>
{unreadCount}
</span>
)}
</button>

{open && (
<div
className="
absolute right-0 top-14 z-50
w-[380px]
overflow-hidden rounded-2xl
border border-white/20
bg-white
backdrop-blur-xl
shadow-2xl
animate-in fade-in zoom-in-95
"
>
<div className="flex items-center justify-between border-b border-outline-variant px-5 py-4">
<div>
<h3 className="font-semibold text-on-surface">
Notifications
</h3>

<p className="text-xs text-outline">
Recent activity and updates
</p>
</div>

<button
onClick={markAllAsRead}
className="
text-xs text-primary
hover:underline
"
>
Mark all as read
</button>
</div>

<div className="max-h-[420px] overflow-y-auto">
{notifications.length === 0 ? (
<div className="flex flex-col items-center justify-center px-6 py-14 text-center">
<span className="material-symbols-outlined text-5xl text-slate-300">
notifications_off
</span>

<h4 className="mt-4 text-sm font-semibold text-slate-700">
No notifications yet
</h4>

<p className="mt-1 text-xs text-slate-500">
You're all caught up.
</p>
</div>
) : (
notifications.map((notification) => (
<button
key={notification.id}
onClick={() => {
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"
}
`}
>
<div
className={`
mt-1 h-2.5 w-2.5 rounded-full
${
notification.unread
? "bg-primary"
: "bg-slate-300"
}
`}
/>

<div className="flex-1">
<div className="flex items-center justify-between gap-4">
<h4 className="text-sm font-semibold text-on-surface">
{notification.title}
</h4>

<span className="text-[11px] text-outline whitespace-nowrap">
{notification.time}
</span>
</div>

<p className="mt-1 text-sm text-on-surface-variant">
{notification.description}
</p>
</div>
</button>
))
)}
</div>
</div>
)}
</div>
);
}
6 changes: 3 additions & 3 deletions frontend/app/components/analytics-comp/AnalyticsHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import Link from "next/link";
import NotificationsPanel from "@/app/components/NotificationsPanel";

type AnalyticsHeaderProps = {
sprint: string;
Expand Down Expand Up @@ -60,9 +61,8 @@ export default function AnalyticsHeader({
</div>

<div className="flex items-center gap-4">
<button className="p-2 text-on-surface-variant hover:bg-surface-container-low rounded-full transition-all duration-200 active:scale-90" type="button">
<span className="material-symbols-outlined">notifications</span>
</button>
<NotificationsPanel />

<button className="p-2 text-on-surface-variant hover:bg-surface-container-low rounded-full transition-all duration-200 active:scale-90" type="button">
<span className="material-symbols-outlined">settings</span>
</button>
Expand Down
11 changes: 9 additions & 2 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 13 additions & 7 deletions frontend/app/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -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;
export const supabase =
isSupabaseConfigured
? createClient(supabaseUrl!, supabaseKey!, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true,
},
})
: null;
Loading
Loading