From 74c438f28920a59be5e354cfaa38ce354050d8e0 Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sat, 28 Feb 2026 23:02:29 +0800 Subject: [PATCH 1/9] Atlas-Bounty: Implement Private Earnings Dashboard --- app/me/earnings/page.tsx | 221 +++++++++++++++++++++++++++++++++++++ components/app-sidebar.tsx | 6 + lib/api/user/earnings.ts | 58 ++++++++++ 3 files changed, 285 insertions(+) create mode 100644 app/me/earnings/page.tsx create mode 100644 lib/api/user/earnings.ts diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx new file mode 100644 index 000000000..fb49a40fb --- /dev/null +++ b/app/me/earnings/page.tsx @@ -0,0 +1,221 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { + IconCurrencyDollar, + IconClock, + IconCheck, + IconArrowUpRight, + IconTrophy, + IconBriefcase, + IconUsers, + IconTarget +} from '@tabler/icons-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { getUserEarnings, claimEarning, EarningsData, EarningActivity } from '@/lib/api/user/earnings'; +import { toast } from 'sonner'; + +export default function EarningsPage() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + const [claimingId, setClaimingId] = 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(); + }, []); + + const handleClaim = async (id: string) => { + setClaimingId(id); + try { + const res = await claimEarning({ activityId: id }); + if (res.success) { + toast.success('Claim successful!'); + // Refresh data + const updated = await getUserEarnings(); + if (updated.data) setData(updated.data); + } else { + toast.error(res.message || 'Claim failed'); + } + } catch (error) { + toast.error('An error occurred during claiming'); + } finally { + setClaimingId(null); + } + }; + + if (loading) { + return ; + } + + if (!data) return
No 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) => ( + handleClaim(activity.id)} + isClaiming={claimingId === activity.id} + /> + )) + )} +
+
+
+
+
+ ); +} + +function SummaryCard({ title, value, icon, description }: { title: string, value: string, icon: React.ReactNode, description: string }) { + return ( + + + {title} + {icon} + + +
{value}
+

{description}

+
+
+ ); +} + +function BreakdownItem({ label, value, icon }: { label: string, value: number, icon: React.ReactNode }) { + return ( +
+
+
+ {icon} +
+ {label} +
+ ${value.toLocaleString()} +
+ ); +} + +function ActivityItem({ activity, onClaim, isClaiming }: { activity: EarningActivity, onClaim: () => void, isClaiming: boolean }) { + return ( +
+
+

+ {activity.title} + + {activity.status} + +

+
+ {activity.source} + + {new Date(activity.createdAt).toLocaleDateString()} +
+
+
+

${activity.amount.toLocaleString()}

+ {activity.status === 'claimable' && ( + + )} +
+
+ ); +} + +function EarningsSkeleton() { + return ( +
+
+ + +
+
+ + + +
+
+ + +
+
+ ); +} 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..824fb6d55 --- /dev/null +++ b/lib/api/user/earnings.ts @@ -0,0 +1,58 @@ +import api from '../api'; +import { ApiResponse } from '../types'; + +export interface EarningActivity { + id: string; + source: 'hackathon' | 'grant' | 'crowdfund' | 'bounty'; + title: string; + amount: number; + currency: string; + status: 'pending' | 'completed' | 'claimable'; + createdAt: string; +} + +export interface EarningsData { + 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; +}; From 9461e9b66ce3ab219905383e0fb446f07582c10a Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sat, 28 Feb 2026 23:09:21 +0800 Subject: [PATCH 2/9] Atlas-Bounty: Address review feedback for Earnings Dashboard --- app/me/earnings/page.tsx | 23 +++++++++++++++++------ lib/api/user/earnings.ts | 6 +++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx index fb49a40fb..3d083c052 100644 --- a/app/me/earnings/page.tsx +++ b/app/me/earnings/page.tsx @@ -19,7 +19,10 @@ import { Skeleton } from '@/components/ui/skeleton'; import { getUserEarnings, claimEarning, EarningsData, EarningActivity } from '@/lib/api/user/earnings'; import { toast } from 'sonner'; -export default function EarningsPage() { +/** + * EarningsPage component for managing and tracking user rewards. + */ +const EarningsPage: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [claimingId, setClaimingId] = useState(null); @@ -41,6 +44,9 @@ export default function EarningsPage() { fetchData(); }, []); + /** + * Handles the claiming of a specific earning activity. + */ const handleClaim = async (id: string) => { setClaimingId(id); try { @@ -49,11 +55,14 @@ export default function EarningsPage() { toast.success('Claim successful!'); // Refresh data const updated = await getUserEarnings(); - if (updated.data) setData(updated.data); + if (updated.success && updated.data) { + setData(updated.data); + } } else { toast.error(res.message || 'Claim failed'); } } catch (error) { + console.error('Failed to claim earning:', error); toast.error('An error occurred during claiming'); } finally { setClaimingId(null); @@ -140,7 +149,7 @@ export default function EarningsPage() { ); -} +}; function SummaryCard({ title, value, icon, description }: { title: string, value: string, icon: React.ReactNode, description: string }) { return ( @@ -175,12 +184,12 @@ function ActivityItem({ activity, onClaim, isClaiming }: { activity: EarningActi return (
-

+

{activity.title} - + {activity.status} -

+
{activity.source} @@ -219,3 +228,5 @@ function EarningsSkeleton() {
); } + +export default EarningsPage; diff --git a/lib/api/user/earnings.ts b/lib/api/user/earnings.ts index 824fb6d55..3f33e318a 100644 --- a/lib/api/user/earnings.ts +++ b/lib/api/user/earnings.ts @@ -1,4 +1,4 @@ -import api from '../api'; +import { api } from '../api'; import { ApiResponse } from '../types'; export interface EarningActivity { @@ -45,7 +45,7 @@ export interface ClaimEarningResponse extends ApiResponse { * Get user earnings data */ export const getUserEarnings = async (): Promise => { - const res = await api.get('/user/earnings'); + const res = await api.get('/user/earnings'); return res.data; }; @@ -53,6 +53,6 @@ export const getUserEarnings = async (): Promise => { * Claim a specific earning */ export const claimEarning = async (data: ClaimEarningRequest): Promise => { - const res = await api.post('/user/earnings/claim', data); + const res = await api.post('/user/earnings/claim', data); return res.data; }; From 7476ee49bf2102f88b134d5081bb6a382503f4fa Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sat, 28 Feb 2026 23:14:22 +0800 Subject: [PATCH 3/9] Atlas-Bounty: Refactor components to const arrow functions and improve empty states --- app/me/earnings/page.tsx | 196 +++++++++++++++++++++++---------------- 1 file changed, 117 insertions(+), 79 deletions(-) diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx index 3d083c052..5b74d1ff8 100644 --- a/app/me/earnings/page.tsx +++ b/app/me/earnings/page.tsx @@ -19,6 +19,116 @@ import { Skeleton } from '@/components/ui/skeleton'; import { getUserEarnings, claimEarning, 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; + onClaim: () => void; + isClaiming: boolean; +} + +/** + * ActivityItem component for displaying a single reward entry. + */ +const ActivityItem: React.FC = ({ activity, onClaim, isClaiming }) => ( +
+
+
+ {activity.title} + + {activity.status} + +
+
+ {activity.source} + + {new Date(activity.createdAt).toLocaleDateString()} +
+
+
+

${activity.amount.toLocaleString()}

+ {activity.status === 'claimable' && ( + + )} +
+
+); + +/** + * EarningsSkeleton component for loading states. + */ +const EarningsSkeleton: React.FC = () => ( +
+
+ + +
+
+ + + +
+
+ + +
+
+); + /** * EarningsPage component for managing and tracking user rewards. */ @@ -73,7 +183,13 @@ const EarningsPage: React.FC = () => { return ; } - if (!data) return
No data found.
; + if (!data) { + return ( +
+

No earnings data found.

+
+ ); + } return (
@@ -151,82 +267,4 @@ const EarningsPage: React.FC = () => { ); }; -function SummaryCard({ title, value, icon, description }: { title: string, value: string, icon: React.ReactNode, description: string }) { - return ( - - - {title} - {icon} - - -
{value}
-

{description}

-
-
- ); -} - -function BreakdownItem({ label, value, icon }: { label: string, value: number, icon: React.ReactNode }) { - return ( -
-
-
- {icon} -
- {label} -
- ${value.toLocaleString()} -
- ); -} - -function ActivityItem({ activity, onClaim, isClaiming }: { activity: EarningActivity, onClaim: () => void, isClaiming: boolean }) { - return ( -
-
-
- {activity.title} - - {activity.status} - -
-
- {activity.source} - - {new Date(activity.createdAt).toLocaleDateString()} -
-
-
-

${activity.amount.toLocaleString()}

- {activity.status === 'claimable' && ( - - )} -
-
- ); -} - -function EarningsSkeleton() { - return ( -
-
- - -
-
- - - -
-
- - -
-
- ); -} - export default EarningsPage; From 50ddce62276e563a147a1de395cfe66f2a428476 Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Sat, 28 Feb 2026 20:58:59 +0100 Subject: [PATCH 4/9] refactor: Earnings Dashboard: Simplify ActivityItem component, remove claim functionality, and update earnings data structure for improved clarity and performance. --- app/me/earnings/page.tsx | 241 ++++++++++++++++++++------------------- lib/api/user/earnings.ts | 22 ++-- 2 files changed, 135 insertions(+), 128 deletions(-) diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx index 5b74d1ff8..f599da39f 100644 --- a/app/me/earnings/page.tsx +++ b/app/me/earnings/page.tsx @@ -2,21 +2,28 @@ import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; -import { - IconCurrencyDollar, - IconClock, - IconCheck, - IconArrowUpRight, +import { + IconCurrencyDollar, + IconClock, + IconCheck, IconTrophy, IconBriefcase, IconUsers, - IconTarget + IconTarget, } from '@tabler/icons-react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; -import { getUserEarnings, claimEarning, EarningsData, EarningActivity } from '@/lib/api/user/earnings'; +import { + getUserEarnings, + EarningsData, + EarningActivity, +} from '@/lib/api/user/earnings'; import { toast } from 'sonner'; /** @@ -32,15 +39,20 @@ interface SummaryCardProps { /** * SummaryCard component for displaying high-level stats. */ -const SummaryCard: React.FC = ({ title, value, icon, description }) => ( +const SummaryCard: React.FC = ({ + title, + value, + icon, + description, +}) => ( - - {title} + + {title} {icon} -
{value}
-

{description}

+
{value}
+

{description}

); @@ -57,15 +69,19 @@ interface BreakdownItemProps { /** * BreakdownItem component for showing source-specific earnings. */ -const BreakdownItem: React.FC = ({ label, value, icon }) => ( -
-
-
+const BreakdownItem: React.FC = ({ + label, + value, + icon, +}) => ( +
+
+
{icon}
- {label} + {label}
- ${value.toLocaleString()} + ${value.toLocaleString()}
); @@ -74,35 +90,25 @@ const BreakdownItem: React.FC = ({ label, value, icon }) => */ interface ActivityItemProps { activity: EarningActivity; - onClaim: () => void; - isClaiming: boolean; } /** * ActivityItem component for displaying a single reward entry. */ -const ActivityItem: React.FC = ({ activity, onClaim, isClaiming }) => ( -
-
-
- {activity.title} - - {activity.status} - -
-
- {activity.source} +const ActivityItem: React.FC = ({ activity }) => ( +
+
+
{activity.title}
+
+ {activity.source} - {new Date(activity.createdAt).toLocaleDateString()} + {new Date(activity.occurredAt).toLocaleDateString()}
-
-

${activity.amount.toLocaleString()}

- {activity.status === 'claimable' && ( - +
+

${activity.amount.toLocaleString()}

+ {activity.currency && ( +

{activity.currency}

)}
@@ -112,19 +118,19 @@ const ActivityItem: React.FC = ({ activity, onClaim, isClaimi * EarningsSkeleton component for loading states. */ const EarningsSkeleton: React.FC = () => ( -
-
- - +
+
+ +
-
- - - +
+ + +
-
- - +
+ +
); @@ -135,7 +141,6 @@ const EarningsSkeleton: React.FC = () => ( const EarningsPage: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); - const [claimingId, setClaimingId] = useState(null); useEffect(() => { const fetchData = async () => { @@ -154,109 +159,105 @@ const EarningsPage: React.FC = () => { fetchData(); }, []); - /** - * Handles the claiming of a specific earning activity. - */ - const handleClaim = async (id: string) => { - setClaimingId(id); - try { - const res = await claimEarning({ activityId: id }); - if (res.success) { - toast.success('Claim successful!'); - // Refresh data - const updated = await getUserEarnings(); - if (updated.success && updated.data) { - setData(updated.data); - } - } else { - toast.error(res.message || 'Claim failed'); - } - } catch (error) { - console.error('Failed to claim earning:', error); - toast.error('An error occurred during claiming'); - } finally { - setClaimingId(null); - } - }; - if (loading) { return ; } if (!data) { return ( -
-

No earnings data found.

+
+

+ No earnings data found. +

); } return ( -
- + -

Earnings Dashboard

-

Manage and track your rewards across the platform.

+

+ Earnings Dashboard +

+

+ Manage and track your rewards across the platform. +

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

No recent activity found.

+

+ No recent activity found. +

) : ( - data.activities.map((activity) => ( - handleClaim(activity.id)} - isClaiming={claimingId === activity.id} - /> + data.activities.map(activity => ( + )) )}
diff --git a/lib/api/user/earnings.ts b/lib/api/user/earnings.ts index 3f33e318a..a86d7dc13 100644 --- a/lib/api/user/earnings.ts +++ b/lib/api/user/earnings.ts @@ -3,18 +3,19 @@ import { ApiResponse } from '../types'; export interface EarningActivity { id: string; - source: 'hackathon' | 'grant' | 'crowdfund' | 'bounty'; + source: 'hackathons' | 'grants' | 'crowdfunding' | 'bounties'; title: string; amount: number; currency: string; - status: 'pending' | 'completed' | 'claimable'; - createdAt: string; + occurredAt: string; } export interface EarningsData { - totalEarned: number; - pendingWithdrawal: number; - completedWithdrawal: number; + summary: { + totalEarned: number; + pendingWithdrawal: number; + completedWithdrawal: number; + }; breakdown: { hackathons: number; grants: number; @@ -52,7 +53,12 @@ export const getUserEarnings = async (): Promise => { /** * Claim a specific earning */ -export const claimEarning = async (data: ClaimEarningRequest): Promise => { - const res = await api.post('/user/earnings/claim', data); +export const claimEarning = async ( + data: ClaimEarningRequest +): Promise => { + const res = await api.post( + '/user/earnings/claim', + data + ); return res.data; }; From 14343d59405e28ffa5e091a3a19afea086c2091f Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sun, 1 Mar 2026 10:00:48 +0800 Subject: [PATCH 5/9] Atlas-Bounty: Fix Hackathon Draft Deletion Failure (Endpoint Mismatch) --- .../organizations/[id]/hackathons/page.tsx | 10 ++++++---- hooks/hackathon/use-delete-hackathon.ts | 15 ++++++++++----- lib/api/hackathons/draft.ts | 13 +++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx index 699c39057..1ecc6449a 100644 --- a/app/(landing)/organizations/[id]/hackathons/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/page.tsx @@ -87,6 +87,7 @@ export default function HackathonsPage() { const [hackathonToDelete, setHackathonToDelete] = useState<{ id: string; title: string; + isDraft: boolean; } | null>(null); const { hackathons, hackathonsLoading, drafts, draftsLoading, refetchAll } = @@ -98,7 +99,8 @@ export default function HackathonsPage() { // Use the separate delete hook const { isDeleting, deleteHackathon } = useDeleteHackathon({ organizationId, - hackathonId: hackathonToDelete?.id || '', // This will be set when we have a hackathon to delete + hackathonId: hackathonToDelete?.id || '', + isDraft: hackathonToDelete?.isDraft || false, onSuccess: () => { // Refresh the hackathons list after successful deletion refetchAll(); @@ -181,12 +183,12 @@ export default function HackathonsPage() { const handleDeleteClick = (hackathonId: string) => { const hackathon = allHackathons.find(item => item.data.id === hackathonId); if (hackathon) { - const title = - hackathon.type === 'draft' + const isDraft = hackathon.type === 'draft'; + const title = isDraft ? (hackathon.data as HackathonDraft).data.information?.name || 'Untitled Hackathon' : (hackathon.data as Hackathon).name || 'Untitled Hackathon'; - setHackathonToDelete({ id: hackathonId, title }); + setHackathonToDelete({ id: hackathonId, title, isDraft }); setDeleteDialogOpen(true); } }; diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts index 4ef6d5780..07eaf6c46 100644 --- a/hooks/hackathon/use-delete-hackathon.ts +++ b/hooks/hackathon/use-delete-hackathon.ts @@ -2,12 +2,14 @@ import { useState, useCallback } from 'react'; import { deleteHackathon } from '@/lib/api/hackathons'; +import { deleteDraft } from '@/lib/api/hackathons/draft'; import { useAuthStatus } from '@/hooks/use-auth'; import { toast } from 'sonner'; interface UseDeleteHackathonOptions { organizationId: string; hackathonId: string; + isDraft?: boolean; onSuccess?: () => void; onError?: (error: string) => void; } @@ -15,6 +17,7 @@ interface UseDeleteHackathonOptions { export function useDeleteHackathon({ organizationId, hackathonId, + isDraft = false, onSuccess, onError, }: UseDeleteHackathonOptions) { @@ -37,18 +40,20 @@ export function useDeleteHackathon({ setError(null); try { - const response = await deleteHackathon(hackathonId); + const response = isDraft + ? await deleteDraft(organizationId, hackathonId) + : await deleteHackathon(hackathonId); if (response.success) { - toast.success('Hackathon deleted successfully'); + toast.success(`${isDraft ? 'Draft' : 'Hackathon'} deleted successfully`); onSuccess?.(); return response.data; } else { - throw new Error(response.message || 'Failed to delete hackathon'); + throw new Error(response.message || `Failed to delete ${isDraft ? 'draft' : 'hackathon'}`); } } catch (err) { const errorMessage = - err instanceof Error ? err.message : 'Failed to delete hackathon'; + err instanceof Error ? err.message : `Failed to delete ${isDraft ? 'draft' : 'hackathon'}`; setError(errorMessage); toast.error(errorMessage); onError?.(errorMessage); @@ -56,7 +61,7 @@ export function useDeleteHackathon({ } finally { setIsDeleting(false); } - }, [organizationId, hackathonId, isAuthenticated, onSuccess, onError]); + }, [organizationId, hackathonId, isDraft, isAuthenticated, onSuccess, onError]); return { isDeleting, diff --git a/lib/api/hackathons/draft.ts b/lib/api/hackathons/draft.ts index d0bf77c8a..b50d69600 100644 --- a/lib/api/hackathons/draft.ts +++ b/lib/api/hackathons/draft.ts @@ -119,3 +119,16 @@ export const getDrafts = async ( return res.data as GetDraftsResponse; }; + +/** + * Delete a hackathon draft + */ +export const deleteDraft = async ( + organizationId: string, + draftId: string +): Promise> => { + const res = await api.delete>( + `/organizations/${organizationId}/hackathons/draft/${draftId}` + ); + return res.data; +}; From 80712dc0ad2d3ea365cd55f097d9c4f1a50d9061 Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sun, 1 Mar 2026 10:07:19 +0800 Subject: [PATCH 6/9] Atlas-Bounty: Polish deletion logic and remove redundant toasts --- .../organizations/[id]/hackathons/page.tsx | 9 ++------- hooks/hackathon/use-delete-hackathon.ts | 13 ++++++++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx index 1ecc6449a..f949e2571 100644 --- a/app/(landing)/organizations/[id]/hackathons/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/page.tsx @@ -104,14 +104,9 @@ export default function HackathonsPage() { onSuccess: () => { // Refresh the hackathons list after successful deletion refetchAll(); - toast.success('Hackathon deleted successfully', { - description: `"${hackathonToDelete?.title}" has been permanently deleted.`, - }); }, - onError: error => { - toast.error('Failed to delete hackathon', { - description: error, - }); + onError: _error => { + // Error handled by hook's internal toast }, }); diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts index 07eaf6c46..03c1e5ff0 100644 --- a/hooks/hackathon/use-delete-hackathon.ts +++ b/hooks/hackathon/use-delete-hackathon.ts @@ -26,14 +26,17 @@ export function useDeleteHackathon({ const [error, setError] = useState(null); const deleteHackathonAction = useCallback(async () => { + const targetLabel = isDraft ? 'draft' : 'hackathon'; + if (!isAuthenticated) { - toast.error('Please sign in to delete hackathons'); + toast.error(`Please sign in to delete ${targetLabel}s`); throw new Error('Authentication required'); } if (!organizationId || !hackathonId) { - toast.error('Organization ID and Hackathon ID are required'); - throw new Error('Organization ID and Hackathon ID are required'); + const idError = `Organization ID and ${isDraft ? 'Draft' : 'Hackathon'} ID are required`; + toast.error(idError); + throw new Error(idError); } setIsDeleting(true); @@ -49,11 +52,11 @@ export function useDeleteHackathon({ onSuccess?.(); return response.data; } else { - throw new Error(response.message || `Failed to delete ${isDraft ? 'draft' : 'hackathon'}`); + throw new Error(response.message || `Failed to delete ${targetLabel}`); } } catch (err) { const errorMessage = - err instanceof Error ? err.message : `Failed to delete ${isDraft ? 'draft' : 'hackathon'}`; + err instanceof Error ? err.message : `Failed to delete ${targetLabel}`; setError(errorMessage); toast.error(errorMessage); onError?.(errorMessage); From 129af639bbca6fcb586c258ec21f69ba664c1a01 Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sun, 1 Mar 2026 10:46:12 +0800 Subject: [PATCH 7/9] chore: fix trailing whitespace --- hooks/hackathon/use-delete-hackathon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts index 03c1e5ff0..b1ed7c4f1 100644 --- a/hooks/hackathon/use-delete-hackathon.ts +++ b/hooks/hackathon/use-delete-hackathon.ts @@ -27,7 +27,7 @@ export function useDeleteHackathon({ const deleteHackathonAction = useCallback(async () => { const targetLabel = isDraft ? 'draft' : 'hackathon'; - + if (!isAuthenticated) { toast.error(`Please sign in to delete ${targetLabel}s`); throw new Error('Authentication required'); @@ -43,7 +43,7 @@ export function useDeleteHackathon({ setError(null); try { - const response = isDraft + const response = isDraft ? await deleteDraft(organizationId, hackathonId) : await deleteHackathon(hackathonId); From f7b341f62e2beca6238ee06f6cc5e369beea7399 Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sun, 1 Mar 2026 10:52:37 +0800 Subject: [PATCH 8/9] fix: resolve draft deletion endpoint mismatch --- hooks/hackathon/use-delete-hackathon.ts | 28 +++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts index b1ed7c4f1..3617d17a7 100644 --- a/hooks/hackathon/use-delete-hackathon.ts +++ b/hooks/hackathon/use-delete-hackathon.ts @@ -1,5 +1,3 @@ -'use client'; - import { useState, useCallback } from 'react'; import { deleteHackathon } from '@/lib/api/hackathons'; import { deleteDraft } from '@/lib/api/hackathons/draft'; @@ -14,36 +12,43 @@ interface UseDeleteHackathonOptions { onError?: (error: string) => void; } -export function useDeleteHackathon({ +/** + * Hook to handle hackathon and draft deletion with proper API routing and feedback. + */ +export const useDeleteHackathon = ({ organizationId, hackathonId, isDraft = false, onSuccess, onError, -}: UseDeleteHackathonOptions) { +}: UseDeleteHackathonOptions) => { const { isAuthenticated } = useAuthStatus(); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); const deleteHackathonAction = useCallback(async () => { const targetLabel = isDraft ? 'draft' : 'hackathon'; - + if (!isAuthenticated) { toast.error(`Please sign in to delete ${targetLabel}s`); throw new Error('Authentication required'); } - if (!organizationId || !hackathonId) { - const idError = `Organization ID and ${isDraft ? 'Draft' : 'Hackathon'} ID are required`; - toast.error(idError); - throw new Error(idError); + if (!hackathonId) { + toast.error('Hackathon ID is required'); + throw new Error('Hackathon ID is required'); + } + + if (isDraft && !organizationId) { + toast.error('Organization ID is required for draft deletion'); + throw new Error('Organization ID is required for draft deletion'); } setIsDeleting(true); setError(null); try { - const response = isDraft + const response = isDraft ? await deleteDraft(organizationId, hackathonId) : await deleteHackathon(hackathonId); @@ -58,6 +63,7 @@ export function useDeleteHackathon({ const errorMessage = err instanceof Error ? err.message : `Failed to delete ${targetLabel}`; setError(errorMessage); + console.error(`Error deleting ${targetLabel}:`, err); toast.error(errorMessage); onError?.(errorMessage); throw err; @@ -71,4 +77,4 @@ export function useDeleteHackathon({ error, deleteHackathon: deleteHackathonAction, }; -} +}; From 574606970c52a875138ae5ded239892144294639 Mon Sep 17 00:00:00 2001 From: Pipixia Watchdog Date: Sun, 1 Mar 2026 15:45:43 +0800 Subject: [PATCH 9/9] fix: make delete hook type-safe and improve error handling --- hooks/hackathon/use-delete-hackathon.ts | 61 +++++++++++++------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts index 3617d17a7..6f1e1b400 100644 --- a/hooks/hackathon/use-delete-hackathon.ts +++ b/hooks/hackathon/use-delete-hackathon.ts @@ -4,50 +4,51 @@ import { deleteDraft } from '@/lib/api/hackathons/draft'; import { useAuthStatus } from '@/hooks/use-auth'; import { toast } from 'sonner'; -interface UseDeleteHackathonOptions { - organizationId: string; - hackathonId: string; - isDraft?: boolean; - onSuccess?: () => void; - onError?: (error: string) => void; -} +type UseDeleteHackathonOptions = + | { + isDraft: true; + organizationId: string; + hackathonId: string; + onSuccess?: () => void; + onError?: (error: string) => void; + } + | { + isDraft?: false; + organizationId?: string; + hackathonId: string; + onSuccess?: () => void; + onError?: (error: string) => void; + }; /** * Hook to handle hackathon and draft deletion with proper API routing and feedback. */ -export const useDeleteHackathon = ({ - organizationId, - hackathonId, - isDraft = false, - onSuccess, - onError, -}: UseDeleteHackathonOptions) => { +export const useDeleteHackathon = (options: UseDeleteHackathonOptions) => { + const { isDraft = false, organizationId, hackathonId, onSuccess, onError } = options; const { isAuthenticated } = useAuthStatus(); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); const deleteHackathonAction = useCallback(async () => { const targetLabel = isDraft ? 'draft' : 'hackathon'; - - if (!isAuthenticated) { - toast.error(`Please sign in to delete ${targetLabel}s`); - throw new Error('Authentication required'); - } - - if (!hackathonId) { - toast.error('Hackathon ID is required'); - throw new Error('Hackathon ID is required'); - } - - if (isDraft && !organizationId) { - toast.error('Organization ID is required for draft deletion'); - throw new Error('Organization ID is required for draft deletion'); - } + const idLabel = isDraft ? 'Draft ID' : 'Hackathon ID'; setIsDeleting(true); setError(null); try { + if (!isAuthenticated) { + throw new Error(`Please sign in to delete ${targetLabel}s`); + } + + if (!hackathonId) { + throw new Error(`${idLabel} is required`); + } + + if (isDraft && !organizationId) { + throw new Error('Organization ID is required for draft deletion'); + } + const response = isDraft ? await deleteDraft(organizationId, hackathonId) : await deleteHackathon(hackathonId); @@ -66,7 +67,7 @@ export const useDeleteHackathon = ({ console.error(`Error deleting ${targetLabel}:`, err); toast.error(errorMessage); onError?.(errorMessage); - throw err; + // We don't re-throw here to allow callers to handle state via hook instead of try/catch } finally { setIsDeleting(false); }