This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { ReactNode } from 'react';
interface AuthGuardProps {
children: ReactNode;
fallback?: ReactNode;
}
/**
* Component that requires authentication
* Shows fallback or warning if user is not authenticated
*/
export function AuthGuard({ children, fallback }: AuthGuardProps) {
const { isConnected } = useWalletConnection();
const { isAuthenticated, authenticate, isAuthenticating } = useAuth();
if (!isConnected) {
return (
fallback || (
<div className="text-center py-12">
<div className="max-w-md mx-auto bg-green-50 border border-green-200 p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-2">
🔌 Wallet Not Connected
</h3>
<p className="text-gray-700">
Please connect your wallet to access this feature.
</p>
</div>
</div>
)
);
}
if (!isAuthenticated) {
return (
fallback || (
<div className="text-center py-12">
<div className="max-w-md mx-auto bg-green-50 border border-green-200 p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
🔐 Authentication Required
</h3>
<p className="text-gray-700 mb-4">
Please sign a message to verify your wallet ownership.
</p>
<button
onClick={() => authenticate()}
disabled={isAuthenticating}
className="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:bg-gray-400"
>
{isAuthenticating ? 'Authenticating...' : 'Sign Message'}
</button>
</div>
</div>
)
);
}
return <>{children}</>;
}

View File

@@ -0,0 +1,70 @@
'use client';
import { WalletButton } from '@/components/wallet/WalletButton';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { Menu, X } from "lucide-react";
import Link from 'next/link';
import { useState } from 'react';
export function Navbar() {
const { isConnected } = useWalletConnection();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-white border-b-2 border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 font-bold text-xl">
<div className="px-2 bg-primary flex items-center justify-center border-2 border-primary">
<span className="text-white text-2xl font-bold">D.M.T.P</span>
</div>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-1">
<Link href="/tasks" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
Marketplace
</Link>
<Link href="/dashboard" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
Dashboard
</Link>
<Link href="/profile" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
Profile
</Link>
</div>
{/* Right Actions */}
<div className="flex items-center gap-4">
<div className="hidden sm:inline-flex">
<WalletButton />
</div>
{/* Mobile Menu Button */}
<button className="md:hidden p-2 hover:bg-gray-100 rounded" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden pb-4 space-y-1 border-t border-gray-200 pt-2">
<Link href="/tasks" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
Marketplace
</Link>
<Link href="/dashboard" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
Dashboard
</Link>
<Link href="/profile" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
Profile
</Link>
<div className="px-4 py-2">
<WalletButton />
</div>
</div>
)}
</div>
</nav>
);
}

View File

@@ -0,0 +1,132 @@
"use client"
import { Github, Linkedin, Mail, Twitter } from "lucide-react"
import Link from "next/link"
export function Footer() {
const currentYear = new Date().getFullYear()
return (
<footer className="bg-gray-50 border-t-2 border-gray-200 mt-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
{/* Brand */}
<div>
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-8 bg-primary flex items-center justify-center border-2 border-primary">
<span className="text-white text-sm font-bold">C</span>
</div>
<span className="font-bold text-lg text-gray-900">
D.M.T.P
</span>
</div>
<p className="text-gray-600 text-sm">AI-powered microtask marketplace on Celo Sepolia</p>
</div>
{/* Product */}
<div>
<h3 className="font-bold mb-4 text-gray-900">Product</h3>
<ul className="space-y-2 text-sm text-gray-600">
<li>
<Link href="/marketplace" className="hover:text-primary transition-colors">
Marketplace
</Link>
</li>
<li>
<Link href="/dashboard" className="hover:text-primary transition-colors">
Dashboard
</Link>
</li>
<li>
<Link href="#" className="hover:text-primary transition-colors">
Pricing
</Link>
</li>
<li>
<Link href="#" className="hover:text-primary transition-colors">
Features
</Link>
</li>
</ul>
</div>
{/* Company */}
<div>
<h3 className="font-bold mb-4 text-gray-900">Company</h3>
<ul className="space-y-2 text-sm text-gray-600">
<li>
<Link href="#" className="hover:text-primary transition-colors">
About
</Link>
</li>
<li>
<Link href="#" className="hover:text-primary transition-colors">
Blog
</Link>
</li>
<li>
<Link href="#" className="hover:text-primary transition-colors">
Careers
</Link>
</li>
<li>
<Link href="#" className="hover:text-primary transition-colors">
Contact
</Link>
</li>
</ul>
</div>
{/* Social */}
<div>
<h3 className="font-bold mb-4 text-gray-900">Follow Us</h3>
<div className="flex gap-4">
<a
href="#"
className="text-gray-600 hover:text-primary transition-colors"
>
<Twitter className="w-5 h-5" />
</a>
<a
href="#"
className="text-gray-600 hover:text-primary transition-colors"
>
<Github className="w-5 h-5" />
</a>
<a
href="#"
className="text-gray-600 hover:text-primary transition-colors"
>
<Linkedin className="w-5 h-5" />
</a>
<a
href="#"
className="text-gray-600 hover:text-primary transition-colors"
>
<Mail className="w-5 h-5" />
</a>
</div>
</div>
</div>
{/* Divider */}
<div className="border-t-2 border-gray-200 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center text-sm text-gray-600">
<p>&copy; {currentYear} D.M.T.P. All rights reserved.</p>
<div className="flex gap-6 mt-4 md:mt-0">
<Link href="#" className="hover:text-primary transition-colors">
Privacy Policy
</Link>
<Link href="#" className="hover:text-primary transition-colors">
Terms of Service
</Link>
<Link href="#" className="hover:text-primary transition-colors">
Cookie Policy
</Link>
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,64 @@
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useEffect, useState } from 'react';
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export function AuthModal({ isOpen, onClose, onSuccess }: AuthModalProps) {
const { authenticate, isAuthenticating, authError } = useAuth();
const handleAuthenticate = async () => {
const success = await authenticate();
if (success) {
onSuccess?.();
onClose();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 max-w-md w-full mx-4">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
🔐 Authentication Required
</h2>
<p className="text-gray-700 mb-6">
Your session has expired. Please sign a message with your wallet to continue.
</p>
{authError && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 mb-4">
{authError}
</div>
)}
<div className="flex gap-3">
<button
onClick={handleAuthenticate}
disabled={isAuthenticating}
className="flex-1 px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:bg-gray-400"
>
{isAuthenticating ? 'Authenticating...' : 'Sign Message'}
</button>
<button
onClick={onClose}
disabled={isAuthenticating}
className="px-4 py-2 bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors"
>
Cancel
</button>
</div>
<p className="text-xs text-gray-500 mt-4">
This signature will not trigger any blockchain transaction or cost gas fees.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
'use client';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { getCurrentNetwork } from '@/lib/celo';
import { useState } from 'react';
import { LoadingSpinner } from '../ui/LoadingSpinner';
interface NetworkSwitchModalProps {
isOpen: boolean;
onClose: () => void;
}
export function NetworkSwitchModal({ isOpen, onClose }: NetworkSwitchModalProps) {
const { switchNetwork } = useWalletConnection();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!isOpen) return null;
const network = getCurrentNetwork();
const handleSwitch = async () => {
setIsLoading(true);
setError(null);
try {
await switchNetwork();
onClose();
} catch (err: any) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Wrong Network</h2>
<p className="text-gray-600 mb-6">
Please switch to <span className="font-semibold">{network.name}</span> to continue.
</p>
{error && (
<div className="bg-red-50 border border-red-200 p-4 mb-6">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div className="flex gap-3">
<button
onClick={onClose}
className="flex-1 py-3 px-4 border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors font-medium"
>
Cancel
</button>
<button
onClick={handleSwitch}
disabled={isLoading}
className="flex-1 py-3 px-4 bg-blue-600 text-white hover:bg-blue-700 transition-colors font-medium disabled:bg-gray-400"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<LoadingSpinner size="sm" />
Switching...
</span>
) : (
'Switch Network'
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
'use client';
import { useTransactions } from '@/hooks/useTransactions';
import { getExplorerUrl } from '@/lib/celo';
import { LoadingSpinner } from '../ui/LoadingSpinner';
export function TransactionModal() {
const { currentTx, updateTransaction } = useTransactions();
if (!currentTx) return null;
const handleClose = () => {
updateTransaction(currentTx.hash, { ...currentTx });
// You might want to actually close the modal here
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white max-w-md w-full p-6">
{/* Status Icon */}
<div className="flex justify-center mb-4">
{currentTx.status === 'pending' && (
<LoadingSpinner size="lg" />
)}
{currentTx.status === 'success' && (
<div className="text-6xl"></div>
)}
{currentTx.status === 'failed' && (
<div className="text-6xl"></div>
)}
</div>
{/* Title */}
<h2 className="text-2xl font-bold text-center text-gray-900 mb-2">
{currentTx.status === 'pending' && 'Transaction Pending'}
{currentTx.status === 'success' && 'Transaction Successful'}
{currentTx.status === 'failed' && 'Transaction Failed'}
</h2>
{/* Description */}
<p className="text-center text-gray-600 mb-6">{currentTx.description}</p>
{/* Transaction Hash */}
{currentTx.hash && (
<div className="bg-gray-50 p-4 mb-6">
<p className="text-sm text-gray-600 mb-2">Transaction Hash:</p>
<a
href={getExplorerUrl(currentTx.hash)}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono text-blue-600 hover:underline break-all"
>
{currentTx.hash}
</a>
</div>
)}
{/* Actions */}
{currentTx.status !== 'pending' && (
<button
onClick={handleClose}
className="w-full py-3 bg-blue-600 text-white hover:bg-blue-700 transition-colors font-medium"
>
Close
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
'use client';
import { formatCurrency, formatTimeRemaining } from '@/lib/utils';
import { Task, TaskType } from '@/types';
import Link from 'next/link';
interface TaskCardProps {
task: Task;
}
export function TaskCard({ task }: TaskCardProps) {
const taskTypeLabels: Record<TaskType, string> = {
[TaskType.TEXT_VERIFICATION]: 'Text',
[TaskType.IMAGE_LABELING]: 'Image',
[TaskType.SURVEY]: 'Survey',
[TaskType.CONTENT_MODERATION]: 'Moderation',
};
return (
<Link href={`/tasks/${task.id}`}>
<div className="bg-white border-2 border-gray-200 hover:border-primary transition-colors p-6 cursor-pointer">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<h3 className="text-base font-bold text-gray-900 line-clamp-2">
{task.title}
</h3>
{task.paymentAmount >= 5 && (
<span className="text-xs bg-primary text-white px-2 py-1 font-semibold ml-2 flex-shrink-0">
HIGH
</span>
)}
</div>
{/* Description */}
<p className="text-sm text-gray-600 mb-4 line-clamp-2 leading-relaxed">{task.description}</p>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-4 mb-4 pb-4 border-b border-gray-200">
<div>
<div className="text-xs text-gray-500 mb-1">Payment</div>
<div className="font-bold text-primary text-base">
{formatCurrency(task.paymentAmount)}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Available</div>
<div className="font-semibold text-gray-900 text-base">
{task.spotsRemaining}/{task.maxSubmissions}
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between">
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-700 border border-gray-200 font-medium">
{taskTypeLabels[task.taskType]}
</span>
<span className="text-xs text-gray-500 font-medium">
{formatTimeRemaining(task.expiresAt)}
</span>
</div>
</div>
</Link>
);
}

View File

@@ -0,0 +1,11 @@
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import * as React from "react"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -0,0 +1,13 @@
export function LoadingSpinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
const sizes = {
sm: 'w-4 h-4 border-2',
md: 'w-6 h-6 border-3',
lg: 'w-8 h-8 border-4',
};
return (
<div
className={`${sizes[size]} border-primary border-t-transparent rounded-full animate-spin`}
/>
);
}

View File

@@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-semibold transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 border-2 border-primary",
destructive:
"bg-destructive text-white hover:bg-destructive/90 border-2 border-destructive",
outline:
"border-2 bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-2 border-secondary",
ghost:
"hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-5 py-2 has-[>svg]:px-4",
sm: "h-9 px-3 has-[>svg]:px-2.5",
lg: "h-11 px-7 has-[>svg]:px-5",
icon: "size-10",
"icon-sm": "size-9",
"icon-lg": "size-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 border-2 py-6",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,102 @@
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useCUSDBalance } from '@/hooks/useCUSDBalance';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { formatAddress } from '@/lib/celo';
import { useState } from 'react';
import { NetworkSwitchModal } from '../modals/NetworkSwitchModal';
export function WalletButton() {
const { address, isConnected, isConnecting, connect, disconnect, chainId } = useWalletConnection();
const { authenticate, isAuthenticating, clearAuth, isAuthenticated } = useAuth();
const { data: balance } = useCUSDBalance(address);
const [showNetworkModal, setShowNetworkModal] = useState(false);
const expectedChainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '11142220');
const isWrongNetwork = isConnected && chainId !== expectedChainId;
const handleConnect = async () => {
try {
// Step 1: Connect wallet
await connect();
// Step 2: Authenticate
await authenticate();
} catch (error) {
console.error('Connection/Authentication error:', error);
}
};
const handleDisconnect = () => {
// Clear authentication data
clearAuth();
// Disconnect wallet
disconnect();
};
// Show "Re-authenticate" button if connected but not authenticated
if (isConnected && address && !isAuthenticated && !isWrongNetwork) {
return (
<button
onClick={() => authenticate()}
disabled={isAuthenticating}
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
>
{isAuthenticating ? 'Authenticating...' : '🔐 Sign to Authenticate'}
</button>
);
}
if (isWrongNetwork) {
return (
<>
<button
onClick={() => setShowNetworkModal(true)}
className="px-6 py-2 bg-red-600 text-white hover:bg-red-700 transition-colors font-medium"
>
Wrong Network
</button>
<NetworkSwitchModal
isOpen={showNetworkModal}
onClose={() => setShowNetworkModal(false)}
/>
</>
);
}
if (isConnected && address) {
return (
<div className="flex items-center gap-3">
{/* Balance */}
<div className="hidden md:block px-4 py-2 bg-green-50 text-green-900 text-sm font-medium">
{parseFloat(balance || '0').toFixed(2)} cUSD
</div>
{/* Address */}
<div className="px-4 py-2 bg-blue-50 text-blue-900 text-sm font-medium">
{formatAddress(address)}
</div>
{/* Disconnect */}
<button
onClick={handleDisconnect}
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
>
Disconnect
</button>
</div>
);
}
return (
<button
onClick={handleConnect}
disabled={isConnecting || isAuthenticating}
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
>
{isConnecting || isAuthenticating ? 'Connecting...' : 'Connect Wallet'}
</button>
);
}