mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
263 lines
9.6 KiB
TypeScript
263 lines
9.6 KiB
TypeScript
"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>
|
|
);
|
|
}
|