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,31 @@
const badges = [
{ name: "Quick Starter", icon: "⚡", earned: true },
{ name: "Accuracy Master", icon: "🎯", earned: true },
{ name: "Streak Champion", icon: "🔥", earned: true },
{ name: "Top Performer", icon: "👑", earned: false },
{ name: "Community Helper", icon: "🤝", earned: false },
{ name: "Legendary Worker", icon: "⭐", earned: false },
]
export function BadgeShowcase() {
return (
<div>
<h3 className="font-semibold text-lg mb-4">Badges</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
{badges.map((badge, index) => (
<div
key={index}
className={`p-4 text-center transition-all ${
badge.earned
? "bg-card/80 backdrop-blur-md border border-border hover:bg-card/90 transition-colors duration-200"
: "bg-card/50 opacity-50"
}`}
>
<div className="text-3xl mb-2">{badge.icon}</div>
<p className="text-xs font-medium">{badge.name}</p>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,74 @@
"use client"
import { motion } from "framer-motion"
import { Brain, Wallet, Zap } from "lucide-react"
export function FeatureShowcase() {
const features = [
{
title: "Seamless Task Marketplace",
description: "Browse thousands of AI-verified tasks. Filter by category, difficulty, and earning potential.",
icon: Zap,
delay: 0,
},
{
title: "AI-Powered Verification",
description: "Advanced AI models verify your work instantly. Get paid only for quality submissions.",
icon: Brain,
delay: 0.1,
},
{
title: "Instant Payments",
description: "Earn cUSD instantly on Celo Sepolia. Withdraw anytime with zero fees.",
icon: Wallet,
delay: 0.2,
},
]
return (
<section className="relative py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 border-y-2 border-gray-200">
<div className="max-w-7xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
Why Choose{" "}
<span className="text-primary">D.M.T.P</span>
</h2>
<p className="text-gray-600 text-base max-w-2xl mx-auto">
The most advanced AI-powered microtask platform with instant payments and transparent verification.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-6">
{features.map((feature, i) => {
const Icon = feature.icon
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: feature.delay }}
viewport={{ once: true }}
>
<div className="bg-white border-2 border-gray-200 p-6 h-full hover:border-primary transition-colors">
{/* Icon */}
<div className="w-12 h-12 bg-primary flex items-center justify-center mb-4 border-2 border-primary">
<Icon className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-bold mb-2 text-gray-900">{feature.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">{feature.description}</p>
</div>
</motion.div>
)
})}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,150 @@
"use client"
import { Button } from "@/components/ui/button"
import { useAuth } from "@/hooks/useAuth";
import { useCUSDBalance } from "@/hooks/useCUSDBalance";
import { useWalletConnection } from "@/hooks/useWalletConnection";
import { motion } from "framer-motion"
import { ArrowRight, CheckCircle } from "lucide-react"
import { useState } from "react";
export function HeroSection() {
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);
}
};
return (
<section className="relative min-h-[600px] flex items-center justify-center overflow-hidden px-4 sm:px-6 lg:px-8 py-20 pt-24 bg-white border-b-2 border-gray-200">
{/* Green accent border on top */}
<div className="absolute top-0 left-0 right-0 h-1 bg-primary" />
{/* Content */}
<div className="relative z-10 max-w-5xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Column - Main Content */}
<div>
<motion.div
className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 border-2 border-primary bg-green-50"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<CheckCircle className="w-4 h-4 text-primary" />
<span className="text-sm font-semibold text-primary">
AI-Powered Verification
</span>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6 leading-tight text-gray-900">
Complete tasks.{" "}
<span className="text-primary">
Get verified by AI.
</span>{" "}
<span className="text-gray-900">Earn instantly.</span>
</h1>
</motion.div>
<motion.p
className="text-lg text-gray-600 mb-8 leading-relaxed"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
Join the AI-powered microtask marketplace on Celo Sepolia. Complete data labeling, surveys, and content
moderation tasks. Get paid in cUSD instantly.
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<Button
onClick={handleConnect}
size="lg"
className="font-semibold"
>
Connect Wallet <ArrowRight className="w-4 h-4 ml-2" />
</Button>
<Button
size="lg"
variant="outline"
>
Try Demo
</Button>
</motion.div>
{/* Feature Pills */}
<motion.div
className="flex flex-wrap items-center gap-4 text-sm text-gray-600"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
{[
{ label: "Powered by Gemini AI" },
{ label: "Built on Celo Sepolia" },
{ label: "Instant Payments" },
].map((item, i) => (
<div
key={i}
className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 border border-gray-200"
>
<div className="w-1.5 h-1.5 bg-primary " />
{item.label}
</div>
))}
</motion.div>
</div>
{/* Right Column - Stats Box */}
<motion.div
className="bg-gray-50 border-2 border-gray-200 p-8"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h3 className="text-lg font-bold text-gray-900 mb-6 pb-3 border-b-2 border-gray-200">Platform Statistics</h3>
<div className="space-y-6">
<div>
<div className="text-3xl font-bold text-primary mb-1">2,847</div>
<div className="text-sm text-gray-600">Active Tasks</div>
</div>
<div className="border-t border-gray-200 pt-6">
<div className="text-3xl font-bold text-primary mb-1">$24,392</div>
<div className="text-sm text-gray-600">Paid This Week</div>
</div>
<div className="border-t border-gray-200 pt-6">
<div className="text-3xl font-bold text-primary mb-1">15,234</div>
<div className="text-sm text-gray-600">Verified Workers</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,79 @@
"use client"
import { motion } from "framer-motion"
import { CheckSquare, TrendingUp, Wallet, Zap } from "lucide-react"
const steps = [
{
icon: Wallet,
title: "Connect Wallet",
description: "Link your Celo wallet to get started in seconds",
},
{
icon: CheckSquare,
title: "Complete Tasks",
description: "Choose from available tasks and complete them",
},
{
icon: Zap,
title: "AI Verification",
description: "Gemini AI verifies your work instantly",
},
{
icon: TrendingUp,
title: "Earn & Withdraw",
description: "Get paid in cUSD directly to your wallet",
},
]
export function HowItWorks() {
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-6xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
How it{" "}
<span className="text-primary">works</span>
</h2>
<p className="text-base text-gray-600">Get started in 4 simple steps</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{steps.map((step, index) => (
<motion.div
key={index}
className="relative"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
{index < steps.length - 1 && (
<div className="hidden md:block absolute top-12 left-[60%] w-[calc(100%-60px)] h-0.5 bg-gray-300" />
)}
<div className="relative bg-white border-2 border-gray-200 hover:border-primary p-6 text-center transition-colors">
<div className="w-16 h-16 bg-primary flex items-center justify-center mx-auto mb-4 border-2 border-primary">
<step.icon className="w-8 h-8 text-white" />
</div>
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 bg-primary text-white text-sm font-bold w-7 h-7 flex items-center justify-center border-2 border-white">
{index + 1}
</div>
<h3 className="font-bold text-base mb-2 text-gray-900">{step.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">{step.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,78 @@
"use client"
import { motion } from "framer-motion"
import { Code2, Database, MessageSquare, Zap } from "lucide-react"
export function IntegrationsSection() {
const integrations = [
{
icon: Code2,
title: "API Integration",
description: "Connect with your favorite tools to streamline workflows",
},
{
icon: Database,
title: "Data Sync",
description: "Seamless data synchronization across platforms",
},
{
icon: Zap,
title: "Automation",
description: "Automate repetitive tasks with intelligent workflows",
},
{
icon: MessageSquare,
title: "Communication",
description: "Real-time notifications and updates",
},
]
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-b-2 border-gray-200">
<div className="max-w-6xl mx-auto">
{/* Header */}
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<div className="inline-block mb-4 px-3 py-1.5 border-2 border-primary bg-green-50">
<span className="text-sm font-bold text-primary">INTEGRATIONS</span>
</div>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
Seamless{" "}
<span className="text-primary">
Integrations
</span>
</h2>
<p className="text-base text-gray-600">Connect with your favorite tools to streamline workflows</p>
</motion.div>
{/* Integration Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{integrations.map((integration, index) => {
const Icon = integration.icon
return (
<motion.div
key={index}
className="flex flex-col items-center text-center"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="w-20 h-20 bg-primary border-2 border-primary flex items-center justify-center mb-4">
<Icon className="w-10 h-10 text-white" />
</div>
<h3 className="font-bold text-base mb-2 text-gray-900">{integration.title}</h3>
<p className="text-sm text-gray-600 leading-relaxed">{integration.description}</p>
</motion.div>
)
})}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,26 @@
export function ReputationMeter() {
const reputation = 78
const nextLevel = 85
return (
<div className="mb-8">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm text-foreground/60 mb-1">Reputation Score</p>
<p className="text-3xl font-bold">{reputation}</p>
</div>
<div className="text-right">
<p className="text-sm text-foreground/60 mb-1">Next Level</p>
<p className="text-lg font-semibold text-green-400">{nextLevel}</p>
</div>
</div>
<div className="w-full bg-black/50 h-3 overflow-hidden border border-green-500/20">
<div
className="h-full bg-linear-to-r from-green-500 to-green-600 transition-all duration-500"
style={{ width: `${(reputation / nextLevel) * 100}%` }}
/>
</div>
<p className="text-sm text-foreground/60 mt-2">{nextLevel - reputation} points until next level</p>
</div>
)
}

View File

@@ -0,0 +1,37 @@
"use client"
import { motion } from "framer-motion"
const stats = [
{ label: "Active Workers", value: "12,450", suffix: "+" },
{ label: "Tasks Completed", value: "2.3M", suffix: "" },
{ label: "Total Earnings", value: "$450K", suffix: "" },
{ label: "Avg Task Pay", value: "$2.50", suffix: "" },
]
export function StatsSection() {
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white border-y-2 border-gray-200">
<div className="max-w-6xl mx-auto">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{stats.map((stat, index) => (
<motion.div
key={index}
className="text-center py-4 border-r-2 border-gray-200 last:border-r-0"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="text-3xl sm:text-4xl font-bold mb-2 text-primary">
{stat.value}
<span className="text-primary">{stat.suffix}</span>
</div>
<p className="text-sm sm:text-base text-gray-600 font-medium">{stat.label}</p>
</motion.div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,94 @@
"use client"
import { motion } from "framer-motion"
import { CheckCircle2 } from "lucide-react"
const taskExamples = [
{
category: "Data Labeling",
tasks: ["Image classification", "Object detection", "Text annotation"],
earning: "$1.50 - $5.00",
time: "5-15 min",
},
{
category: "Content Moderation",
tasks: ["Review flagged content", "Verify guidelines compliance", "Quality assurance"],
earning: "$2.00 - $6.00",
time: "10-20 min",
},
{
category: "Surveys & Research",
tasks: ["Market research", "User feedback", "Opinion surveys"],
earning: "$1.00 - $4.00",
time: "5-10 min",
},
{
category: "Transcription",
tasks: ["Audio transcription", "Video captioning", "Translation"],
earning: "$3.00 - $8.00",
time: "15-30 min",
},
]
export function TaskExamples() {
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-6xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
Available{" "}
<span className="text-primary">
Task Types
</span>
</h2>
<p className="text-base text-gray-600">Choose from diverse tasks that match your skills and schedule</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{taskExamples.map((task, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="bg-white border-2 border-gray-200 hover:border-primary p-6 transition-colors h-full">
<h3 className="text-lg font-bold mb-4 text-primary">
{task.category}
</h3>
<ul className="space-y-2 mb-6">
{task.tasks.map((item, i) => (
<li
key={i}
className="flex items-center gap-2 text-gray-700 text-sm"
>
<CheckCircle2 className="w-4 h-4 text-primary shrink-0" />
{item}
</li>
))}
</ul>
<div className="grid grid-cols-2 gap-4 pt-4 border-t-2 border-gray-200">
<div>
<p className="text-xs text-gray-500 mb-1">Earning</p>
<p className="font-bold text-primary text-sm">{task.earning}</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-500 mb-1">Time</p>
<p className="font-bold text-gray-900 text-sm">{task.time}</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,229 @@
"use client"
import { motion } from "framer-motion"
import { Star } from "lucide-react"
import { useEffect, useState } from "react"
const testimonials = [
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
{
name: "Sarah Chen",
role: "Student",
content: "I earn $200-300 per week doing tasks in my spare time. The AI verification is super fast!",
rating: 5,
avatar: "SC",
},
{
name: "Marcus Johnson",
role: "Freelancer",
content: "Finally a platform where I get paid instantly. No more waiting for payments. Love it!",
rating: 5,
avatar: "MJ",
},
{
name: "Elena Rodriguez",
role: "Remote Worker",
content: "The variety of tasks keeps things interesting. I've earned over $2000 in 3 months.",
rating: 5,
avatar: "ER",
},
{
name: "James Park",
role: "Side Hustler",
content: "Best platform I've used. Transparent, fair, and the Celo integration is seamless.",
rating: 5,
avatar: "JP",
},
]
export function Testimonials() {
const [currentIndex, setCurrentIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % testimonials.length)
}, 5000)
return () => clearInterval(interval)
}, [])
return (
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 border-y-2 border-gray-200">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">Loved by workers worldwide</h2>
<p className="text-base text-gray-600">Join thousands earning on D.M.T.P</p>
</div>
<div className="relative overflow-hidden">
<motion.div
className="flex gap-6"
animate={{ x: -currentIndex * (100 + 24) + "%" }}
transition={{ duration: 0.8, ease: "easeInOut" }}
>
{testimonials.map((testimonial, index) => (
<motion.div key={index} className="flex-shrink-0 w-full md:w-1/2 lg:w-1/3">
<div className="bg-white border-2 border-gray-200 p-6 hover:border-primary transition-colors h-full flex flex-col">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-primary flex items-center justify-center text-white font-bold border-2 border-primary">
{testimonial.avatar}
</div>
<div>
<p className="font-bold text-gray-900">{testimonial.name}</p>
<p className="text-sm text-gray-600">{testimonial.role}</p>
</div>
</div>
<div className="flex gap-1 mb-3">
{Array.from({ length: testimonial.rating }).map((_, i) => (
<Star key={i} className="w-4 h-4 fill-primary text-primary" />
))}
</div>
<p className="text-gray-700 text-sm leading-relaxed flex-grow">{testimonial.content}</p>
</div>
</motion.div>
))}
</motion.div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,430 @@
'use client';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { api } from '@/lib/api';
import { formatCurrency } from '@/lib/utils';
import { VerificationStatus } from '@/types';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { Award, CheckCircle2, Clock, ExternalLink, Lock, TrendingUp, Wallet, XCircle } from 'lucide-react';
import Link from 'next/link';
import { useEffect } from 'react';
import { Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
const earningsData = [
{ day: "Mon", earnings: 12.5 },
{ day: "Tue", earnings: 18.3 },
{ day: "Wed", earnings: 15.7 },
{ day: "Thu", earnings: 22.1 },
{ day: "Fri", earnings: 25.4 },
{ day: "Sat", earnings: 19.8 },
{ day: "Sun", earnings: 28.6 },
]
const tasksData = [
{ day: "Mon", completed: 4 },
{ day: "Tue", completed: 6 },
{ day: "Wed", completed: 5 },
{ day: "Thu", completed: 7 },
{ day: "Fri", completed: 8 },
{ day: "Sat", completed: 6 },
{ day: "Sun", completed: 9 },
]
export default function DashboardPage() {
const { isConnected, address, connect } = useWalletConnection();
const { data: profileData, isLoading: profileLoading, refetch: refetchProfile } = useQuery({
queryKey: ['profile'],
queryFn: () => api.users.getProfile(),
enabled: isConnected,
});
const { data: submissionsData, isLoading: submissionsLoading, refetch: refetchSubmissions } = useQuery({
queryKey: ['submissions'],
queryFn: () => api.submissions.mySubmissions(),
enabled: isConnected,
});
// Refetch when wallet connects
useEffect(() => {
if (isConnected) {
refetchProfile();
refetchSubmissions();
}
}, [isConnected, refetchProfile, refetchSubmissions]);
if (!isConnected) {
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-2xl mx-auto px-4 py-32 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<motion.div
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="inline-flex items-center justify-center w-24 h-24 bg-linear-to-br from-green-500/20 to-green-600/10 mb-6"
>
<Lock className="w-12 h-12 text-green-500" />
</motion.div>
<h2 className="text-4xl font-bold mb-4">
<span className="bg-linear-to-r from-green-400 via-green-500 to-green-600 bg-clip-text text-transparent">
Connect Your Wallet
</span>
</h2>
<p className="text-foreground/60 mb-8 text-lg">
Please connect your wallet to view your dashboard and track your earnings
</p>
<motion.button
onClick={connect}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-8 py-4 bg-linear-to-r from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all font-bold text-lg shadow-lg shadow-green-500/25 inline-flex items-center gap-2"
>
<Wallet className="w-5 h-5" />
Connect Wallet
</motion.button>
</motion.div>
</div>
</div>
);
}
if (profileLoading || submissionsLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner size="lg" />
</div>
);
}
const profile = profileData?.data;
const submissions = submissionsData?.data || [];
const stats = [
{
icon: Wallet,
label: 'Total Earnings',
value: formatCurrency(profile?.totalEarnings || 0),
gradient: 'from-green-500 to-green-600',
bgGradient: 'from-green-500/10 to-green-600/5',
border: 'border-green-500/30',
},
{
icon: CheckCircle2,
label: 'Completed Tasks',
value: profile?.stats?.submissionsApproved || 0,
gradient: 'from-green-500 to-green-600',
bgGradient: 'from-green-500/10 to-green-600/5',
border: 'border-green-500/30',
},
{
icon: TrendingUp,
label: 'Approval Rate',
value: `${profile?.stats?.approvalRate || 0}%`,
gradient: 'from-blue-500 to-blue-600',
bgGradient: 'from-blue-500/10 to-blue-600/5',
border: 'border-blue-500/30',
},
{
icon: Award,
label: 'Reputation',
value: `${profile?.reputationScore || 0}`,
gradient: 'from-green-400 to-green-500',
bgGradient: 'from-green-400/10 to-green-500/5',
border: 'border-green-400/30',
},
];
const getStatusConfig = (status: string) => {
switch (status) {
case VerificationStatus.APPROVED:
return { icon: CheckCircle2, color: 'text-green-600', bg: 'bg-green-100', text: 'Approved' };
case VerificationStatus.REJECTED:
return { icon: XCircle, color: 'text-red-600', bg: 'bg-red-100', text: 'Rejected' };
default:
return { icon: Clock, color: 'text-green-600', bg: 'bg-green-100', text: 'Pending' };
}
};
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 py-12 pt-32">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mb-8"
>
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="text-4xl font-bold mb-2">
<span className="bg-linear-to-r from-green-400 via-green-500 to-green-600 bg-clip-text text-transparent">
Dashboard
</span>
</h1>
<div className="flex items-center gap-2 text-sm text-foreground/60">
<Wallet className="w-4 h-4" />
<span className="font-mono">{address?.slice(0, 6)}...{address?.slice(-4)}</span>
</div>
</div>
<div className="inline-flex items-center gap-2 px-4 py-2 border border-green-500/30 bg-green-500/5 backdrop-blur-sm">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
>
<LoadingSpinner />
</motion.div>
<span className="text-sm font-semibold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Active Worker
</span>
</div>
</div>
</motion.div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
whileHover={{ y: -4 }}
className={`bg-linear-to-br ${stat.bgGradient} backdrop-blur-md border ${stat.border} p-6 group cursor-pointer`}
>
<div className="flex items-start justify-between mb-4">
<div className={`w-12 h-12 bg-linear-to-r ${stat.gradient} flex items-center justify-center`}>
<stat.icon className="w-6 h-6 text-white" />
</div>
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 2, repeat: Infinity, delay: index * 0.2 }}
className={`w-2 h-2 bg-linear-to-r ${stat.gradient} `}
/>
</div>
<div className="text-sm text-foreground/60 mb-1">{stat.label}</div>
<div className="text-3xl font-bold text-foreground">{stat.value}</div>
</motion.div>
))}
</div>
{/* Recent Submissions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-background/80 backdrop-blur-md border border-green-500/20 overflow-hidden"
>
<div className="p-6 border-b border-green-500/10">
<h2 className="text-2xl font-bold text-foreground flex items-center gap-2">
<LoadingSpinner />
Recent Submissions
</h2>
</div>
{submissions.length === 0 ? (
<div className="text-center py-20">
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="inline-flex items-center justify-center w-20 h-20 bg-linear-to-br from-green-500/20 to-green-600/10 mb-4"
>
<LoadingSpinner />
</motion.div>
<p className="text-foreground/60 mb-6 text-lg">No submissions yet</p>
<Link href="/tasks">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-linear-to-r from-green-500 to-green-600 text-white font-semibold inline-flex items-center gap-2"
>
Browse Tasks
<ExternalLink className="w-4 h-4" />
</motion.button>
</Link>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-foreground/5">
<tr>
<th className="px-6 py-4 text-left text-xs font-bold text-foreground/60 uppercase tracking-wider">
Task
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-foreground/60 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-foreground/60 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-foreground/60 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-4 text-left text-xs font-bold text-foreground/60 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody className="divide-y divide-green-500/10">
{submissions.map((submission: any, index: number) => {
const statusConfig = getStatusConfig(submission.verificationStatus);
const StatusIcon = statusConfig.icon;
return (
<motion.tr
key={submission.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-green-500/5 transition-colors"
>
<td className="px-6 py-4">
<div className="text-sm font-semibold text-foreground">
{submission.task.title}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm font-bold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{formatCurrency(submission.task.paymentAmount)}
</div>
</td>
<td className="px-6 py-4">
<span className={`px-3 py-1.5 inline-flex items-center gap-1.5 text-xs font-semibold ${statusConfig.bg} ${statusConfig.color}`}>
<StatusIcon className="w-3.5 h-3.5" />
{statusConfig.text}
</span>
</td>
<td className="px-6 py-4 text-sm text-foreground/60">
{new Date(submission.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4">
<Link
href={`/submissions/${submission.id}`}
className="text-sm text-green-500 hover:text-green-600 font-semibold inline-flex items-center gap-1 transition-colors"
>
View Details
<ExternalLink className="w-3.5 h-3.5" />
</Link>
</td>
</motion.tr>
);
})}
</tbody>
</table>
</div>
)}
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-8">
{/* Earnings Chart */}
<div className="bg-black/40 backdrop-blur-xl border border-green-500/20 p-6 hover:border-green-500/40 transition-colors">
<h3 className="font-semibold text-lg mb-4">Weekly Earnings</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={earningsData}>
<defs>
<linearGradient id="colorEarnings" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ff8c00" stopOpacity={0.8} />
<stop offset="95%" stopColor="#ffa500" stopOpacity={0.3} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,140,0,0.1)" />
<XAxis dataKey="day" stroke="rgba(255,255,255,0.5)" />
<YAxis stroke="rgba(255,255,255,0.5)" />
<Tooltip
contentStyle={{
backgroundColor: "rgba(15, 15, 15, 0.95)",
border: "1px solid rgba(255,140,0,0.3)",
borderRadius: "8px",
}}
/>
<Bar dataKey="earnings" fill="url(#colorEarnings)" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
{/* Tasks Chart */}
<div className="bg-black/40 backdrop-blur-xl border border-green-500/20 p-6 hover:border-green-500/40 transition-colors">
<h3 className="font-semibold text-lg mb-4">Tasks Completed</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={tasksData}>
<defs>
<linearGradient id="colorTasks" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#ff8c00" stopOpacity={1} />
<stop offset="100%" stopColor="#ffa500" stopOpacity={1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,140,0,0.1)" />
<XAxis dataKey="day" stroke="rgba(255,255,255,0.5)" />
<YAxis stroke="rgba(255,255,255,0.5)" />
<Tooltip
contentStyle={{
backgroundColor: "rgba(15, 15, 15, 0.95)",
border: "1px solid rgba(255,140,0,0.3)",
borderRadius: "8px",
}}
/>
<Line
type="monotone"
dataKey="completed"
stroke="url(#colorTasks)"
strokeWidth={3}
dot={{ fill: "#ff8c00", r: 5 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
}

BIN
dmtp/client/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

168
dmtp/client/app/globals.css Normal file
View File

@@ -0,0 +1,168 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.45 0.15 155);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.96 0 0);
--secondary-foreground: oklch(0.145 0 0);
--muted: oklch(0.96 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.50 0.15 155);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.85 0 0);
--input: oklch(0.85 0 0);
--ring: oklch(0.45 0.15 155);
--chart-1: oklch(0.55 0.15 155);
--chart-2: oklch(0.60 0.12 185);
--chart-3: oklch(0.40 0.07 230);
--chart-4: oklch(0.83 0.19 85);
--chart-5: oklch(0.77 0.19 70);
--radius: 0.25rem;
--sidebar: oklch(0.98 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.45 0.15 155);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.96 0 0);
--sidebar-accent-foreground: oklch(0.145 0 0);
--sidebar-border: oklch(0.85 0 0);
--sidebar-ring: oklch(0.45 0.15 155);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--font-sans: "Inter", "Inter Fallback";
/* McMaster-Carr inspired: Clean white with green accents */
--color-background: #ffffff;
--color-foreground: #1a1a1a;
--color-card: #ffffff;
--color-card-hover: #f9fafb;
/* Green Theme */
--color-primary: #008542;
--color-primary-dark: #006633;
--color-accent: #00a854;
--color-accent-light: #4caf50;
/* Semantic */
--color-success: #008542;
--color-warning: #f59e0b;
--color-error: #dc2626;
--color-info: #0066cc;
/* Borders & Dividers */
--color-border: #d1d5db;
--color-border-light: #e5e7eb;
/* Radius */
--radius: 0.5rem;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
/* Removed glow effects, added smooth transitions only */
.transition-smooth {
@apply transition-all duration-300 ease-out;
}
/* Scrollbar styling */
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-background;
}
::-webkit-scrollbar-thumb {
@apply bg-border hover:bg-border-light;
}

View File

@@ -0,0 +1,42 @@
import { Footer } from "@/components/layout/footer";
import { Navbar } from "@/components/layout/Navbar";
import { ThemeProvider } from "@/components/theme-provider";
import { AuthProvider } from "@/providers/AuthProvider";
import { QueryProvider } from "@/providers/QueryProvider";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Celo Task Marketplace",
description: "AI-powered micro-task marketplace on Celo blockchain",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.className} bg-white text-gray-900`}>
<QueryProvider>
<AuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={false}
disableTransitionOnChange
>
<Navbar />
<main>{children}</main>
<Footer />
</ThemeProvider>
</AuthProvider>
</QueryProvider>
</body>
</html>
);
}

213
dmtp/client/app/page.tsx Normal file
View File

@@ -0,0 +1,213 @@
"use client";
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import { ArrowRight, ChevronDown } from "lucide-react";
import { useState } from "react";
import { FeatureShowcase } from "./_sections/feature-showcase";
import { HeroSection } from "./_sections/hero-section";
import { HowItWorks } from "./_sections/how-it-works";
import { IntegrationsSection } from "./_sections/integrations-section";
import { StatsSection } from "./_sections/stats-section";
import { TaskExamples } from "./_sections/task-examples";
import { Testimonials } from "./_sections/testimonials";
export default function Home() {
const [expandedFaq, setExpandedFaq] = useState<number | null>(null);
const faqItems = [
{
q: "How much can I earn?",
a: "Earnings vary by task complexity. Most workers earn $200-500/month.",
},
{
q: "How long does verification take?",
a: "AI verification is instant. Most tasks are approved within seconds.",
},
{
q: "When do I get paid?",
a: "Payments are instant to your Celo wallet. No waiting periods.",
},
{
q: "Is there a minimum withdrawal?",
a: "No minimum. Withdraw any amount anytime to your wallet.",
},
];
return (
<main className="min-h-screen bg-white">
<HeroSection />
<StatsSection />
<FeatureShowcase />
<TaskExamples />
<HowItWorks />
<Testimonials />
<IntegrationsSection />
{/* Security & Trust Section */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 border-y-2 border-gray-200">
<div className="max-w-6xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
Secure &{" "}
<span className="text-primary">
Transparent
</span>
</h2>
<p className="text-base text-gray-600">
Your earnings and data are protected with blockchain technology
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-6">
{[
{
title: "Smart Contract Verified",
desc: "All payments verified on-chain",
},
{
title: "Zero Hidden Fees",
desc: "100% transparent pricing model",
},
{
title: "Instant Withdrawals",
desc: "Access your earnings anytime",
},
].map((item, i) => (
<motion.div
key={i}
className="bg-white border-2 border-gray-200 p-6 h-full hover:border-primary transition-colors"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: i * 0.1 }}
viewport={{ once: true }}
>
<h3 className="text-lg font-bold mb-2 text-gray-900">{item.title}</h3>
<p className="text-gray-600 text-sm">{item.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<h2 className="text-3xl sm:text-4xl font-bold mb-4 text-gray-900">
Frequently Asked{" "}
<span className="text-primary">
Questions
</span>
</h2>
</motion.div>
<div className="space-y-3">
{faqItems.map((item, i) => (
<motion.div
key={i}
className="bg-white border-2 border-gray-200 overflow-hidden hover:border-primary transition-colors"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: i * 0.1 }}
viewport={{ once: true }}
>
<button
onClick={() => setExpandedFaq(expandedFaq === i ? null : i)}
className="w-full p-5 flex items-center justify-between hover:bg-gray-50 transition-colors"
>
<h3 className="font-bold text-base text-left text-gray-900">{item.q}</h3>
<motion.div
animate={{ rotate: expandedFaq === i ? 180 : 0 }}
transition={{ duration: 0.3 }}
className="flex-shrink-0 ml-4"
>
<ChevronDown className="w-5 h-5 text-primary" />
</motion.div>
</button>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: expandedFaq === i ? "auto" : 0,
opacity: expandedFaq === i ? 1 : 0,
}}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="px-5 pb-5 text-gray-600 text-sm border-t-2 border-gray-100 pt-4">
{item.a}
</div>
</motion.div>
</motion.div>
))}
</div>
</div>
</section>
{/* Final CTA Section */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 border-t-2 border-gray-200">
<div className="max-w-4xl mx-auto text-center">
<motion.h2
className="text-3xl sm:text-4xl font-bold mb-6 text-gray-900"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
Ready to start{" "}
<span className="text-primary">
earning
</span>
?
</motion.h2>
<motion.p
className="text-base text-gray-600 mb-8"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
>
Join thousands of workers completing AI-verified tasks on Celo
Sepolia
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 justify-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<Button
size="lg"
className="font-semibold"
>
Get Started <ArrowRight className="w-4 h-4 ml-2" />
</Button>
<Button
size="lg"
variant="outline"
>
Try Demo
</Button>
</motion.div>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,165 @@
"use client";
import { Button } from "@/components/ui/button";
import { Award, TrendingUp, Zap } from "lucide-react";
import { motion } from "framer-motion";
import { BadgeShowcase } from "../_sections/badge-showcase";
import { ReputationMeter } from "../_sections/reputation-meter";
export default function Profile() {
return (
<main className="min-h-screen py-8 px-4 sm:px-6 lg:px-8 pt-[100px]">
<div className="max-w-4xl mx-auto">
{/* Profile Header */}
<motion.div
className="bg-linear-to-br from-green-500/10 to-black/50 backdrop-blur-xl border border-green-500/30 p-8 mb-8 hover:border-green-500/50 transition-colors"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6 mb-6">
<motion.div
className="w-24 h-24 bg-linear-to-br from-green-500 to-green-600 flex items-center justify-center shadow-lg shadow-green-500/50"
animate={{
boxShadow: [
"0 0 20px rgba(255,140,0,0.5)",
"0 0 40px rgba(255,140,0,0.8)",
"0 0 20px rgba(255,140,0,0.5)",
],
}}
transition={{ duration: 3, repeat: Number.POSITIVE_INFINITY }}
>
<span className="text-4xl font-bold text-white">RB</span>
</motion.div>
<div className="flex-1">
<h1 className="text-4xl font-bold mb-2 bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Raj Bhattacharya
</h1>
<p className="text-foreground/70 mb-4">
Verified Worker Member since Jan 2024
</p>
<div className="flex gap-2">
<motion.div whileHover={{ scale: 1.05 }}>
<Button
size="sm"
className="bg-linear-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white"
>
Edit Profile
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }}>
<Button
size="sm"
variant="outline"
className="border-green-500/50 hover:border-green-500 hover:bg-green-500/10 bg-transparent"
>
Share Profile
</Button>
</motion.div>
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
{[
{ label: "Tasks Completed", value: "156" },
{ label: "Approval Rate", value: "98.7%" },
{ label: "Total Earned", value: "$1,247.80", highlight: true },
].map((stat, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: i * 0.1 }}
>
<p className="text-sm text-foreground/60 mb-1">{stat.label}</p>
<p
className={`text-2xl font-bold ${
stat.highlight ? "text-green-400" : ""
}`}
>
{stat.value}
</p>
</motion.div>
))}
</div>
</motion.div>
{/* Reputation Section */}
<motion.div
className="bg-black/40 backdrop-blur-xl border border-green-500/20 p-8 mb-8 hover:border-green-500/40 transition-colors"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<h2 className="text-2xl font-bold mb-6 bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Reputation & Badges
</h2>
<ReputationMeter />
<BadgeShowcase />
</motion.div>
{/* Activity Section */}
<motion.div
className="bg-black/40 backdrop-blur-xl border border-green-500/20 p-8 hover:border-green-500/40 transition-colors"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
viewport={{ once: true }}
>
<h2 className="text-2xl font-bold mb-6 bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
Activity Highlights
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{
icon: Zap,
title: "Current Streak",
value: "12 days",
subtitle: "Keep it up!",
color: "green",
},
{
icon: TrendingUp,
title: "This Week",
value: "$142.50",
subtitle: "+12.5% vs last week",
color: "green",
},
{
icon: Award,
title: "Badges Earned",
value: "8",
subtitle: "3 new this month",
color: "green",
},
].map((item, i) => {
const Icon = item.icon;
return (
<motion.div
key={i}
className="p-6 bg-linear-to-br from-green-500/10 to-black/30 border border-green-500/20 hover:border-green-500/40 transition-colors"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: i * 0.1 }}
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
>
<div className="flex items-center gap-2 mb-2">
<Icon className="w-5 h-5 text-green-400" />
<p className="font-medium">{item.title}</p>
</div>
<p className="text-2xl font-bold">{item.value}</p>
<p className="text-sm text-foreground/60">{item.subtitle}</p>
</motion.div>
);
})}
</div>
</motion.div>
</div>
</main>
);
}

View File

@@ -0,0 +1,366 @@
'use client';
import { api } from '@/lib/api';
import { formatCurrency } from '@/lib/utils';
import { VerificationStatus } from '@/types';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { AlertCircle, CheckCircle2, Clock, ExternalLink, FileText, TrendingUp, Wallet, XCircle } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
export default function SubmissionStatusPage() {
const params = useParams();
const submissionId = params.submissionId as string;
const { data, isLoading } = useQuery({
queryKey: ['submission', submissionId],
queryFn: () => api.submissions.getStatus(submissionId),
refetchInterval: 5000, // Poll every 5 seconds
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner size="lg" />
</div>
);
}
const submission = data?.data?.submission;
const task = data?.data?.task;
const payment = data?.data?.payment;
if (!submission) {
return (
<div className="relative min-h-screen overflow-hidden">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.5, 0.3] }}
transition={{ duration: 8, repeat: Infinity }}
style={{ top: "20%", left: "10%" }}
/>
</div>
<div className="relative z-10 text-center py-32">
<h2 className="text-2xl font-bold text-foreground">Submission not found</h2>
</div>
</div>
);
}
const statusConfig = {
[VerificationStatus.PENDING]: {
gradient: 'from-green-500 to-green-600',
bgGradient: 'from-green-500/10 to-green-600/5',
border: 'border-green-500/30',
icon: Clock,
iconColor: 'text-green-500',
title: 'Verification in Progress',
description: 'AI is verifying your submission. This usually takes 1-2 minutes.',
},
[VerificationStatus.APPROVED]: {
gradient: 'from-green-500 to-green-600',
bgGradient: 'from-green-500/10 to-green-600/5',
border: 'border-green-500/30',
icon: CheckCircle2,
iconColor: 'text-green-500',
title: 'Submission Approved!',
description: 'Your submission has been approved and payment has been sent to your wallet.',
},
[VerificationStatus.REJECTED]: {
gradient: 'from-red-500 to-red-600',
bgGradient: 'from-red-500/10 to-red-600/5',
border: 'border-red-500/30',
icon: XCircle,
iconColor: 'text-red-500',
title: 'Submission Rejected',
description: 'Your submission did not meet the verification criteria.',
},
};
const config = statusConfig[submission.status as VerificationStatus];
const StatusIcon = config.icon;
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-4xl mx-auto px-4 py-12 pt-32">
{/* Status Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className={`bg-linear-to-br ${config.bgGradient} backdrop-blur-md border ${config.border} p-8 mb-8`}
>
<div className="flex items-start gap-6">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.3 }}
className={`w-20 h-20 bg-linear-to-r ${config.gradient} flex items-center justify-center flex-shrink-0`}
>
<StatusIcon className="w-10 h-10 text-white" />
</motion.div>
<div className="flex-1">
<motion.h1
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="text-3xl font-bold text-foreground mb-2"
>
{config.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="text-foreground/70 text-lg"
>
{config.description}
</motion.p>
{/* Progress Bar for Pending */}
{submission.status === VerificationStatus.PENDING && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mt-6"
>
<div className="w-full bg-foreground/10 h-2 overflow-hidden">
<motion.div
className="h-full bg-linear-to-r from-green-500 to-green-600 "
animate={{ width: ["40%", "70%", "40%"] }}
transition={{ duration: 2, repeat: Infinity }}
/>
</div>
<p className="text-sm text-foreground/60 mt-2 flex items-center gap-2">
<LoadingSpinner />
Estimated time: 1-2 minutes
</p>
</motion.div>
)}
</div>
</div>
</motion.div>
{/* Task Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="bg-background/80 backdrop-blur-md border border-green-500/20 p-6 mb-6"
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center">
<FileText className="w-5 h-5 text-white" />
</div>
<h2 className="text-xl font-bold text-foreground">Task Details</h2>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-foreground/5 ">
<span className="text-foreground/60">Task:</span>
<span className="font-semibold text-foreground">{task?.title}</span>
</div>
<div className="flex justify-between items-center p-3 bg-foreground/5 ">
<span className="text-foreground/60">Payment:</span>
<span className="font-bold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{formatCurrency(task?.paymentAmount || 0)}
</span>
</div>
<div className="flex justify-between items-center p-3 bg-foreground/5 ">
<span className="text-foreground/60">Submitted:</span>
<span className="font-medium text-foreground">
{new Date(submission.submittedAt).toLocaleString()}
</span>
</div>
</div>
</motion.div>
{/* Verification Results */}
{submission.verificationResult && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="bg-background/80 backdrop-blur-md border border-green-500/20 p-6 mb-6"
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-white" />
</div>
<h2 className="text-xl font-bold text-foreground">Verification Results</h2>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center p-4 bg-linear-to-br from-green-500/10 to-green-600/5 border border-green-500/20">
<span className="text-foreground/60 font-medium">Score:</span>
<div className="flex items-center gap-2">
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.5 }}
className="text-2xl font-bold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent"
>
{submission.verificationResult.score}
</motion.span>
<span className="text-foreground/60">/100</span>
</div>
</div>
{submission.verificationResult.reasoning && (
<div>
<span className="text-foreground/60 block mb-2 font-medium">AI Reasoning:</span>
<div className="bg-foreground/5 border border-green-500/20 p-4 ">
<p className="text-sm text-foreground/70 leading-relaxed">
{submission.verificationResult.reasoning}
</p>
</div>
</div>
)}
{submission.verificationResult.error && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-linear-to-br from-red-500/10 to-red-600/5 border border-red-500/30 p-4"
>
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-semibold text-red-600 mb-1">Error:</p>
<p className="text-sm text-red-600/80">
{submission.verificationResult.error}
</p>
{submission.verificationResult.blockchainError && (
<p className="text-xs text-red-600/70 mt-2">
Blockchain: {submission.verificationResult.blockchainError}
</p>
)}
</div>
</div>
</motion.div>
)}
</div>
</motion.div>
)}
{/* Payment Info */}
{payment && payment.transactionHash && payment.transactionHash !== 'pending' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 backdrop-blur-md border border-green-500/30 p-6 mb-6"
>
<div className="flex items-center gap-3 mb-4">
<motion.div
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-10 h-10 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center"
>
<Wallet className="w-5 h-5 text-white" />
</motion.div>
<h2 className="text-xl font-bold text-foreground">💰 Payment Details</h2>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-background/50 ">
<span className="text-foreground/60">Amount:</span>
<span className="font-bold text-green-600 text-lg">
{formatCurrency(payment.amount)}
</span>
</div>
<div className="p-3 bg-background/50 ">
<span className="text-foreground/60 block mb-2">Transaction Hash:</span>
<a
href={`https://sepolia.celoscan.io/tx/${payment.transactionHash}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-sm text-green-500 hover:text-green-600 break-all flex items-center gap-2 transition-colors"
>
{payment.transactionHash}
<ExternalLink className="w-4 h-4 flex-shrink-0" />
</a>
</div>
</div>
</motion.div>
)}
{/* Pending Payment */}
{submission.status === VerificationStatus.APPROVED && (!payment || !payment.transactionHash || payment.transactionHash === 'pending') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 backdrop-blur-md border border-green-500/30 p-6 mb-6"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center">
<Wallet className="w-5 h-5 text-white" />
</div>
<h2 className="text-xl font-bold text-foreground">💰 Payment Processing</h2>
</div>
<div className="flex items-center gap-3 p-4 bg-background/50 ">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<LoadingSpinner />
</motion.div>
<p className="text-sm text-foreground/70">
Payment is being processed on the blockchain. This may take a few moments...
</p>
</div>
</motion.div>
)}
{/* Actions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4"
>
<Link href="/tasks" className="flex-1">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full py-4 bg-linear-to-r from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all font-bold shadow-lg shadow-green-500/25"
>
Browse More Tasks
</motion.button>
</Link>
<Link href="/dashboard" className="flex-1">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full py-4 bg-background/80 backdrop-blur-sm text-green-500 border-2 border-green-500/50 hover:border-green-500 hover:bg-green-500/10 transition-all font-bold"
>
View Dashboard
</motion.button>
</Link>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,310 @@
'use client';
import { api } from '@/lib/api';
import { formatCurrency, formatTimeRemaining } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { AlertCircle, ArrowLeft, Award, CheckCircle2, Clock, Users, Wallet } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
export default function TaskDetailsPage() {
const params = useParams();
const taskId = params.taskId as string;
const { data, isLoading } = useQuery({
queryKey: ['task', taskId],
queryFn: () => api.tasks.getById(taskId),
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner size="lg" />
</div>
);
}
const task = data?.data;
if (!task) {
return (
<div className="relative min-h-screen overflow-hidden">
{/* Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.5, 0.3] }}
transition={{ duration: 8, repeat: Infinity }}
style={{ top: "20%", left: "10%" }}
/>
</div>
<div className="relative z-10 max-w-3xl mx-auto px-4 py-32 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 bg-linear-to-br from-green-500/20 to-green-600/10 mb-6">
<AlertCircle className="w-8 h-8 text-green-500" />
</div>
<h2 className="text-3xl font-bold text-foreground mb-4">Task Not Found</h2>
<p className="text-foreground/60 mb-8">This task may have been removed or doesn't exist.</p>
<Link href="/tasks">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-flex items-center gap-2 px-6 py-3 bg-linear-to-r from-green-500 to-green-600 text-white font-semibold"
>
<ArrowLeft className="w-4 h-4" />
Back to Tasks
</motion.button>
</Link>
</motion.div>
</div>
</div>
);
}
const progressPercentage = ((task.maxSubmissions - task.spotsRemaining) / task.maxSubmissions) * 100;
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-5xl mx-auto px-4 py-12 pt-32">
{/* Back Button */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Link
href="/tasks"
className="inline-flex items-center gap-2 text-green-500 hover:text-green-600 mb-8 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Tasks
</Link>
</motion.div>
{/* Main Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="bg-background/80 backdrop-blur-md border border-green-500/20 overflow-hidden"
>
{/* Header Section */}
<div className="p-8 border-b border-green-500/10">
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-3xl sm:text-4xl font-bold text-foreground mb-4"
>
{task.title}
</motion.h1>
<div className="flex flex-wrap gap-3">
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="px-4 py-2 bg-linear-to-r from-green-500/20 to-green-600/20 border border-green-500/30 text-green-500 text-sm font-semibold"
>
{task.taskType.replace('_', ' ')}
</motion.span>
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.4, type: "spring" }}
className="px-4 py-2 bg-linear-to-r from-green-500 to-green-600 text-white text-sm font-bold flex items-center gap-1"
>
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-2 h-2 bg-white "
/>
{formatCurrency(task.paymentAmount)}
</motion.span>
{task.isExpiringSoon && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.5, type: "spring" }}
className="px-4 py-2 bg-green-500/10 border border-green-500/30 text-green-500 text-sm font-semibold flex items-center gap-1"
>
<Clock className="w-4 h-4" />
Expiring Soon
</motion.span>
)}
</div>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex justify-between text-sm text-foreground/60">
<span>{task.maxSubmissions - task.spotsRemaining} completed</span>
<span>{task.spotsRemaining} spots left</span>
</div>
<div className="w-full bg-foreground/10 h-2 overflow-hidden">
<motion.div
className="h-full bg-linear-to-r from-green-500 to-green-600 "
initial={{ width: 0 }}
animate={{ width: `${progressPercentage}%` }}
transition={{ duration: 1, delay: 0.6 }}
/>
</div>
</div>
</div>
{/* Content Section */}
<div className="p-8 space-y-8">
{/* Description */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<h2 className="text-2xl font-bold text-foreground mb-4 flex items-center gap-2">
<LoadingSpinner />
Description
</h2>
<p className="text-foreground/70 leading-relaxed text-lg">{task.description}</p>
</motion.div>
{/* Requirements */}
{task.verificationCriteria?.requiredFields && task.verificationCriteria.requiredFields.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<h2 className="text-2xl font-bold text-foreground mb-4 flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-500" />
Requirements
</h2>
<ul className="space-y-3">
{task.verificationCriteria.requiredFields.map((field: string, index: number) => (
<motion.li
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="flex items-start gap-3 text-foreground/70"
>
<div className="mt-1 w-1.5 h-1.5 bg-linear-to-r from-green-500 to-green-600 flex-shrink-0" />
<span>{field}</span>
</motion.li>
))}
</ul>
</motion.div>
)}
{/* Stats Grid */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4"
>
{[
{ icon: Wallet, label: 'Payment', value: formatCurrency(task.paymentAmount), color: 'from-green-500 to-green-600' },
{ icon: Users, label: 'Spots Left', value: `${task.spotsRemaining}/${task.maxSubmissions}`, color: 'from-green-600 to-green-700' },
{ icon: Clock, label: 'Time Left', value: formatTimeRemaining(task.expiresAt), color: 'from-green-400 to-green-500' },
{ icon: Award, label: 'Submissions', value: task.submissionCount.toString(), color: 'from-green-500 to-green-600' },
].map((stat, index) => (
<motion.div
key={index}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.6 + index * 0.1, type: "spring" }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 border border-green-500/20 p-4 text-center"
>
<div className={`inline-flex items-center justify-center w-10 h-10 bg-linear-to-r ${stat.color} mb-2`}>
<stat.icon className="w-5 h-5 text-white" />
</div>
<div className="text-xs text-foreground/60 mb-1">{stat.label}</div>
<div className="text-lg font-bold text-foreground">{stat.value}</div>
</motion.div>
))}
</motion.div>
{/* Requester Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 border border-green-500/20 p-6"
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center flex-shrink-0">
<Wallet className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="text-sm text-foreground/60 mb-1">Posted by</div>
<div className="font-mono text-sm text-foreground bg-background/50 px-3 py-2 break-all mb-2">
{task.requester.walletAddress}
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-foreground/60">Reputation:</span>
<div className="flex items-center gap-1">
<Award className="w-4 h-4 text-green-500" />
<span className="font-semibold text-green-500">{task.requester.reputationScore}</span>
</div>
</div>
</div>
</div>
</motion.div>
{/* Action Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
>
<Link href={`/tasks/${taskId}/submit`}>
<motion.button
whileHover={task.spotsRemaining > 0 && task.canSubmit ? { scale: 1.02 } : {}}
whileTap={task.spotsRemaining > 0 && task.canSubmit ? { scale: 0.98 } : {}}
disabled={task.spotsRemaining === 0 || !task.canSubmit}
className="w-full py-5 bg-linear-to-r from-green-500 to-green-600 text-white font-bold text-lg hover:from-green-600 hover:to-green-700 transition-all disabled:from-gray-400 disabled:to-gray-500 disabled:cursor-not-allowed shadow-lg shadow-green-500/25 disabled:shadow-none"
>
{task.spotsRemaining === 0 ? 'No Spots Available' : !task.canSubmit ? 'Cannot Submit' : 'Submit Task'}
</motion.button>
</Link>
</motion.div>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,715 @@
'use client';
import { useWallet } from '@/hooks/useWallet';
import { api } from '@/lib/api';
import { TaskType } from '@/types';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { AlertTriangle, ArrowLeft, CheckCircle2, FileText, Upload, X } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
export default function SubmitTaskPage() {
const params = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const { isConnected } = useWallet();
const taskId = params.taskId as string;
const [formData, setFormData] = useState({
text: '',
imageFile: null as File | null,
labels: '',
answers: [] as string[],
comment: '',
decision: '',
customFields: {} as Record<string, any>
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [imagePreview, setImagePreview] = useState<string | null>(null);
const { data: taskData, isLoading } = useQuery({
queryKey: ['task', taskId],
queryFn: () => api.tasks.getById(taskId),
});
const submitMutation = useMutation({
mutationFn: (data: any) => api.submissions.submit(data),
onSuccess: (response) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['submissions'] });
const submissionId = response.data.submissionId;
router.push(`/submissions/${submissionId}`);
},
onError: (error: any) => {
alert(error.response?.data?.error?.message || 'Submission failed');
},
});
const task = taskData?.data;
const requiredFields: string[] = task?.verificationCriteria?.requiredFields || [];
// Helper function to check if a field is required
const isFieldRequired = (fieldName: string) => requiredFields.includes(fieldName);
// Helper function to render survey questions
const getSurveyQuestions = () => {
// This could come from the task description or verification criteria
// For now, we'll use some default questions based on task type
if (task?.taskType === TaskType.SURVEY) {
return [
'How would you rate the overall user experience? (1-5)',
'What features did you find most useful?',
'What improvements would you suggest?'
];
}
// You could also get questions from task.verificationCriteria.questions if available
return task?.verificationCriteria?.questions || [];
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) {
setErrors({ ...errors, image: 'File size must be less than 5MB' });
return;
}
// Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (!validTypes.includes(file.type)) {
setErrors({ ...errors, image: 'Only JPG, PNG, WebP images are allowed' });
return;
}
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
setFormData({ ...formData, imageFile: file });
setErrors({ ...errors, image: '' });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isConnected) {
alert('Please connect your wallet first');
return;
}
// Dynamic validation based on verification criteria
const newErrors: Record<string, string> = {};
requiredFields.forEach((field: string) => {
switch (field) {
case 'text':
if (!formData.text.trim()) {
newErrors.text = 'Text is required';
}
break;
case 'image':
if (!formData.imageFile) {
newErrors.image = 'Image is required';
}
break;
case 'labels':
if (!formData.labels.trim()) {
newErrors.labels = 'Labels are required';
}
break;
case 'answers':
if (!formData.answers || formData.answers.length === 0 || formData.answers.some(answer => !answer?.trim())) {
newErrors.answers = 'All survey questions must be answered';
}
break;
case 'comment':
if (!formData.comment.trim()) {
newErrors.comment = 'Comment is required';
}
break;
case 'decision':
if (!formData.decision) {
newErrors.decision = 'Decision is required';
}
break;
}
});
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Prepare submission data dynamically
const submissionData: any = {};
requiredFields.forEach((field: string) => {
switch (field) {
case 'text':
submissionData.text = formData.text;
break;
case 'image':
// In production, upload to cloud storage (S3, Cloudinary, etc.)
submissionData.imageUrls = ['https://placeholder.com/image.jpg'];
submissionData.metadata = { fileName: formData.imageFile?.name };
break;
case 'labels':
submissionData.labels = formData.labels.split(',').map(label => label.trim());
break;
case 'answers':
submissionData.answers = formData.answers;
break;
case 'comment':
submissionData.comment = formData.comment;
break;
case 'decision':
submissionData.decision = formData.decision;
break;
}
});
// Submit
await submitMutation.mutateAsync({
taskId,
submissionData,
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner size="lg" />
</div>
);
}
if (!task) {
return (
<div className="relative min-h-screen overflow-hidden">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{ scale: [1, 1.2, 1], opacity: [0.3, 0.5, 0.3] }}
transition={{ duration: 8, repeat: Infinity }}
style={{ top: "20%", left: "10%" }}
/>
</div>
<div className="relative z-10 text-center py-32">
<h2 className="text-2xl font-bold text-foreground">Task not found</h2>
</div>
</div>
);
}
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-4xl mx-auto px-4 py-12 pt-32">
{/* Back Button */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
className="mb-8"
>
<button
onClick={() => router.back()}
className="inline-flex items-center gap-2 text-green-500 hover:text-green-600 font-medium transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Task
</button>
</motion.div>
{/* Task Info Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 backdrop-blur-md border border-green-500/30 p-6 mb-8"
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center flex-shrink-0">
<LoadingSpinner />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold text-foreground mb-2">{task.title}</h2>
<p className="text-foreground/70 mb-3">{task.description}</p>
<div className="inline-flex items-center gap-2 px-4 py-2 bg-linear-to-r from-green-500 to-green-600 text-white font-bold">
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="w-2 h-2 bg-white "
/>
Payment: ${task.paymentAmount} cUSD
</div>
</div>
</div>
</motion.div>
{/* Submission Form */}
<motion.form
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
onSubmit={handleSubmit}
className="bg-background/80 backdrop-blur-md border border-green-500/20 p-8"
>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center">
<FileText className="w-5 h-5 text-white" />
</div>
<h3 className="text-2xl font-bold text-foreground">Submit Your Work</h3>
</div>
{/* Text Field - Dynamic based on required fields */}
{isFieldRequired('text') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-3">
{task.taskType === TaskType.TEXT_VERIFICATION ? 'Your Response' :
task.taskType === TaskType.CONTENT_MODERATION ? 'Comment to Review' : 'Text'} *
</label>
<div className="relative">
<textarea
value={formData.text}
onChange={(e) => setFormData({ ...formData, text: e.target.value })}
rows={task.taskType === TaskType.TEXT_VERIFICATION ? 8 : 4}
placeholder={
task.taskType === TaskType.TEXT_VERIFICATION ? "Enter your response here..." :
task.taskType === TaskType.CONTENT_MODERATION ? "Paste the comment to review here..." :
"Enter your text here..."
}
className={`w-full px-4 py-3 bg-background/50 border focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all ${errors.text ? 'border-red-500' : 'border-green-500/30'
} text-foreground placeholder:text-foreground/40`}
/>
{errors.text && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.text}
</motion.p>
)}
<div className="mt-2 flex items-center justify-between text-sm">
<span className="text-foreground/60">{formData.text.length} characters</span>
{formData.text.length > 0 && (
<span className="text-green-500 flex items-center gap-1">
<CheckCircle2 className="w-4 h-4" />
Looking good!
</span>
)}
</div>
</div>
</motion.div>
)}
{/* Comment Field (for content moderation when text is not required) */}
{isFieldRequired('comment') && !isFieldRequired('text') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-3">
Comment to Review *
</label>
<div className="relative">
<textarea
value={formData.comment}
onChange={(e) => setFormData({ ...formData, comment: e.target.value })}
rows={4}
placeholder="Paste the comment to review here..."
className={`w-full px-4 py-3 bg-background/50 border focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all ${errors.comment ? 'border-red-500' : 'border-green-500/30'
} text-foreground placeholder:text-foreground/40`}
/>
{errors.comment && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.comment}
</motion.p>
)}
</div>
</motion.div>
)}
{/* Decision Field (for content moderation) */}
{isFieldRequired('decision') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.35 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-3">
Moderation Decision *
</label>
<div className="relative">
<select
value={formData.decision}
onChange={(e) => setFormData({ ...formData, decision: e.target.value })}
className={`w-full px-4 py-3 bg-background/50 border focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all ${errors.decision ? 'border-red-500' : 'border-green-500/30'
} text-foreground`}
>
<option value="">Select a decision...</option>
<option value="approved"> Approve - Content is appropriate</option>
<option value="rejected"> Reject - Content violates rules</option>
<option value="flagged">🚩 Flag for Review - Needs human review</option>
</select>
{errors.decision && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.decision}
</motion.p>
)}
</div>
</motion.div>
)}
{/* Labels Field (for image labeling) */}
{isFieldRequired('labels') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-3">
Image Labels *
</label>
<div className="relative">
<input
type="text"
value={formData.labels}
onChange={(e) => setFormData({ ...formData, labels: e.target.value })}
placeholder="Enter labels separated by commas (e.g., car, tree, building)"
className={`w-full px-4 py-3 bg-background/50 border focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all ${errors.labels ? 'border-red-500' : 'border-green-500/30'
} text-foreground placeholder:text-foreground/40`}
/>
{errors.labels && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.labels}
</motion.p>
)}
<p className="mt-2 text-sm text-foreground/60">
Separate multiple labels with commas
</p>
</div>
</motion.div>
)}
{/* Survey Answers Field */}
{isFieldRequired('answers') && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-4">
Survey Questions *
</label>
<div className="space-y-4">
{getSurveyQuestions().map((question: string, index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="bg-linear-to-br from-green-500/5 to-green-600/5 backdrop-blur-sm border border-green-500/20 p-4"
>
<p className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-6 h-6 bg-linear-to-r from-green-500 to-green-600 text-white flex items-center justify-center text-xs font-bold">
{index + 1}
</span>
{question}
</p>
<textarea
value={formData.answers[index] || ''}
onChange={(e) => {
const newAnswers = [...formData.answers];
newAnswers[index] = e.target.value;
setFormData({ ...formData, answers: newAnswers });
}}
rows={3}
placeholder="Enter your answer here..."
className="w-full px-4 py-3 bg-background/50 border border-green-500/20 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all text-foreground placeholder:text-foreground/40"
/>
</motion.div>
))}
</div>
{errors.answers && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-3 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.answers}
</motion.p>
)}
</motion.div>
)}
{/* Image Labeling */}
{task.taskType === TaskType.IMAGE_LABELING && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-6"
>
<label className="block text-sm font-semibold text-foreground mb-3">
Upload Image *
</label>
<div className={`relative border-2 border-dashed p-8 text-center transition-all ${errors.image ? 'border-red-500' : 'border-green-500/30 hover:border-green-500/50'
}`}>
{imagePreview ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="relative"
>
<img
src={imagePreview}
alt="Preview"
className="max-h-96 mx-auto shadow-lg"
/>
<motion.button
type="button"
onClick={() => {
setImagePreview(null);
setFormData({ ...formData, imageFile: null });
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="absolute top-4 right-4 bg-red-500 text-white p-2 hover:bg-red-600 transition-colors shadow-lg"
>
<X className="w-5 h-5" />
</motion.button>
<div className="mt-4 text-sm text-foreground/60">
{formData.imageFile?.name}
</div>
</motion.div>
) : (
<div>
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="inline-flex items-center justify-center w-20 h-20 bg-linear-to-br from-green-500/20 to-green-600/10 mb-4"
>
<Upload className="w-10 h-10 text-green-500" />
</motion.div>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="hidden"
id="image-upload"
/>
<label
htmlFor="image-upload"
className="cursor-pointer"
>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-block px-6 py-3 bg-linear-to-r from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all font-semibold shadow-lg shadow-green-500/25"
>
Choose Image
</motion.div>
</label>
<p className="mt-3 text-sm text-foreground/60">PNG, JPG, WebP up to 5MB</p>
</div>
)}
</div>
{errors.image && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-sm text-red-500 flex items-center gap-1"
>
<AlertTriangle className="w-4 h-4" />
{errors.image}
</motion.p>
)}
</motion.div>
)}
{/* Task-specific Instructions */}
{(isFieldRequired('text') && isFieldRequired('image')) && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-linear-to-br from-blue-500/10 to-blue-600/5 border border-blue-500/30 p-4 mb-6"
>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-linear-to-r from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0">
<FileText className="w-4 h-4 text-white" />
</div>
<div>
<p className="text-sm text-foreground font-medium mb-1">📋 Mixed Task Instructions</p>
<p className="text-sm text-foreground/70">
This task requires both text and image inputs. Please provide both components to complete your submission.
</p>
</div>
</div>
</motion.div>
)}
{/* Additional Context for Complex Tasks */}
{task?.verificationCriteria?.aiPrompt && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.52 }}
className="bg-linear-to-br from-purple-500/10 to-purple-600/5 border border-purple-500/30 p-4 mb-6"
>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-linear-to-r from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0">
<LoadingSpinner />
</div>
<div>
<p className="text-sm text-foreground font-medium mb-1">🎯 Verification Criteria</p>
<p className="text-sm text-foreground/70">
AI will verify your submission based on: {task.verificationCriteria.aiPrompt}
</p>
</div>
</div>
</motion.div>
)}
{/* Required Fields Summary */}
{requiredFields.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.54 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 border border-green-500/30 p-4 mb-6"
>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center flex-shrink-0">
<CheckCircle2 className="w-4 h-4 text-white" />
</div>
<div className="flex-1">
<p className="text-sm text-foreground font-medium mb-2"> Required Fields</p>
<div className="flex flex-wrap gap-2">
{requiredFields.map((field: string) => (
<motion.span
key={field}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.56 + requiredFields.indexOf(field) * 0.05 }}
className="px-3 py-1 bg-green-500/20 text-green-700 dark:text-green-300 text-xs font-medium border border-green-500/30"
>
{field.charAt(0).toUpperCase() + field.slice(1)}
</motion.span>
))}
</div>
</div>
</div>
</motion.div>
)}
{/* Warning */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-linear-to-br from-green-500/10 to-green-600/5 border border-green-500/30 p-4 mb-6"
>
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-linear-to-r from-green-500 to-green-600 flex items-center justify-center flex-shrink-0">
<LoadingSpinner />
</div>
<div>
<p className="text-sm text-foreground font-medium mb-1">AI Verification</p>
<p className="text-sm text-foreground/70">
Your submission will be automatically verified by Gemini AI. Please ensure it meets all requirements to avoid rejection.
</p>
</div>
</div>
</motion.div>
{/* Submit Button */}
<motion.button
type="submit"
disabled={submitMutation.isPending || !isConnected}
whileHover={submitMutation.isPending || !isConnected ? {} : { scale: 1.02 }}
whileTap={submitMutation.isPending || !isConnected ? {} : { scale: 0.98 }}
className="w-full py-5 bg-linear-to-r from-green-500 to-green-600 text-white font-bold hover:from-green-600 hover:to-green-700 transition-all disabled:from-gray-400 disabled:to-gray-500 disabled:cursor-not-allowed text-lg shadow-lg shadow-green-500/25 disabled:shadow-none"
>
{submitMutation.isPending ? (
<span className="flex items-center justify-center gap-2">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<LoadingSpinner />
</motion.div>
Submitting...
</span>
) : !isConnected ? (
'Connect Wallet to Submit'
) : (
<span className="flex items-center justify-center gap-2">
Submit Task
<CheckCircle2 className="w-5 h-5" />
</span>
)}
</motion.button>
</motion.form>
</div>
</div>
);
}

View File

@@ -0,0 +1,262 @@
"use client";
import { api } from "@/lib/api";
import { formatCurrency, formatTimeRemaining } from "@/lib/utils";
import { Task, TaskStatus, TaskType } from "@/types";
import { useQuery } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { Filter } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
function TaskCard({ task, index }: { task: Task; index: number }) {
const taskTypeLabels: Record<TaskType, string> = {
[TaskType.TEXT_VERIFICATION]: "Text",
[TaskType.IMAGE_LABELING]: "Image",
[TaskType.SURVEY]: "Survey",
[TaskType.CONTENT_MODERATION]: "Moderation",
};
const taskTypeColors: Record<TaskType, string> = {
[TaskType.TEXT_VERIFICATION]: "from-green-500 to-green-600",
[TaskType.IMAGE_LABELING]: "from-green-600 to-green-700",
[TaskType.SURVEY]: "from-green-400 to-green-500",
[TaskType.CONTENT_MODERATION]: "from-green-500 to-green-700",
};
return (
<Link href={`/tasks/${task.id}`}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -8, transition: { duration: 0.2 } }}
className="relative group cursor-pointer"
>
<div className="absolute inset-0 bg-linear-to-br from-green-500/20 to-green-600/10 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative bg-background/80 backdrop-blur-sm border border-green-500/20 p-6 hover:border-green-500/40 transition-all duration-300">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<h3 className="text-lg font-semibold text-foreground line-clamp-2 flex-1">
{task.title}
</h3>
{task.paymentAmount >= 5 && (
<motion.span
className="ml-2 text-xs bg-linear-to-r from-green-500 to-green-600 text-white px-2 py-1 font-semibold"
animate={{ scale: [1, 1.05, 1] }}
transition={{ duration: 2, repeat: Infinity }}
>
💰 High
</motion.span>
)}
</div>
{/* Description */}
<p className="text-sm text-foreground/60 mb-4 line-clamp-2">
{task.description}
</p>
{/* Stats */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<motion.div
className="w-2 h-2 bg-linear-to-r from-green-500 to-green-600 "
animate={{ scale: [1, 1.3, 1] }}
transition={{ duration: 2, repeat: Infinity }}
/>
<span className="text-lg font-bold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{formatCurrency(task.paymentAmount)}
</span>
</div>
<span className="text-sm text-foreground/60">
<span className="font-semibold text-foreground">
{task.spotsRemaining}
</span>
/{task.maxSubmissions} spots
</span>
</div>
{/* Progress Bar */}
<div className="w-full bg-foreground/10 h-1.5 mb-4 overflow-hidden">
<motion.div
className="h-full bg-linear-to-r from-green-500 to-green-600 "
initial={{ width: 0 }}
animate={{
width: `${
((task.maxSubmissions - task.spotsRemaining) /
task.maxSubmissions) *
100
}%`,
}}
transition={{ duration: 1, delay: index * 0.1 + 0.3 }}
/>
</div>
{/* Footer */}
<div className="flex items-center justify-between pt-4 border-t border-foreground/10">
<span
className={`text-xs px-3 py-1.5 bg-linear-to-r ${
taskTypeColors[task.taskType]
} text-white font-semibold`}
>
{taskTypeLabels[task.taskType]}
</span>
<span className="text-xs text-foreground/50 font-medium">
{formatTimeRemaining(task.expiresAt)}
</span>
</div>
</div>
</motion.div>
</Link>
);
}
export default function TasksPage() {
const [filters, setFilters] = useState({
status: TaskStatus.OPEN,
taskType: undefined as TaskType | undefined,
});
const { data, isLoading } = useQuery({
queryKey: ["tasks", filters],
queryFn: () => api.tasks.list(filters),
});
const tasks = data?.data || [];
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner size="lg" />
</div>
);
}
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-500/20 to-green-600/10 blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
}}
transition={{ duration: 20, repeat: Infinity }}
style={{ top: "10%", left: "-10%" }}
/>
<motion.div
className="absolute w-96 h-96 bg-linear-to-br from-green-600/10 to-green-500/20 blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, -50, 0],
}}
transition={{ duration: 25, repeat: Infinity }}
style={{ bottom: "10%", right: "-10%" }}
/>
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 py-12 pt-32">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<div className="inline-flex items-center gap-2 mb-4 px-4 py-2 border border-green-500/30 bg-green-500/5 backdrop-blur-sm">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
>
<LoadingSpinner />
</motion.div>
<span className="text-sm font-semibold bg-linear-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{tasks.length} Tasks Available
</span>
</div>
<h1 className="text-4xl sm:text-5xl font-bold mb-4">
<span className="bg-linear-to-r from-green-400 via-green-500 to-green-600 bg-clip-text text-transparent">
Available Tasks
</span>
</h1>
<p className="text-foreground/60 text-lg max-w-2xl mx-auto">
Complete AI-verified tasks and earn cUSD instantly on Celo Sepolia
</p>
</motion.div>
{/* Filters */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="bg-background/60 backdrop-blur-md border border-green-500/20 p-6 mb-8"
>
<div className="flex items-center gap-3 mb-4">
<Filter className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-foreground">
Filter Tasks
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-foreground/70 mb-2">
Task Type
</label>
<select
value={filters.taskType || ""}
onChange={(e) =>
setFilters({
...filters,
taskType: (e.target.value as TaskType) || undefined,
})
}
className="w-full px-4 py-3 bg-background/80 border border-green-500/30 focus:ring-2 focus:ring-green-500 focus:border-transparent text-foreground backdrop-blur-sm transition-all"
>
<option value="">All Types</option>
<option value={TaskType.TEXT_VERIFICATION}>
Text Verification
</option>
<option value={TaskType.IMAGE_LABELING}>Image Labeling</option>
<option value={TaskType.SURVEY}>Survey</option>
<option value={TaskType.CONTENT_MODERATION}>
Content Moderation
</option>
</select>
</div>
</div>
</motion.div>
{/* Task Grid */}
{tasks.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tasks.map((task: Task, index: number) => (
<TaskCard key={task.id} task={task} index={index} />
))}
</div>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="text-center py-20"
>
<div className="inline-flex items-center justify-center w-16 h-16 bg-linear-to-br from-green-500/20 to-green-600/10 mb-4">
<LoadingSpinner />
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">
No tasks available
</h3>
<p className="text-foreground/60">
Check back soon for new opportunities!
</p>
</motion.div>
)}
</div>
</div>
);
}