From 913bd7dd8eb74054a35f333ce99a6eacb02579aa Mon Sep 17 00:00:00 2001 From: aahnik Date: Wed, 12 Mar 2025 23:59:51 +0530 Subject: [PATCH 1/4] Make sidebar dynamic based on current user role --- components/app-sidebar.tsx | 150 +++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index d40c01d..1e5300c 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -64,40 +64,6 @@ import Link from "next/link"; import { AuthContext } from "@/contexts/auth-context"; import { notFound } from "next/navigation"; -const data = { - navMain: [ - { - title: "Users", - url: "users", - icon: Users, - }, - { - title: "Groups", - url: "groups", - icon: Contact, - items: [], - }, - { - title: "Contests", - url: "contests", - icon: Trophy, - items: [], - }, - { - title: "Problems", - url: "problems", - icon: FileCode, - items: [], - }, - { - title: "Submissions", - url: "submissions", - icon: FileCheck, - items: [], - }, - ], -}; - const defaultUser = { name: "shadcn", email: "m@example.com", @@ -193,18 +159,57 @@ function SidebarSkeleton({ children }: { children: React.ReactNode }) { ); } +// First, let's remove the static data object and make it a function +// that returns filtered items based on role +const getNavItems = (role?: string) => { + const allItems = [ + { + title: "Users", + url: "users", + icon: Users, + allowedRoles: ["owner", "admin"], + }, + { + title: "Groups", + url: "groups", + icon: Contact, + allowedRoles: ["owner"], + items: [], + }, + { + title: "Contests", + url: "contests", + icon: Trophy, + allowedRoles: ["owner", "admin", "member"], + items: [], + }, + { + title: "Problems", + url: "problems", + icon: FileCode, + allowedRoles: ["owner", "admin"], + items: [], + }, + { + title: "Submissions", + url: "submissions", + icon: FileCheck, + allowedRoles: ["owner", "admin"], + items: [], + }, + ]; + + if (!role) return []; + return allItems.filter((item) => item.allowedRoles.includes(role)); +}; + export function AppSidebar({ children }: { children: React.ReactNode }) { const { logout, user, isAuthenticated, isLoading } = useContext(AuthContext); const pathname = usePathname(); const router = useRouter(); const pathName = usePathname(); - - // Get orgId from URL const orgId = pathname.split("/")[1]; - console.log("orgId", orgId); - - // Transform user orgs into teams format or use default teams const teams = useMemo(() => { if (user?.orgs && user.orgs.length > 0) { return user.orgs.map((org) => ({ @@ -215,13 +220,13 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { return []; }, [user?.orgs]); - console.log("teams in sidebar", teams); + const [activeTeam, setActiveTeam] = useState<(typeof teams)[0] | null>(() => { + const teamFromUrl = teams.find((team) => team.nameId === orgId); + return teamFromUrl || null; + }); - // Check if the orgId exists in teams, if not return 404 + // Move this useEffect up here with other hooks useEffect(() => { - console.log("404 check - orgId:", orgId, "type:", typeof orgId); - console.log("404 check - teams:", teams); - console.log("404 check - teams.length:", teams.length); if ( isAuthenticated && orgId && @@ -230,17 +235,6 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { ) { notFound(); } - console.log("WTF"); - }, [orgId, teams, isAuthenticated]); - - // Set active team based on URL orgId - const [activeTeam, setActiveTeam] = useState<(typeof teams)[0] | null>(() => { - const teamFromUrl = teams.find((team) => team.nameId === orgId); - return teamFromUrl || null; - }); - - // Update activeTeam when URL changes or teams load - useEffect(() => { const teamFromUrl = teams.find((team) => team.nameId === orgId); if ( teamFromUrl && @@ -248,26 +242,18 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { ) { setActiveTeam(teamFromUrl); } - }, [orgId, teams, activeTeam]); + }, [orgId, teams, activeTeam, isAuthenticated]); - // Handle team change with URL update - const handleTeamChange = (team: (typeof teams)[0]) => { - setActiveTeam(team); - // Get the current path segments after the org ID - const pathSegments = pathname.split("/").slice(2); - // Construct new path with new org ID and maintain the rest of the path - const newPath = `/${team.nameId}${pathSegments.length ? "/" + pathSegments.join("/") : ""}`; - router.push(newPath); - }; + const currentOrgRole = useMemo(() => { + if (!activeTeam) return undefined; + return activeTeam.role; + }, [activeTeam]); - const handleLogout = () => { - logout(); - }; - - console.log("NEXT_PUBLIC_DEBUG", process.env.NEXT_PUBLIC_DEBUG); - console.log("user", user); - console.log("isLoading", isLoading); + const navItems = useMemo(() => { + return getNavItems(currentOrgRole); + }, [currentOrgRole]); + // Conditional returns can now come after all hooks if (isLoading) { return {children}; } @@ -295,6 +281,24 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { })); }; + // Handle team change with URL update + const handleTeamChange = (team: (typeof teams)[0]) => { + setActiveTeam(team); + // Get the current path segments after the org ID + const pathSegments = pathname.split("/").slice(2); + // Construct new path with new org ID and maintain the rest of the path + const newPath = `/${team.nameId}${pathSegments.length ? "/" + pathSegments.join("/") : ""}`; + router.push(newPath); + }; + + const handleLogout = () => { + logout(); + }; + + console.log("NEXT_PUBLIC_DEBUG", process.env.NEXT_PUBLIC_DEBUG); + console.log("user", user); + console.log("isLoading", isLoading); + return ( @@ -367,7 +371,7 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { Main Menu - {data.navMain.map((item) => { + {navItems.map((item) => { const isActive = pathname.includes(`${basePath}/${item.url}`); return ( From 41dd3eb18713e1a491e9d5936e1018c8f0627c83 Mon Sep 17 00:00:00 2001 From: aahnik Date: Thu, 13 Mar 2025 00:05:09 +0530 Subject: [PATCH 2/4] Show the role badge for org --- components/app-sidebar.tsx | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 1e5300c..396af37 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -203,6 +203,25 @@ const getNavItems = (role?: string) => { return allItems.filter((item) => item.allowedRoles.includes(role)); }; +// Add this new component near the top of the file +function RoleBadge({ role }: { role: string }) { + const colors = { + owner: "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary", + admin: + "bg-secondary/20 text-secondary-foreground dark:bg-secondary/30 dark:text-secondary-foreground", + member: + "bg-muted text-muted-foreground dark:bg-muted/50 dark:text-muted-foreground", + }; + + return ( + + {role} + + ); +} + export function AppSidebar({ children }: { children: React.ReactNode }) { const { logout, user, isAuthenticated, isLoading } = useContext(AuthContext); const pathname = usePathname(); @@ -317,11 +336,14 @@ export function AppSidebar({ children }: { children: React.ReactNode }) {
-
- - {activeTeam.name} - - +
+
+ + {activeTeam.name} + + +
+ {activeTeam.nameId}
@@ -347,8 +369,10 @@ export function AppSidebar({ children }: { children: React.ReactNode }) {
- {team.name} - {team.role} +
+ {team.name} + +
))} From c39867e84370a62c03f34f3b96cdcae0c8d001af Mon Sep 17 00:00:00 2001 From: aahnik Date: Thu, 13 Mar 2025 00:14:23 +0530 Subject: [PATCH 3/4] Add ComingSoonBadge component and update sidebar items to indicate upcoming features --- components/app-sidebar.tsx | 41 ++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 396af37..18e1711 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -159,6 +159,15 @@ function SidebarSkeleton({ children }: { children: React.ReactNode }) { ); } +// Add this new component near the RoleBadge component +function ComingSoonBadge() { + return ( + + coming soon + + ); +} + // First, let's remove the static data object and make it a function // that returns filtered items based on role const getNavItems = (role?: string) => { @@ -174,6 +183,8 @@ const getNavItems = (role?: string) => { url: "groups", icon: Contact, allowedRoles: ["owner"], + disabled: true, + comingSoon: true, items: [], }, { @@ -400,16 +411,30 @@ export function AppSidebar({ children }: { children: React.ReactNode }) { return ( - - {item.icon && } - {item.title} - + {item.disabled ? ( +
+ {item.icon && } +
+ {item.title} + {item.comingSoon && } +
+
+ ) : ( + + {item.icon && } + {item.title} + + )}
); From 58e283713f16e17d4a8c98dd40be43ffb2b91411 Mon Sep 17 00:00:00 2001 From: aahnik Date: Thu, 13 Mar 2025 00:46:32 +0530 Subject: [PATCH 4/4] Implement organization statistics fetching and display in GraphicsPage --- app/[orgId]/page.tsx | 131 +++++++++++++++++++---------------------- hooks/use-org-stats.ts | 101 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 70 deletions(-) create mode 100644 hooks/use-org-stats.ts diff --git a/app/[orgId]/page.tsx b/app/[orgId]/page.tsx index fc82722..bd74c0f 100644 --- a/app/[orgId]/page.tsx +++ b/app/[orgId]/page.tsx @@ -1,81 +1,72 @@ "use client"; -import Charts from "@/components/statstable"; +import { useContext } from "react"; +import { AuthContext } from "@/contexts/auth-context"; +import { useOrgStats } from "@/hooks/use-org-stats"; +import { useParams } from "next/navigation"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import StatCard from "@/components/stat-card"; -interface ChartData { - submissions: { - total: number; - weeklyData: Array<{ - date: string; - steps: number; - }>; - weeklyTotal: number; - }; - upcomingContest: { - count: number; - variability: number; - weeklyData: Array<{ - date: string; - resting: number; - }>; - }; - contestDetails: { - currentYear: { - year: string; - contestsPerDay: number; - }; - previousYear: { - year: string; - contestsPerDay: number; - }; - }; +function StatsLoadingSkeleton() { + return ( +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ + +
+ ))} +
+
+ ); } export default function GraphicsPage() { - const chartData: ChartData = { - submissions: { - total: 12584, - weeklyData: [ - { date: "2024-01-01", steps: 2000 }, - { date: "2024-01-02", steps: 2100 }, - { date: "2024-01-03", steps: 2200 }, - { date: "2024-01-04", steps: 2300 }, - { date: "2024-01-05", steps: 2400 }, - { date: "2024-01-06", steps: 2500 }, - { date: "2024-01-07", steps: 2600 }, - // ... more data - ], - weeklyTotal: 5305, - }, - upcomingContest: { - count: 62, - variability: 35, - weeklyData: [ - { date: "2024-01-01", resting: 62 }, - { date: "2024-01-02", resting: 63 }, - { date: "2024-01-03", resting: 64 }, - { date: "2024-01-04", resting: 65 }, - { date: "2024-01-05", resting: 66 }, - { date: "2024-01-06", resting: 67 }, - { date: "2024-01-07", resting: 68 }, + const { user } = useContext(AuthContext); + const params = useParams(); + const orgId = params.orgId as string; + + const currentOrg = user?.orgs.find((org) => org.nameId === orgId); + const { stats, loading, error } = useOrgStats(orgId, currentOrg?.role); - // ... more data - ], - }, - contestDetails: { - currentYear: { - year: "2024", - contestsPerDay: 12453, - }, - previousYear: { - year: "2023", - contestsPerDay: 10103, - }, - }, - }; + if (loading) return ; + if (error) + return ( + + + Error + {error} + + ); return ( -
- +
+
+ + + + {currentOrg?.role === "owner" && ( + + )} +
); } diff --git a/hooks/use-org-stats.ts b/hooks/use-org-stats.ts new file mode 100644 index 0000000..c3b8fc4 --- /dev/null +++ b/hooks/use-org-stats.ts @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { fetchApi } from "@/lib/client/fetch"; + +type Period = "day" | "week" | "month" | "year"; + +interface StatsResponse { + value: number; +} + +interface StatConfig { + key: keyof typeof defaultStats; + stat: string; + period?: Period; + roleRequired?: string[]; +} + +const defaultStats = { + totalContests: 0, + upcomingContests: 0, + endedContests: 0, + totalMembers: 0, + totalProblems: 0, + totalGroups: 0, +}; + +const statsConfig: StatConfig[] = [ + { + key: "totalContests", + stat: "total-contests", + roleRequired: ["owner", "organizer", "member"], + }, + { + key: "upcomingContests", + stat: "upcoming-contests", + roleRequired: ["owner", "organizer", "member"], + }, + { + key: "endedContests", + stat: "ended-contests", + roleRequired: ["owner", "organizer", "member"], + }, + { + key: "totalMembers", + stat: "total-members", + roleRequired: ["owner"], + }, + { + key: "totalProblems", + stat: "total-problems", + roleRequired: ["owner", "organizer", "member"], + }, + { + key: "totalGroups", + stat: "total-groups", + roleRequired: ["owner", "organizer", "member"], + }, +]; + +export function useOrgStats(orgId: string, currentRole?: string) { + const [stats, setStats] = useState(defaultStats); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStats = async () => { + try { + // Filter stats based on user's role + const allowedStats = statsConfig.filter((config) => + config.roleRequired?.includes(currentRole || ""), + ); + + // Create array of promises for allowed stats + const responses = await Promise.all( + allowedStats.map(({ stat, period }) => + fetchApi( + `/orgs/${orgId}/stats?stat=${stat}${period ? `&period=${period}` : ""}`, + ), + ), + ); + + // Combine responses into stats object + const newStats = { ...defaultStats }; + allowedStats.forEach(({ key }, index) => { + newStats[key] = responses[index].value; + }); + + setStats(newStats); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch stats"); + } finally { + setLoading(false); + } + }; + + if (orgId) { + fetchStats(); + } + }, [orgId, currentRole]); + + return { stats, loading, error }; +}