Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api/compliance/status/route.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions app/api/compliance/terms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
Expand Down
9 changes: 5 additions & 4 deletions app/api/compliance/upgrade/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 });
}
}
5 changes: 3 additions & 2 deletions app/api/withdrawal/submit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
);
}
Expand Down
44 changes: 42 additions & 2 deletions app/profile/[userId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;

Expand All @@ -39,13 +42,39 @@ export default function ProfilePage() {
}));
}, [reputation]);

const myClaims = useMemo<MyClaim[]>(() => {
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 (
<div className="container mx-auto py-8">
<Skeleton className="h-10 w-32 mb-8" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<Skeleton className="h-[400px] md:col-span-1" />
<Skeleton className="h-[400px] md:col-span-2" />
<Skeleton className="h-100 md:col-span-1" />
<Skeleton className="h-100 md:col-span-2" />
</div>
</div>
);
Expand Down Expand Up @@ -134,6 +163,12 @@ export default function ProfilePage() {
>
Analytics
</TabsTrigger>
<TabsTrigger
value="claims"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-3"
>
My Claims
</TabsTrigger>
</TabsList>

<TabsContent value="history" className="mt-6">
Expand All @@ -149,6 +184,11 @@ export default function ProfilePage() {
Detailed analytics coming soon.
</div>
</TabsContent>

<TabsContent value="claims" className="mt-6">
<h2 className="text-xl font-bold mb-4">My Claims</h2>
<MyClaims claims={myClaims} />
</TabsContent>
</Tabs>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions components/compliance/appeal-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 3 additions & 2 deletions components/compliance/document-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 1 addition & 5 deletions components/compliance/limits-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,18 @@ export function LimitsDisplay({ onUpgradeClick }: LimitsDisplayProps) {
<div className="space-y-3">
<LimitBar
label="Daily Limit"
used={compliance.usage.dailyUsed}
total={compliance.limits.daily}
remaining={remaining.daily}
percent={remaining.percentUsed.daily}
/>
<LimitBar
label="Weekly Limit"
used={compliance.usage.weeklyUsed}
total={compliance.limits.weekly}
remaining={remaining.weekly}
percent={remaining.percentUsed.weekly}
/>
<LimitBar
label="Monthly Limit"
used={compliance.usage.monthlyUsed}
total={compliance.limits.monthly}
remaining={remaining.monthly}
percent={remaining.percentUsed.monthly}
Expand All @@ -75,9 +72,8 @@ export function LimitsDisplay({ onUpgradeClick }: LimitsDisplayProps) {
);
}

function LimitBar({ label, used, total, remaining, percent }: {
function LimitBar({ label, total, remaining, percent }: {
label: string;
used: number;
total: number;
remaining: number;
percent: number;
Expand Down
2 changes: 1 addition & 1 deletion components/compliance/terms-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
Expand Down
8 changes: 4 additions & 4 deletions components/compliance/tier-upgrade-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ import { DocumentUpload } from "./document-upload";
interface TierUpgradeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => 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<string | null>(null);
const [uploadedDocs, setUploadedDocs] = useState<Set<DocumentType>>(new Set());
Expand All @@ -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);
}
};

Expand Down
37 changes: 37 additions & 0 deletions components/reputation/__tests__/my-claims.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
103 changes: 103 additions & 0 deletions components/reputation/my-claims.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-6">
{getClaimsBySection(claims).map(({ section, claims: sectionClaims }) => {

return (
<Card key={section.title} className="gap-4 py-5">
<CardHeader className="px-5">
<CardTitle className="text-lg">{section.title}</CardTitle>
<CardDescription>
{sectionClaims.length === 0
? "No claims in this section."
: `${sectionClaims.length} claim${sectionClaims.length === 1 ? "" : "s"}`}
</CardDescription>
</CardHeader>
<CardContent className="px-5">
{sectionClaims.length === 0 ? (
<p className="text-sm text-muted-foreground">No opportunities yet.</p>
) : (
<div className="space-y-3">
{sectionClaims.map((claim) => (
<div
key={`${section.title}-${claim.bountyId}`}
className="rounded-lg border border-border/60 bg-secondary/5 p-4"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-2">
<h4 className="truncate text-sm font-semibold" title={claim.title}>
{claim.title}
</h4>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">{formatStatusLabel(claim.status)}</Badge>
{typeof claim.rewardAmount === "number" && (
<span className="text-sm text-muted-foreground">
{formatCurrency(claim.rewardAmount, "$")}
</span>
)}
</div>
{claim.nextMilestone && (
<p className="text-xs text-muted-foreground">
Next milestone: {claim.nextMilestone}
</p>
)}
</div>
<Button asChild variant="outline" size="sm" className="shrink-0">
<Link href={`/bounty/${claim.bountyId}`}>View Opportunity</Link>
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
})}
</div>
);
}
1 change: 0 additions & 1 deletion components/wallet/wallet-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading