Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:3001/api
NEXT_PUBLIC_WS_URL=ws://localhost:3001

# Blockchain Configuration
NEXT_PUBLIC_OPTIMISM_RPC_URL=https://mainnet.optimism.io
NEXT_PUBLIC_CHAIN_ID=10
NEXT_PUBLIC_TOKEN_CONTRACT_ADDRESS=0x...

# IPFS Configuration
NEXT_PUBLIC_IPFS_GATEWAY=https://ipfs.io/ipfs/

# Worldcoin Configuration
NEXT_PUBLIC_WORLDCOIN_APP_ID=app_...
NEXT_PUBLIC_WORLDCOIN_ACTION=verify

# Analytics (Optional)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
34 changes: 34 additions & 0 deletions app/(dashboard)/history/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { ClaimHistoryList } from '@/components/features/ClaimHistoryList';
import { useAuth } from '@/hooks/useAuth';

export default function ClaimHistoryPage() {
const { user } = useAuth();

if (!user?.address) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto py-8 px-4">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Claim History</h1>
<p className="text-gray-600">Please connect your wallet to view your claim history.</p>
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto py-8 px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Claim History</h1>
<p className="text-gray-600">View all claims you've submitted and their current status.</p>
</div>

<ClaimHistoryList userAddress={user.address} />
</div>
</div>
);
}
73 changes: 73 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
23 changes: 23 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AuthProvider } from '@/hooks/useAuth';
import './globals.css';

export const metadata = {
title: 'TruthBounty - Decentralized News Verification',
description: 'Community-driven fact-checking across Ethereum and Stellar ecosystems',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
39 changes: 39 additions & 0 deletions components/features/ClaimCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from 'next/link';
import { Claim } from '@/types/claim';
import { ClaimStatusBadge } from './ClaimStatusBadge';

interface ClaimCardProps {
claim: Claim;
}

export function ClaimCard({ claim }: ClaimCardProps) {
return (
<div className="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-2">
<Link
href={`/claims/${claim.id}`}
className="hover:text-blue-600 transition-colors"
>
{claim.title}
</Link>
</h3>
<ClaimStatusBadge status={claim.status} />
</div>

<p className="text-gray-600 text-sm mb-4 line-clamp-3">
{claim.description}
</p>

<div className="flex justify-between items-center text-xs text-gray-500">
<div className="flex gap-4">
<span>Verifications: {claim.verificationCount}</span>
<span>Disputes: {claim.disputeCount}</span>
</div>
<span>
{new Date(claim.createdAt).toLocaleDateString()}
</span>
</div>
</div>
);
}
119 changes: 119 additions & 0 deletions components/features/ClaimHistoryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use client';

import { useState } from 'react';
import { useUserClaims, useUserClaimsInfinite } from '@/lib/api/claims';
import { ClaimCard } from './ClaimCard';
import { Button } from '@/components/ui/Button';

interface ClaimHistoryListProps {
userAddress: string;
useInfiniteScroll?: boolean;
}

export function ClaimHistoryList({ userAddress, useInfiniteScroll = true }: ClaimHistoryListProps) {
const [page, setPage] = useState(1);

const {
data: paginatedData,
isLoading: isLoadingPaginated,
error: errorPaginated,
} = useUserClaims(userAddress, page, 10);

const {
data: infiniteData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: isLoadingInfinite,
error: errorInfinite,
} = useUserClaimsInfinite(userAddress, 10);

const isLoading = useInfiniteScroll ? isLoadingInfinite : isLoadingPaginated;
const error = useInfiniteScroll ? errorInfinite : errorPaginated;

const claims = useInfiniteScroll
? infiniteData?.pages.flatMap(page => page.claims) || []
: paginatedData?.claims || [];

const totalPages = paginatedData ? Math.ceil(paginatedData.total / paginatedData.pageSize) : 0;

if (isLoading && claims.length === 0) {
return (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="bg-white border border-gray-200 rounded-lg p-6">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-3 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-5/6 mb-4"></div>
<div className="flex justify-between">
<div className="h-3 bg-gray-200 rounded w-20"></div>
<div className="h-3 bg-gray-200 rounded w-16"></div>
</div>
</div>
</div>
))}
</div>
);
}

if (error) {
return (
<div className="text-center py-8">
<p className="text-red-600">Failed to load claims. Please try again.</p>
</div>
);
}

if (claims.length === 0) {
return (
<div className="text-center py-8">
<p className="text-gray-500">No claims found for this user.</p>
</div>
);
}

return (
<div className="space-y-4">
{claims.map((claim) => (
<ClaimCard key={claim.id} claim={claim} />
))}

{useInfiniteScroll ? (
hasNextPage && (
<div className="text-center pt-4">
<Button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
variant="outline"
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</Button>
</div>
)
) : (
<div className="flex justify-center gap-2 pt-4">
<Button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
variant="outline"
size="sm"
>
Previous
</Button>
<span className="flex items-center px-3 text-sm text-gray-600">
Page {page} of {totalPages}
</span>
<Button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
variant="outline"
size="sm"
>
Next
</Button>
</div>
)}
</div>
);
}
35 changes: 35 additions & 0 deletions components/features/ClaimStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Badge } from '@/components/ui/Badge';
import { ClaimStatus } from '@/types/claim';

interface ClaimStatusBadgeProps {
status: ClaimStatus;
}

const statusConfig = {
pending: {
label: 'Pending',
variant: 'warning' as const,
},
verified: {
label: 'Verified',
variant: 'success' as const,
},
disputed: {
label: 'Disputed',
variant: 'destructive' as const,
},
resolved: {
label: 'Resolved',
variant: 'default' as const,
},
};

export function ClaimStatusBadge({ status }: ClaimStatusBadgeProps) {
const config = statusConfig[status];

return (
<Badge variant={config.variant}>
{config.label}
</Badge>
);
}
26 changes: 26 additions & 0 deletions components/ui/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { cn } from '@/lib/utils';

interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning';
className?: string;
}

export function Badge({ children, variant = 'default', className }: BadgeProps) {
const baseStyles = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2';

const variants = {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground border border-input bg-background hover:bg-accent hover:text-accent-foreground',
success: 'bg-green-100 text-green-800 hover:bg-green-200',
warning: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200',
};

return (
<div className={cn(baseStyles, variants[variant], className)}>
{children}
</div>
);
}
Loading