diff --git a/app/me/analytics/page.tsx b/app/me/analytics/page.tsx
index c1ff1b6d9..e6a082c9d 100644
--- a/app/me/analytics/page.tsx
+++ b/app/me/analytics/page.tsx
@@ -1,7 +1,137 @@
-import { redirect } from 'next/navigation';
+'use client';
-const page = () => {
- redirect('/coming-soon');
-};
+import { useMemo, useState } from 'react';
+import { motion } from 'framer-motion';
+import { useAuthStatus } from '@/hooks/use-auth';
+import { GetMeResponse } from '@/lib/api/types';
+import { Activity } from '@/types/user';
+import { AnalyticsBentoGrid } from '@/components/analytics/AnalyticsBentoGrid';
+import { AnalyticsChart } from '@/components/analytics/AnalyticsChart';
+import { AuthGuard } from '@/components/auth';
+import LoadingSpinner from '@/components/LoadingSpinner';
+import ActivityHeatmap from '@/components/profile/ActivityHeatMap';
+import ActivityFeed from '@/components/profile/ActivityFeed';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
-export default page;
+const FILTER_OPTIONS = [
+ 'All Time',
+ 'This Year',
+ 'This Month',
+ 'This Week',
+ 'Today',
+];
+
+function AnalyticsContent() {
+ const { user, isLoading } = useAuthStatus();
+ const [activityFilter, setActivityFilter] = useState('All Time');
+
+ const meData = useMemo(
+ () => (user?.profile as GetMeResponse | undefined) ?? null,
+ [user?.profile]
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!meData?.stats || !meData?.chart) {
+ return (
+
+ Analytics data unavailable.
+
+ );
+ }
+
+ const activities = (meData.user?.activities ?? []) as Activity[];
+
+ return (
+
+ {/* Page header — matches earnings page pattern */}
+
+ Analytics
+
+ Your personal growth dashboard across the platform.
+
+
+
+ {/* Bento stats grid */}
+
+
+ {/* Activity chart */}
+
+
+ {/* Activity heatmap — uses recentActivities as activity array */}
+
+
+ Contribution Graph
+ Your activity over the last year
+
+
+
+
+
+
+ {/* Recent activity feed */}
+
+
+
+ Recent Activity
+
+ What you have been up to on the platform
+
+
+
+ {/* Filter toggle — matches the pattern ActivityFeed expects */}
+
+ {FILTER_OPTIONS.map(f => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
+export default function AnalyticsPage() {
+ return (
+ Authenticating...}
+ >
+
+
+ );
+}
diff --git a/components/analytics/AnalyticsBentoGrid.tsx b/components/analytics/AnalyticsBentoGrid.tsx
new file mode 100644
index 000000000..066262990
--- /dev/null
+++ b/components/analytics/AnalyticsBentoGrid.tsx
@@ -0,0 +1,230 @@
+'use client';
+
+import { useMemo } from 'react';
+import { motion, Variants } from 'framer-motion';
+import {
+ TrendingUp,
+ TrendingDown,
+ Minus,
+ Users,
+ FolderGit2,
+ Trophy,
+ Star,
+ DollarSign,
+ MessageSquare,
+ ThumbsUp,
+ GitBranch,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { GetMeResponse } from '@/lib/api/types';
+import { calculateChartTrend, TrendResult } from '@/lib/utils/calculateTrend';
+
+interface Props {
+ stats: GetMeResponse['stats'];
+ chart: GetMeResponse['chart'];
+}
+
+function TrendBadge({ trend }: { trend: TrendResult }) {
+ if (trend.direction === 'up') {
+ return (
+
+
+ {trend.percentage}%
+
+ );
+ }
+ if (trend.direction === 'down') {
+ return (
+
+
+ {trend.percentage}%
+
+ );
+ }
+ return (
+
+
+ 0%
+
+ );
+}
+
+interface TileConfig {
+ label: string;
+ value: number;
+ icon: React.ReactNode;
+ trend: TrendResult;
+ colSpan: string;
+ rowSpan: string;
+ large?: boolean;
+}
+
+const containerVariants: Variants = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.07 } },
+};
+
+const tileVariants: Variants = {
+ hidden: { opacity: 0, y: 16 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.35, ease: [0.4, 0, 0.2, 1] as const },
+ },
+};
+
+export function AnalyticsBentoGrid({ stats, chart }: Props) {
+ const chartTrend = useMemo(() => calculateChartTrend(chart), [chart]);
+ const flat: TrendResult = { percentage: 0, direction: 'flat' };
+
+ const tiles: TileConfig[] = useMemo(
+ () => [
+ {
+ label: 'Global Reputation',
+ value: stats.reputation,
+ icon: ,
+ trend: chartTrend,
+ colSpan: 'col-span-2',
+ rowSpan: 'row-span-2',
+ large: true,
+ },
+ {
+ label: 'Community Score',
+ value: stats.communityScore,
+ icon: ,
+ trend: chartTrend,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Projects Created',
+ value: stats.projectsCreated,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Hackathons Entered',
+ value: stats.hackathons,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Followers',
+ value: stats.followers,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Total Contributed',
+ value: stats.totalContributed,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Comments Posted',
+ value: stats.commentsPosted,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Votes Cast',
+ value: stats.votes,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ {
+ label: 'Following',
+ value: stats.following,
+ icon: ,
+ trend: flat,
+ colSpan: 'col-span-1',
+ rowSpan: 'row-span-1',
+ },
+ ],
+ [stats, chartTrend, flat]
+ );
+
+ return (
+ <>
+ {/* Screen reader fallback table */}
+
+
+
+ | Metric |
+ Value |
+ Trend |
+
+
+
+ {tiles.map(t => (
+
+ | {t.label} |
+ {t.value.toLocaleString()} |
+
+ {t.trend.direction === 'flat'
+ ? 'No change'
+ : `${t.trend.direction === 'up' ? 'Up' : 'Down'} ${t.trend.percentage}%`}
+ |
+
+ ))}
+
+
+
+
+ {tiles.map(tile => (
+
+
+
+
+ {tile.icon}
+
+
+
+
+
+ {tile.value.toLocaleString()}
+
+
+ {tile.label}
+
+
+
+
+ ))}
+
+ >
+ );
+}
diff --git a/components/analytics/AnalyticsChart.tsx b/components/analytics/AnalyticsChart.tsx
new file mode 100644
index 000000000..6748a85ab
--- /dev/null
+++ b/components/analytics/AnalyticsChart.tsx
@@ -0,0 +1,184 @@
+'use client';
+
+import { useMemo, useState } from 'react';
+import {
+ ResponsiveContainer,
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ TooltipProps,
+} from 'recharts';
+import { motion } from 'framer-motion';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { GetMeResponse } from '@/lib/api/types';
+import {
+ transformChartData,
+ filterChartByRange,
+} from '@/lib/utils/calculateTrend';
+
+type Range = '7D' | '30D' | '90D' | 'ALL';
+const RANGE_DAYS: Record = {
+ '7D': 7,
+ '30D': 30,
+ '90D': 90,
+ ALL: 'ALL',
+};
+
+interface Props {
+ chart: GetMeResponse['chart'];
+}
+
+function CustomTooltip({
+ active,
+ payload,
+ label,
+}: TooltipProps) {
+ if (!active || !payload?.length) return null;
+ return (
+
+
{label}
+
+ {payload[0].value}{' '}
+ activities
+
+
+ );
+}
+
+export function AnalyticsChart({ chart }: Props) {
+ const [range, setRange] = useState('30D');
+ const ranges: Range[] = ['7D', '30D', '90D', 'ALL'];
+
+ const allTransformed = useMemo(() => transformChartData(chart), [chart]);
+
+ const filtered = useMemo(
+ () => filterChartByRange(allTransformed, RANGE_DAYS[range]),
+ [allTransformed, range]
+ );
+
+ return (
+
+
+
+
+ Activity Over Time
+ Your platform activity trend
+
+
+
+ {ranges.map(r => (
+
+ ))}
+
+
+
+
+ {filtered.length === 0 ? (
+
+ No activity data available for this period.
+
+ ) : (
+ <>
+ {/* Screen reader fallback */}
+
+
+
+ | Date |
+ Activity Count |
+
+
+
+ {filtered.map(d => (
+
+ | {d.label} |
+ {d.count} |
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/lib/utils/calculateTrend.ts b/lib/utils/calculateTrend.ts
new file mode 100644
index 000000000..1f02c913d
--- /dev/null
+++ b/lib/utils/calculateTrend.ts
@@ -0,0 +1,65 @@
+export interface TrendResult {
+ percentage: number;
+ direction: 'up' | 'down' | 'flat';
+}
+
+export function calculateTrend(
+ current: number | null | undefined,
+ previous: number | null | undefined
+): TrendResult {
+ const curr = current ?? 0;
+ const prev = previous ?? 0;
+
+ if (prev === 0) {
+ if (curr === 0) return { percentage: 0, direction: 'flat' };
+ return { percentage: 100, direction: 'up' };
+ }
+
+ const percentage = ((curr - prev) / prev) * 100;
+ const rounded = Math.round(percentage * 10) / 10;
+
+ if (rounded > 0) return { percentage: rounded, direction: 'up' };
+ if (rounded < 0) return { percentage: Math.abs(rounded), direction: 'down' };
+ return { percentage: 0, direction: 'flat' };
+}
+
+export function calculateChartTrend(
+ data: Array<{ date: string; count: number }> | null | undefined
+): TrendResult {
+ if (!data || data.length === 0) return { percentage: 0, direction: 'flat' };
+ const mid = Math.floor(data.length / 2);
+ const previous = data
+ .slice(0, mid)
+ .reduce((sum, d) => sum + (d.count ?? 0), 0);
+ const current = data.slice(mid).reduce((sum, d) => sum + (d.count ?? 0), 0);
+ return calculateTrend(current, previous);
+}
+
+/** Transforms raw API chart data into Recharts-compatible shape */
+export function transformChartData(
+ data: Array<{ date: string; count: number }> | null | undefined
+): Array<{ date: string; count: number; label: string }> {
+ if (!data || data.length === 0) return [];
+ return [...data]
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
+ .map(d => ({
+ date: d.date,
+ count: d.count ?? 0,
+ label: new Date(d.date).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ }),
+ }));
+}
+
+/** Filters transformed chart data by day range */
+export function filterChartByRange(
+ data: Array<{ date: string; count: number; label: string }>,
+ days: number | 'ALL'
+): Array<{ date: string; count: number; label: string }> {
+ if (days === 'ALL' || data.length === 0) return data;
+ const cutoff = new Date();
+ cutoff.setDate(cutoff.getDate() - days);
+ const filtered = data.filter(d => new Date(d.date) >= cutoff);
+ return filtered.length > 0 ? filtered : data.slice(-days);
+}