From e9f3ddb250dd0ee3935080c455ddd5cb1effd1a3 Mon Sep 17 00:00:00 2001 From: KingFRANKHOOD Date: Thu, 19 Feb 2026 21:31:34 +0100 Subject: [PATCH 1/2] feat:implement user dashboard-my claims --- app/profile/[userId]/page.tsx | 40 +++++++ .../reputation/__tests__/my-claims.test.ts | 37 +++++++ components/reputation/my-claims.tsx | 103 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 components/reputation/__tests__/my-claims.test.ts create mode 100644 components/reputation/my-claims.tsx diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 77f8d29..81547df 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -1,8 +1,10 @@ "use client"; import { useContributorReputation } from "@/hooks/use-reputation"; +import { useBounties } from "@/hooks/use-bounties"; import { ReputationCard } from "@/components/reputation/reputation-card"; import { CompletionHistory } from "@/components/reputation/completion-history"; +import { MyClaims, type MyClaim } from "@/components/reputation/my-claims"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -15,6 +17,7 @@ export default function ProfilePage() { const params = useParams(); const userId = params.userId as string; const { data: reputation, isLoading, error } = useContributorReputation(userId); + const { data: bountyResponse } = useBounties(); const MAX_MOCK_HISTORY = 50; @@ -39,6 +42,32 @@ export default function ProfilePage() { })); }, [reputation]); + const myClaims = useMemo(() => { + const bounties = bountyResponse?.data ?? []; + + return bounties + .filter((bounty) => bounty.claimedBy === userId) + .map((bounty) => { + let status = "active"; + + if (bounty.status === "closed") { + status = "completed"; + } else if (bounty.status === "claimed" && bounty.claimExpiresAt) { + const claimExpiry = new Date(bounty.claimExpiresAt); + if (!Number.isNaN(claimExpiry.getTime()) && claimExpiry < new Date()) { + status = "in-review"; + } + } + + return { + bountyId: bounty.id, + title: bounty.issueTitle, + status, + rewardAmount: bounty.rewardAmount ?? undefined, + }; + }); + }, [bountyResponse?.data, userId]); + if (isLoading) { return (
@@ -134,6 +163,12 @@ export default function ProfilePage() { > Analytics + + My Claims + @@ -149,6 +184,11 @@ export default function ProfilePage() { Detailed analytics coming soon.
+ + +

My Claims

+ +
diff --git a/components/reputation/__tests__/my-claims.test.ts b/components/reputation/__tests__/my-claims.test.ts new file mode 100644 index 0000000..92d6022 --- /dev/null +++ b/components/reputation/__tests__/my-claims.test.ts @@ -0,0 +1,37 @@ +import { CLAIM_SECTIONS, getClaimsBySection, normalizeStatus, type MyClaim } from "@/components/reputation/my-claims"; +import { describe, expect, it } from "@jest/globals"; + +describe("My Claims helpers", () => { + it("normalizes status values consistently", () => { + expect(normalizeStatus(" In Review ")).toBe("in-review"); + expect(normalizeStatus("UNDER_REVIEW")).toBe("under-review"); + expect(normalizeStatus("in_review")).toBe("in-review"); + }); + + it("groups claims into Active Claims, In Review, and Completed by status", () => { + const claims: MyClaim[] = [ + { bountyId: "1", title: "Active A", status: "active" }, + { bountyId: "2", title: "Active B", status: "claimed" }, + { bountyId: "3", title: "Review A", status: "in review" }, + { bountyId: "4", title: "Review B", status: "UNDER_REVIEW" }, + { bountyId: "5", title: "Completed A", status: "completed" }, + { bountyId: "6", title: "Completed B", status: "closed" }, + { bountyId: "7", title: "Unknown", status: "queued" }, + ]; + + const groups = getClaimsBySection(claims); + + expect(groups).toHaveLength(CLAIM_SECTIONS.length); + + const activeGroup = groups.find((group) => group.section.title === "Active Claims"); + const reviewGroup = groups.find((group) => group.section.title === "In Review"); + const completedGroup = groups.find((group) => group.section.title === "Completed"); + + expect(activeGroup?.claims.map((claim) => claim.bountyId)).toEqual(["1", "2"]); + expect(reviewGroup?.claims.map((claim) => claim.bountyId)).toEqual(["3", "4"]); + expect(completedGroup?.claims.map((claim) => claim.bountyId)).toEqual(["5", "6"]); + + const groupedIds = new Set(groups.flatMap((group) => group.claims.map((claim) => claim.bountyId))); + expect(groupedIds.has("7")).toBe(false); + }); +}); diff --git a/components/reputation/my-claims.tsx b/components/reputation/my-claims.tsx new file mode 100644 index 0000000..f6ed931 --- /dev/null +++ b/components/reputation/my-claims.tsx @@ -0,0 +1,103 @@ +import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatCurrency } from "@/helpers/format.helper"; + +export type MyClaim = { + bountyId: string; + title: string; + status: string; + nextMilestone?: string; + rewardAmount?: number; +}; + +interface MyClaimsProps { + claims: MyClaim[]; +} + +export const CLAIM_SECTIONS: { title: string; statuses: string[] }[] = [ + { title: "Active Claims", statuses: ["active", "claimed", "in-progress"] }, + { title: "In Review", statuses: ["in-review", "in review", "review", "pending", "under-review"] }, + { title: "Completed", statuses: ["completed", "closed", "accepted", "done"] }, +]; + +export function normalizeStatus(status: string) { + return status.trim().toLowerCase().replace(/[\s_]+/g, "-"); +} + +function formatStatusLabel(status: string) { + return status + .replace(/[-_]+/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); +} + +export function getClaimsBySection(claims: MyClaim[]) { + return CLAIM_SECTIONS.map((section) => ({ + section, + claims: claims.filter((claim) => { + const normalizedClaimStatus = normalizeStatus(claim.status); + return section.statuses.some((status) => normalizeStatus(status) === normalizedClaimStatus); + }), + })); +} + +export function MyClaims({ claims }: MyClaimsProps) { + return ( +
+ {getClaimsBySection(claims).map(({ section, claims: sectionClaims }) => { + + return ( + + + {section.title} + + {sectionClaims.length === 0 + ? "No claims in this section." + : `${sectionClaims.length} claim${sectionClaims.length === 1 ? "" : "s"}`} + + + + {sectionClaims.length === 0 ? ( +

No opportunities yet.

+ ) : ( +
+ {sectionClaims.map((claim) => ( +
+
+
+

+ {claim.title} +

+
+ {formatStatusLabel(claim.status)} + {typeof claim.rewardAmount === "number" && ( + + {formatCurrency(claim.rewardAmount, "$")} + + )} +
+ {claim.nextMilestone && ( +

+ Next milestone: {claim.nextMilestone} +

+ )} +
+ +
+
+ ))} +
+ )} +
+
+ ); + })} +
+ ); +} From 65f877d9d9fc46d2a9cbfc424be47d539332be2c Mon Sep 17 00:00:00 2001 From: KingFRANKHOOD Date: Fri, 20 Feb 2026 08:41:45 +0100 Subject: [PATCH 2/2] fix: resolve CI failure --- app/api/compliance/status/route.ts | 4 +-- app/api/compliance/terms/route.ts | 4 +-- app/api/compliance/upgrade/route.ts | 9 +++--- app/api/withdrawal/submit/route.ts | 5 +-- app/profile/[userId]/page.tsx | 4 +-- components/compliance/appeal-dialog.tsx | 5 +-- components/compliance/document-upload.tsx | 5 +-- components/compliance/limits-display.tsx | 6 +--- components/compliance/terms-dialog.tsx | 2 +- components/compliance/tier-upgrade-dialog.tsx | 8 ++--- components/wallet/wallet-sheet.tsx | 1 - components/wallet/withdrawal-section.tsx | 32 ++++++++----------- lib/services/appeal.ts | 2 +- lib/services/compliance.ts | 2 -- lib/services/geo-restriction.ts | 5 +-- 15 files changed, 41 insertions(+), 53 deletions(-) diff --git a/app/api/compliance/status/route.ts b/app/api/compliance/status/route.ts index 9054ac5..79033fa 100644 --- a/app/api/compliance/status/route.ts +++ b/app/api/compliance/status/route.ts @@ -1,9 +1,9 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/server-auth"; import { ComplianceService } from "@/lib/services/compliance"; import { TermsService } from "@/lib/services/terms"; -export async function GET(request: NextRequest) { +export async function GET() { try { const user = await getCurrentUser(); if (!user) { diff --git a/app/api/compliance/terms/route.ts b/app/api/compliance/terms/route.ts index 22ab0e5..71ddbef 100644 --- a/app/api/compliance/terms/route.ts +++ b/app/api/compliance/terms/route.ts @@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/server-auth"; import { TermsService } from "@/lib/services/terms"; -export async function GET(request: NextRequest) { +export async function GET() { try { const terms = await TermsService.getCurrentTermsVersion(); return NextResponse.json(terms); - } catch (error) { + } catch { return NextResponse.json({ error: "Failed to fetch terms" }, { status: 500 }); } } diff --git a/app/api/compliance/upgrade/route.ts b/app/api/compliance/upgrade/route.ts index c54ad2e..e9001b2 100644 --- a/app/api/compliance/upgrade/route.ts +++ b/app/api/compliance/upgrade/route.ts @@ -17,16 +17,17 @@ export async function POST(request: NextRequest) { ); return NextResponse.json(verificationRequest); - } catch (error: any) { + } catch (error: unknown) { console.error("Error creating verification request:", error); + const message = error instanceof Error ? error.message : "Failed to create verification request"; return NextResponse.json( - { error: error.message || "Failed to create verification request" }, + { error: message }, { status: 400 } ); } } -export async function GET(request: NextRequest) { +export async function GET() { try { const user = await getCurrentUser(); if (!user) { @@ -35,7 +36,7 @@ export async function GET(request: NextRequest) { const status = await VerificationService.getVerificationStatus(user.id); return NextResponse.json(status); - } catch (error) { + } catch { return NextResponse.json({ error: "Failed to fetch verification status" }, { status: 500 }); } } diff --git a/app/api/withdrawal/submit/route.ts b/app/api/withdrawal/submit/route.ts index ce43753..d916fab 100644 --- a/app/api/withdrawal/submit/route.ts +++ b/app/api/withdrawal/submit/route.ts @@ -21,10 +21,11 @@ export async function POST(request: NextRequest) { ); return NextResponse.json(withdrawal); - } catch (error: any) { + } catch (error: unknown) { console.error("Error submitting withdrawal:", error); + const message = error instanceof Error ? error.message : "Withdrawal failed"; return NextResponse.json( - { error: error.message || "Withdrawal failed" }, + { error: message }, { status: 400 } ); } diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index 81547df..743a525 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -73,8 +73,8 @@ export default function ProfilePage() {
- - + +
); diff --git a/components/compliance/appeal-dialog.tsx b/components/compliance/appeal-dialog.tsx index 3cbbcd1..4cc4047 100644 --- a/components/compliance/appeal-dialog.tsx +++ b/components/compliance/appeal-dialog.tsx @@ -30,8 +30,9 @@ export function AppealDialog({ open, onOpenChange, verificationRequestId, userId onOpenChange(false); setReason(""); setAdditionalInfo(""); - } catch (error: any) { - alert(error.message || 'Failed to submit appeal'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to submit appeal'; + alert(message); } finally { setSubmitting(false); } diff --git a/components/compliance/document-upload.tsx b/components/compliance/document-upload.tsx index bf932a7..9dceb31 100644 --- a/components/compliance/document-upload.tsx +++ b/components/compliance/document-upload.tsx @@ -40,8 +40,9 @@ export function DocumentUpload({ type, label, onUpload, uploaded }: DocumentUplo setUploading(true); try { await onUpload(selectedFile); - } catch (err: any) { - setError(err.message || "Upload failed"); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Upload failed"; + setError(message); } finally { setUploading(false); } diff --git a/components/compliance/limits-display.tsx b/components/compliance/limits-display.tsx index d93fc64..ced6d39 100644 --- a/components/compliance/limits-display.tsx +++ b/components/compliance/limits-display.tsx @@ -51,21 +51,18 @@ export function LimitsDisplay({ onUpgradeClick }: LimitsDisplayProps) {
void; - currentTier: KYCTier; targetTier: KYCTier; } const formatCurrency = (amount: number) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(amount); -export function TierUpgradeDialog({ open, onOpenChange, currentTier, targetTier }: TierUpgradeDialogProps) { +export function TierUpgradeDialog({ open, onOpenChange, targetTier }: TierUpgradeDialogProps) { const [step, setStep] = useState<'info' | 'documents'>('info'); const [requestId, setRequestId] = useState(null); const [uploadedDocs, setUploadedDocs] = useState>(new Set()); @@ -37,8 +36,9 @@ export function TierUpgradeDialog({ open, onOpenChange, currentTier, targetTier } else { onOpenChange(false); } - } catch (error: any) { - alert(error.message || 'Failed to request upgrade'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to request upgrade'; + alert(message); } }; diff --git a/components/wallet/wallet-sheet.tsx b/components/wallet/wallet-sheet.tsx index 42087e9..6446ddc 100644 --- a/components/wallet/wallet-sheet.tsx +++ b/components/wallet/wallet-sheet.tsx @@ -12,7 +12,6 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { Wallet, Copy, diff --git a/components/wallet/withdrawal-section.tsx b/components/wallet/withdrawal-section.tsx index 63f0a03..98650ff 100644 --- a/components/wallet/withdrawal-section.tsx +++ b/components/wallet/withdrawal-section.tsx @@ -24,10 +24,9 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) { const [amount, setAmount] = useState(""); const [showTermsDialog, setShowTermsDialog] = useState(false); const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); - const [validationError, setValidationError] = useState(null); const { data: complianceData } = useComplianceStatus(); - const validateMutation = useValidateWithdrawal(); + const { data: validationData, mutate: validateWithdrawal } = useValidateWithdrawal(); const submitMutation = useSubmitWithdrawal(); const bankAccounts = [ @@ -36,22 +35,17 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) { const parsedAmount = parseFloat(amount); const isValidAmount = !isNaN(parsedAmount) && isFinite(parsedAmount) && parsedAmount >= 10; + const isAmountWithinBalance = parsedAmount <= walletInfo.balance; useEffect(() => { - if (isValidAmount && parsedAmount <= walletInfo.balance) { - validateMutation.mutate(parsedAmount, { - onSuccess: (result) => { - if (!result.valid) { - setValidationError(result.errors[0] || 'Validation failed'); - } else { - setValidationError(null); - } - }, - }); - } else { - setValidationError(null); + if (isValidAmount && isAmountWithinBalance) { + validateWithdrawal(parsedAmount); } - }, [parsedAmount]); + }, [isAmountWithinBalance, isValidAmount, parsedAmount, validateWithdrawal]); + + const validationError = isValidAmount && isAmountWithinBalance && validationData && !validationData.valid + ? validationData.errors[0] || 'Validation failed' + : null; const handleWithdraw = async () => { if (!isValidAmount || !complianceData) return; @@ -64,8 +58,9 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) { }); alert('Withdrawal submitted successfully!'); setAmount(''); - } catch (error: any) { - alert(error.message || 'Withdrawal failed'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Withdrawal failed'; + alert(message); } }; @@ -77,7 +72,7 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) { }; const canWithdraw = isValidAmount && - parsedAmount <= walletInfo.balance && + isAmountWithinBalance && !validationError && complianceData?.compliance.holdState === 'NONE' && !complianceData?.termsStatus.requiresAcceptance; @@ -103,7 +98,6 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) { )} diff --git a/lib/services/appeal.ts b/lib/services/appeal.ts index 272bd49..5c24c3f 100644 --- a/lib/services/appeal.ts +++ b/lib/services/appeal.ts @@ -1,4 +1,4 @@ -import { VerificationAppeal, AppealStatus } from "@/types/compliance"; +import { VerificationAppeal } from "@/types/compliance"; import { EmailService } from "./email"; const MOCK_APPEALS: Record = {}; diff --git a/lib/services/compliance.ts b/lib/services/compliance.ts index 07437bd..6d66cf7 100644 --- a/lib/services/compliance.ts +++ b/lib/services/compliance.ts @@ -2,8 +2,6 @@ import { UserCompliance, KYCTier, KYCTierConfig, - WithdrawalLimits, - WithdrawalUsage, RemainingLimits, ComplianceHoldState, VerificationStatus, diff --git a/lib/services/geo-restriction.ts b/lib/services/geo-restriction.ts index 19ecd01..2072fe3 100644 --- a/lib/services/geo-restriction.ts +++ b/lib/services/geo-restriction.ts @@ -8,10 +8,6 @@ const RESTRICTED: RestrictedJurisdiction[] = [ { code: 'US-NY', name: 'New York', type: 'STATE', reason: 'BitLicense requirements', effectiveDate: '2024-01-01' }, ]; -const VPN_INDICATORS = [ - 'vpn', 'proxy', 'tor', 'relay', 'hosting', 'datacenter' -]; - export class GeoRestrictionService { static async checkLocation(ip: string): Promise { // In production: call ipapi.co, MaxMind, or ip-api.com @@ -63,6 +59,7 @@ export class GeoRestrictionService { static async detectProxy(ip: string): Promise { // In production: check proxy databases or use APIs // For now, use similar logic as VPN detection + void ip; return false; }