mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
31
dmtp/client/app/_sections/badge-showcase.tsx
Normal file
31
dmtp/client/app/_sections/badge-showcase.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
74
dmtp/client/app/_sections/feature-showcase.tsx
Normal file
74
dmtp/client/app/_sections/feature-showcase.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
150
dmtp/client/app/_sections/hero-section.tsx
Normal file
150
dmtp/client/app/_sections/hero-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
79
dmtp/client/app/_sections/how-it-works.tsx
Normal file
79
dmtp/client/app/_sections/how-it-works.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
78
dmtp/client/app/_sections/integrations-section.tsx
Normal file
78
dmtp/client/app/_sections/integrations-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
26
dmtp/client/app/_sections/reputation-meter.tsx
Normal file
26
dmtp/client/app/_sections/reputation-meter.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
37
dmtp/client/app/_sections/stats-section.tsx
Normal file
37
dmtp/client/app/_sections/stats-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
94
dmtp/client/app/_sections/task-examples.tsx
Normal file
94
dmtp/client/app/_sections/task-examples.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
229
dmtp/client/app/_sections/testimonials.tsx
Normal file
229
dmtp/client/app/_sections/testimonials.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
430
dmtp/client/app/dashboard/page.tsx
Normal file
430
dmtp/client/app/dashboard/page.tsx
Normal 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
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
168
dmtp/client/app/globals.css
Normal 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;
|
||||
}
|
||||
42
dmtp/client/app/layout.tsx
Normal file
42
dmtp/client/app/layout.tsx
Normal 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
213
dmtp/client/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
165
dmtp/client/app/profile/page.tsx
Normal file
165
dmtp/client/app/profile/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
366
dmtp/client/app/submissions/[submissionId]/page.tsx
Normal file
366
dmtp/client/app/submissions/[submissionId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
310
dmtp/client/app/tasks/[taskId]/page.tsx
Normal file
310
dmtp/client/app/tasks/[taskId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
715
dmtp/client/app/tasks/[taskId]/submit/page.tsx
Normal file
715
dmtp/client/app/tasks/[taskId]/submit/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
262
dmtp/client/app/tasks/page.tsx
Normal file
262
dmtp/client/app/tasks/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user