diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx new file mode 100644 index 000000000..f599da39f --- /dev/null +++ b/app/me/earnings/page.tsx @@ -0,0 +1,271 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { + IconCurrencyDollar, + IconClock, + IconCheck, + IconTrophy, + IconBriefcase, + IconUsers, + IconTarget, +} from '@tabler/icons-react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + getUserEarnings, + EarningsData, + EarningActivity, +} from '@/lib/api/user/earnings'; +import { toast } from 'sonner'; + +/** + * Interface for SummaryCard props. + */ +interface SummaryCardProps { + title: string; + value: string; + icon: React.ReactNode; + description: string; +} + +/** + * SummaryCard component for displaying high-level stats. + */ +const SummaryCard: React.FC = ({ + title, + value, + icon, + description, +}) => ( + + + {title} + {icon} + + +
{value}
+

{description}

+
+
+); + +/** + * Interface for BreakdownItem props. + */ +interface BreakdownItemProps { + label: string; + value: number; + icon: React.ReactNode; +} + +/** + * BreakdownItem component for showing source-specific earnings. + */ +const BreakdownItem: React.FC = ({ + label, + value, + icon, +}) => ( +
+
+
+ {icon} +
+ {label} +
+ ${value.toLocaleString()} +
+); + +/** + * Interface for ActivityItem props. + */ +interface ActivityItemProps { + activity: EarningActivity; +} + +/** + * ActivityItem component for displaying a single reward entry. + */ +const ActivityItem: React.FC = ({ activity }) => ( +
+
+
{activity.title}
+
+ {activity.source} + + {new Date(activity.occurredAt).toLocaleDateString()} +
+
+
+

${activity.amount.toLocaleString()}

+ {activity.currency && ( +

{activity.currency}

+ )} +
+
+); + +/** + * EarningsSkeleton component for loading states. + */ +const EarningsSkeleton: React.FC = () => ( +
+
+ + +
+
+ + + +
+
+ + +
+
+); + +/** + * EarningsPage component for managing and tracking user rewards. + */ +const EarningsPage: React.FC = () => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await getUserEarnings(); + if (res.success && res.data) { + setData(res.data); + } + } catch (error) { + console.error('Failed to fetch earnings:', error); + toast.error('Failed to load earnings data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + if (loading) { + return ; + } + + if (!data) { + return ( +
+

+ No earnings data found. +

+
+ ); + } + + return ( +
+ +

+ Earnings Dashboard +

+

+ Manage and track your rewards across the platform. +

+
+ + {/* Summary Cards */} +
+ } + description='Lifetime earnings' + /> + } + description='Awaiting processing' + /> + } + description='Successfully cashed out' + /> +
+ +
+ {/* Breakdown */} + + + Source Breakdown + + Earnings categorized by activity type + + + + } + /> + } + /> + } + /> + } + /> + + + + {/* Activity Feed */} + + + Recent Activity + Your latest wins and rewards + + +
+ {data.activities.length === 0 ? ( +

+ No recent activity found. +

+ ) : ( + data.activities.map(activity => ( + + )) + )} +
+
+
+
+
+ ); +}; + +export default EarningsPage; diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index ad72176a8..6f0bf025e 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { IconBell, IconChartBar, + IconCurrencyDollar, IconDashboard, IconFileText, IconFolder, @@ -39,6 +40,11 @@ const navigationData = { url: '/me/analytics', icon: IconChartBar, }, + { + title: 'Earnings', + url: '/me/earnings', + icon: IconCurrencyDollar, + }, ], projects: [ { diff --git a/lib/api/user/earnings.ts b/lib/api/user/earnings.ts new file mode 100644 index 000000000..a86d7dc13 --- /dev/null +++ b/lib/api/user/earnings.ts @@ -0,0 +1,64 @@ +import { api } from '../api'; +import { ApiResponse } from '../types'; + +export interface EarningActivity { + id: string; + source: 'hackathons' | 'grants' | 'crowdfunding' | 'bounties'; + title: string; + amount: number; + currency: string; + occurredAt: string; +} + +export interface EarningsData { + summary: { + totalEarned: number; + pendingWithdrawal: number; + completedWithdrawal: number; + }; + breakdown: { + hackathons: number; + grants: number; + crowdfunding: number; + bounties: number; + }; + activities: EarningActivity[]; +} + +export interface GetEarningsResponse extends ApiResponse { + success: true; + data: EarningsData; +} + +export interface ClaimEarningRequest { + activityId: string; +} + +export interface ClaimEarningResponse extends ApiResponse { + success: boolean; + message: string; + data?: { + transactionHash: string; + }; +} + +/** + * Get user earnings data + */ +export const getUserEarnings = async (): Promise => { + const res = await api.get('/user/earnings'); + return res.data; +}; + +/** + * Claim a specific earning + */ +export const claimEarning = async ( + data: ClaimEarningRequest +): Promise => { + const res = await api.post( + '/user/earnings/claim', + data + ); + return res.data; +};