diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e2ee959
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/app/(dashboard)/history/page.tsx b/app/(dashboard)/history/page.tsx
new file mode 100644
index 0000000..96905c6
--- /dev/null
+++ b/app/(dashboard)/history/page.tsx
@@ -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 (
+
+
+
+
Claim History
+
Please connect your wallet to view your claim history.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
Claim History
+
View all claims you've submitted and their current status.
+
+
+
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..c1b2bf2
--- /dev/null
+++ b/app/globals.css
@@ -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;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..8fb9fbf
--- /dev/null
+++ b/app/layout.tsx
@@ -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 (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/components/features/ClaimCard.tsx b/components/features/ClaimCard.tsx
new file mode 100644
index 0000000..f4b82f7
--- /dev/null
+++ b/components/features/ClaimCard.tsx
@@ -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 (
+
+
+
+
+ {claim.title}
+
+
+
+
+
+
+ {claim.description}
+
+
+
+
+ Verifications: {claim.verificationCount}
+ Disputes: {claim.disputeCount}
+
+
+ {new Date(claim.createdAt).toLocaleDateString()}
+
+
+
+ );
+}
diff --git a/components/features/ClaimHistoryList.tsx b/components/features/ClaimHistoryList.tsx
new file mode 100644
index 0000000..4788471
--- /dev/null
+++ b/components/features/ClaimHistoryList.tsx
@@ -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 (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Failed to load claims. Please try again.
+
+ );
+ }
+
+ if (claims.length === 0) {
+ return (
+
+
No claims found for this user.
+
+ );
+ }
+
+ return (
+
+ {claims.map((claim) => (
+
+ ))}
+
+ {useInfiniteScroll ? (
+ hasNextPage && (
+
+
+
+ )
+ ) : (
+
+
+
+ Page {page} of {totalPages}
+
+
+
+ )}
+
+ );
+}
diff --git a/components/features/ClaimStatusBadge.tsx b/components/features/ClaimStatusBadge.tsx
new file mode 100644
index 0000000..a6fcb59
--- /dev/null
+++ b/components/features/ClaimStatusBadge.tsx
@@ -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 (
+
+ {config.label}
+
+ );
+}
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
new file mode 100644
index 0000000..bfe280f
--- /dev/null
+++ b/components/ui/Badge.tsx
@@ -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 (
+
+ {children}
+
+ );
+}
diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx
new file mode 100644
index 0000000..0eefc32
--- /dev/null
+++ b/components/ui/Button.tsx
@@ -0,0 +1,33 @@
+import { cn } from '@/lib/utils';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg' | 'icon';
+}
+
+export function Button({ className, variant = 'default', size = 'default', ...props }: ButtonProps) {
+ const baseStyles = 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
+
+ const variants = {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ };
+
+ const sizes = {
+ default: 'h-10 px-4 py-2',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ icon: 'h-10 w-10',
+ };
+
+ return (
+
+ );
+}
diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts
new file mode 100644
index 0000000..7cce89a
--- /dev/null
+++ b/hooks/useAuth.ts
@@ -0,0 +1,79 @@
+'use client';
+
+import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+interface User {
+ address: string;
+ ens?: string;
+}
+
+interface AuthContextType {
+ user: User | null;
+ isConnected: boolean;
+ connect: () => Promise;
+ disconnect: () => void;
+ isLoading: boolean;
+}
+
+const AuthContext = createContext(undefined);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Check if wallet is already connected
+ checkConnection();
+ }, []);
+
+ const checkConnection = async () => {
+ try {
+ // This would integrate with wagmi/rainbowkit
+ // For now, we'll simulate a connected state
+ const address = localStorage.getItem('walletAddress');
+ if (address) {
+ setUser({ address });
+ }
+ } catch (error) {
+ console.error('Failed to check connection:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const connect = async () => {
+ try {
+ // This would integrate with wagmi/rainbowkit
+ // For now, we'll simulate connecting
+ const mockAddress = '0x1234567890123456789012345678901234567890';
+ setUser({ address: mockAddress });
+ localStorage.setItem('walletAddress', mockAddress);
+ } catch (error) {
+ console.error('Failed to connect:', error);
+ throw error;
+ }
+ };
+
+ const disconnect = () => {
+ setUser(null);
+ localStorage.removeItem('walletAddress');
+ };
+
+ const value = {
+ user,
+ isConnected: !!user,
+ connect,
+ disconnect,
+ isLoading,
+ };
+
+ return {children};
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
diff --git a/lib/api/claims.ts b/lib/api/claims.ts
new file mode 100644
index 0000000..59109ec
--- /dev/null
+++ b/lib/api/claims.ts
@@ -0,0 +1,72 @@
+import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
+import { Claim, UserClaimsResponse } from '@/types/claim';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
+
+interface ClaimsResponse {
+ data: Claim[];
+ pagination: {
+ page: number;
+ pageSize: number;
+ total: number;
+ hasNextPage: boolean;
+ };
+}
+
+export const useUserClaims = (address: string, page = 1, pageSize = 10) => {
+ return useQuery({
+ queryKey: ['userClaims', address, page, pageSize],
+ queryFn: async (): Promise => {
+ const response = await fetch(
+ `${API_BASE_URL}/users/${address}/claims?page=${page}&pageSize=${pageSize}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch user claims');
+ }
+
+ const data: ClaimsResponse = await response.json();
+
+ return {
+ claims: data.data,
+ total: data.pagination.total,
+ page: data.pagination.page,
+ pageSize: data.pagination.pageSize,
+ };
+ },
+ enabled: !!address,
+ staleTime: 60 * 1000, // 1 minute
+ });
+};
+
+export const useUserClaimsInfinite = (address: string, pageSize = 10) => {
+ return useInfiniteQuery({
+ queryKey: ['userClaimsInfinite', address, pageSize],
+ queryFn: async ({ pageParam = 1 }): Promise => {
+ const response = await fetch(
+ `${API_BASE_URL}/users/${address}/claims?page=${pageParam}&pageSize=${pageSize}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch user claims');
+ }
+
+ const data: ClaimsResponse = await response.json();
+
+ return {
+ claims: data.data,
+ total: data.pagination.total,
+ page: data.pagination.page,
+ pageSize: data.pagination.pageSize,
+ };
+ },
+ enabled: !!address,
+ getNextPageParam: (lastPage) => {
+ if (lastPage.page * lastPage.pageSize < lastPage.total) {
+ return lastPage.page + 1;
+ }
+ return undefined;
+ },
+ staleTime: 60 * 1000,
+ });
+};
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..9ad0df4
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..ab74d30
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,14 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ experimental: {
+ appDir: true,
+ },
+ typescript: {
+ ignoreBuildErrors: false,
+ },
+ eslint: {
+ ignoreDuringBuilds: false,
+ },
+};
+
+module.exports = nextConfig;
diff --git a/package.json b/package.json
index 83f977e..99661e5 100644
--- a/package.json
+++ b/package.json
@@ -6,41 +6,6 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "eslint",
- "type-check": "tsc --noEmit",
- "test": "jest",
- "test:e2e": "playwright test"
- },
- "dependencies": {
- "@radix-ui/react-slot": "^1.2.4",
- "@rainbow-me/rainbowkit": "^2.0.0",
- "@tanstack/react-query": "^5.28.0",
- "@wagmi/core": "^2.5.0",
- "axios": "^1.6.0",
- "class-variance-authority": "^0.7.1",
- "lucide-react": "^0.563.0",
- "next": "16.1.6",
- "react": "19.2.3",
- "react-dom": "19.2.3",
- "tailwind-merge": "^3.4.0",
- "viem": "^2.7.0",
- "wagmi": "^2.5.0"
- },
- "devDependencies": {
- "@playwright/test": "^1.40.0",
- "@tailwindcss/postcss": "^4",
- "@testing-library/jest-dom": "^6.1.5",
- "@testing-library/react": "^14.1.0",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "babel-plugin-react-compiler": "1.0.0",
- "eslint": "^9",
- "eslint-config-next": "16.1.6",
- "jest": "^29.7.0",
- "prettier": "^3.1.0",
- "tailwindcss": "^4",
- "tailwindcss-animate": "^1.0.7",
- "typescript": "^5"
+
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..da23e8e
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,76 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: ["class"],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+}
diff --git a/tsconfig.json b/tsconfig.json
index cf9c65d..1f02a60 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,6 @@
{
"compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
+
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,24 +10,13 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react-jsx",
+
"incremental": true,
"plugins": [
{
"name": "next"
}
],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts",
- "**/*.mts"
- ],
+
"exclude": ["node_modules"]
}
diff --git a/types/claim.ts b/types/claim.ts
new file mode 100644
index 0000000..bb6372d
--- /dev/null
+++ b/types/claim.ts
@@ -0,0 +1,21 @@
+export type ClaimStatus = 'pending' | 'verified' | 'disputed' | 'resolved';
+
+export interface Claim {
+ id: string;
+ title: string;
+ description: string;
+ status: ClaimStatus;
+ submitter: string;
+ evidence: string[];
+ createdAt: string;
+ updatedAt: string;
+ verificationCount: number;
+ disputeCount: number;
+}
+
+export interface UserClaimsResponse {
+ claims: Claim[];
+ total: number;
+ page: number;
+ pageSize: number;
+}