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:
381
dmtp/README.md
Normal file
381
dmtp/README.md
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# 🪙 D.M.T.P — Agentic AI-Verified Micro-Task Platform on Celo Blockchain
|
||||||
|
|
||||||
|
An **AI-powered decentralized task Platform** built on **Celo Sepolia Testnet**, where requesters post micro-tasks, workers complete them, AI verifies the results, and **payments are automatically released in cUSD** upon approval.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#-overview)
|
||||||
|
- [Tech Stack](#️-tech-stack)
|
||||||
|
- [AI Integration](#-ai-integration-google-gemini-15-pro)
|
||||||
|
- [Database Schema](#️-database-schema-core-entities)
|
||||||
|
- [Folder Structure](#-folder-structure)
|
||||||
|
- [Setup Guide](#️-setup-guide)
|
||||||
|
- [Smart Contract Deployment](#-smart-contract-deployment)
|
||||||
|
- [Workflow](#-workflow)
|
||||||
|
- [API Examples](#-example-api-calls)
|
||||||
|
- [Architecture](#-architecture-diagram)
|
||||||
|
- [Features](#-features-checklist)
|
||||||
|
- [Testing](#-testing)
|
||||||
|
- [Deployment](#-deployment)
|
||||||
|
- [Future Roadmap](#-future-roadmap)
|
||||||
|
- [License](#-license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Overview
|
||||||
|
|
||||||
|
**D.M.T.P** introduces verifiable trust between micro-task requesters and workers. AI moderation (powered by **Google Gemini**) ensures submissions are **authentic and high-quality**, while the **TaskEscrow smart contract** on **Celo** guarantees transparent, secure on-chain payments.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- 🤖 **AI-Powered Verification** — Automated content moderation using Google Gemini
|
||||||
|
- ⛓️ **Blockchain Escrow** — Smart contract-based payment security
|
||||||
|
- 💰 **Instant Payments** — Automatic cUSD release upon task approval
|
||||||
|
- 🔐 **Wallet Authentication** — Secure login via MetaMask or MiniPay
|
||||||
|
- 📊 **Real-Time Updates** — Live task and submission status tracking
|
||||||
|
- 🎯 **Quality Control** — AI prevents spam, fraud, and low-effort submissions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
### 🖥️ Frontend — Next.js (App Router)
|
||||||
|
|
||||||
|
| Feature | Library/Framework |
|
||||||
|
| ------------------ | --------------------------------------------------- |
|
||||||
|
| UI Components | [shadcn/ui](https://ui.shadcn.com) |
|
||||||
|
| Styling | [TailwindCSS](https://tailwindcss.com) |
|
||||||
|
| State Management | [Zustand](https://zustand-demo.pmnd.rs) |
|
||||||
|
| Data Fetching | [TanStack Query](https://tanstack.com/query/v5) |
|
||||||
|
| Wallet Integration | [wagmi](https://wagmi.sh) + [viem](https://viem.sh) |
|
||||||
|
| Blockchain SDK | [@celo/contractkit](https://docs.celo.org/) |
|
||||||
|
| Wallets Supported | **MiniPay** and **MetaMask** |
|
||||||
|
| AI Moderation | [Google Gemini 1.5](https://ai.google.dev/gemini-api/) |
|
||||||
|
|
||||||
|
### ⚙️ Backend — Express.js + TypeScript
|
||||||
|
|
||||||
|
| Feature | Technology |
|
||||||
|
| ----------------- | ------------------------------------------ |
|
||||||
|
| Framework | Express.js |
|
||||||
|
| Database | PostgreSQL + Prisma ORM |
|
||||||
|
| Job Queue | Bull + Redis |
|
||||||
|
| AI Verification | Gemini API |
|
||||||
|
| Blockchain | Celo Sepolia |
|
||||||
|
| Authentication | Wallet Signature (EIP-191) |
|
||||||
|
| File Verification | cUSD Escrow via TaskEscrow.sol |
|
||||||
|
| Notifications | Background service + WebSocket placeholder |
|
||||||
|
|
||||||
|
### ⛓️ Blockchain — Celo Sepolia Testnet
|
||||||
|
|
||||||
|
- **Smart Contract:** `TaskEscrow.sol`
|
||||||
|
- Holds task payments in escrow
|
||||||
|
- Releases funds when AI-verified
|
||||||
|
- Refunds requester if submissions are rejected
|
||||||
|
- **Token:** `cUSD` (Stablecoin)
|
||||||
|
- **Network RPC:** https://forno.celo-sepolia.celo-testnet.org
|
||||||
|
- **Explorer:** [CeloScan (Sepolia)](https://sepolia.celoscan.io)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 AI Integration (Google Gemini 2.5 Pro)
|
||||||
|
|
||||||
|
Used for:
|
||||||
|
|
||||||
|
- ✅ Spam, fraud, and duplicate prevention
|
||||||
|
- ✅ Toxic or low-effort content rejection
|
||||||
|
- ✅ Criteria-based submission scoring
|
||||||
|
|
||||||
|
Gemini runs asynchronously in a **Bull Queue worker**, sending results via webhook to approve or reject submissions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Schema (Core Entities)
|
||||||
|
|
||||||
|
| Table | Description |
|
||||||
|
| ----------------------- | ---------------------------------------------------------------------------------- |
|
||||||
|
| **users** | Stores wallet addresses, roles (`requester` / `worker`), reputation & earnings |
|
||||||
|
| **tasks** | Contains task metadata, blockchain taskID, payment amount, expiry |
|
||||||
|
| **submissions** | Tracks worker submissions, AI verification results |
|
||||||
|
| **payments** | Logs payment releases (+txHash) |
|
||||||
|
| **notifications** | In-app notifications for verification or payment updates |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💎 Folder Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
D.M.T.P/
|
||||||
|
├── client/ # Next.js frontend
|
||||||
|
│ ├── app/ # App Router pages
|
||||||
|
│ ├── components/ # shadcn + custom UI
|
||||||
|
│ ├── hooks/ # Zustand + wagmi hooks
|
||||||
|
│ ├── lib/ # API client + Celo utils
|
||||||
|
│ └── types/ # Shared TypeScript models
|
||||||
|
│
|
||||||
|
└── server/ # Express backend
|
||||||
|
├── src/
|
||||||
|
│ ├── routes/ # /api/v1 tasks, submissions, users
|
||||||
|
│ ├── controllers/ # Request handlers
|
||||||
|
│ ├── services/ # AI, blockchain, queue, moderation
|
||||||
|
│ ├── workers/ # Bull verification worker
|
||||||
|
│ └── database/ # Prisma schema + migrations
|
||||||
|
└── prisma/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Setup Guide
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- ✅ Node.js >= 20
|
||||||
|
- ✅ npm >= 9
|
||||||
|
- ✅ Redis (running locally or hosted)
|
||||||
|
- ✅ PostgreSQL database
|
||||||
|
- ✅ Celo wallet with test cUSD
|
||||||
|
|
||||||
|
### 1️⃣ Environment Variables
|
||||||
|
|
||||||
|
#### `.env` (Backend)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Server
|
||||||
|
PORT=3001
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/D.M.T.P"
|
||||||
|
|
||||||
|
# Blockchain
|
||||||
|
PRIVATE_KEY=YOUR_PRIVATE_KEY_WITH_CUSD
|
||||||
|
CELO_RPC_URL=https://forno.celo-sepolia.celo-testnet.org
|
||||||
|
CONTRACT_ADDRESS=0xYourTaskEscrowAddress
|
||||||
|
CUSD_SEPOLIA_ADDRESS=0x874069fa1eb16d44d622f2e0ca25eea172369bc1
|
||||||
|
|
||||||
|
# AI (Gemini)
|
||||||
|
GEMINI_API_KEY=your_google_gemini_api_key
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `.env.local` (Frontend)
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||||
|
NEXT_PUBLIC_CELO_RPC_URL=https://forno.celo-sepolia.celo-testnet.org
|
||||||
|
NEXT_PUBLIC_CUSD_ADDRESS=0x874069fa1eb16d44d622f2e0ca25eea172369bc1
|
||||||
|
NEXT_PUBLIC_CONTRACT_ADDRESS=0xYourTaskEscrowAddress
|
||||||
|
NEXT_PUBLIC_CHAIN_ID=44787
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ Backend Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Prisma setup + migrate
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma db push
|
||||||
|
|
||||||
|
# Start Redis (using Docker)
|
||||||
|
docker run -d -p 6379:6379 redis:alpine
|
||||||
|
|
||||||
|
# Start backend in dev mode
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ Frontend Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit → [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 Smart Contract Deployment
|
||||||
|
|
||||||
|
### Our Deployed Smart Contract- [0xa520d207c91C0FE0e9cFe8D63AbE02fd18B2254e](https://sepolia.celoscan.io/address/0xa520d207c91c0fe0e9cfe8d63abe02fd18b2254e)
|
||||||
|
Deploy `TaskEscrow.sol` on **Celo Sepolia** using Remix or Hardhat.
|
||||||
|
|
||||||
|
### Compile & Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat run scripts/deploy.ts --network celo-sepolia
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Contract
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat verify --network celo-sepolia 0xYourContractAddress "0x874069fa1eb16d44d622f2e0ca25eea172369bc1"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Workflow
|
||||||
|
|
||||||
|
### ✳️ Requester Flow
|
||||||
|
|
||||||
|
1. Connect wallet → Create Task
|
||||||
|
2. Payment locked in smart contract
|
||||||
|
3. Task visible to workers
|
||||||
|
|
||||||
|
### 🛠️ Worker Flow
|
||||||
|
|
||||||
|
1. Accept Task → Submit Response
|
||||||
|
2. Gemini AI verifies content
|
||||||
|
3. If approved ✅ → Payment auto-released (cUSD)
|
||||||
|
4. If rejected ❌ → Refund to requester
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧾 Example API Calls
|
||||||
|
|
||||||
|
### Create Task
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/tasks/create
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Label Images",
|
||||||
|
"description": "Tag each image with emotion category",
|
||||||
|
"paymentAmount": 3.5,
|
||||||
|
"expiresAt": "2025-11-10T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Submit Task
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/submissions/submit
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"taskId": "uuid",
|
||||||
|
"submissionData": {
|
||||||
|
"imageUrls": ["https://ipfs.tech/image1.png"],
|
||||||
|
"metadata": { "label": "happy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI Queue → Gemini Verification → Payment Released**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────┐
|
||||||
|
│ Next.js │────┐
|
||||||
|
└────────────┘ │
|
||||||
|
▼
|
||||||
|
┌───────────┐ ┌──────────────┐
|
||||||
|
│Express.js │◄─│ PostgreSQL │
|
||||||
|
└───────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
┌─────┼──────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
Gemini Redis Celo
|
||||||
|
AI /Bull Network
|
||||||
|
(Verify) (Queue) (Escrow)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Features Checklist
|
||||||
|
|
||||||
|
- [X] Wallet Signature Login (EIP-191)
|
||||||
|
- [X] Gemini-based Content Moderation
|
||||||
|
- [X] Blockchain-Backed Escrow Payments
|
||||||
|
- [X] Task + Submission CRUD
|
||||||
|
- [X] Bull Queue Worker Verification
|
||||||
|
- [X] Real-Time Status Updates
|
||||||
|
- [X] cUSD Balance Tracking
|
||||||
|
- [X] Transaction Confirmation + CeloScan Link
|
||||||
|
- [X] Admin + Requester Dashboard
|
||||||
|
- [X] Fully Responsive UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run backend tests
|
||||||
|
cd server
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Database Studio
|
||||||
|
npx prisma studio
|
||||||
|
|
||||||
|
# Queue dashboard
|
||||||
|
npm run bull:dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Deployment
|
||||||
|
|
||||||
|
### Suggested Setup
|
||||||
|
|
||||||
|
| Layer | Platform | Notes |
|
||||||
|
| -------- | ------------------------------------------------------- | ------------------------ |
|
||||||
|
| Frontend | [Vercel](https://vercel.com) | Auto-deploy from main |
|
||||||
|
| Backend | [Railway](https://railway.app) / [Render](https://render.com) | Node + Postgres service |
|
||||||
|
| Database | Neon / Supabase | Free Postgres DB |
|
||||||
|
| Redis | Upstash / Redis Cloud | Connection for Bull jobs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Future Roadmap
|
||||||
|
|
||||||
|
- 🪄 On-chain Gemini verification proofs
|
||||||
|
- 🧩 Decentralized task reputation scoring
|
||||||
|
- 💬 Worker messaging + chat
|
||||||
|
- 🪶 File uploads to IPFS / Web3.Storage
|
||||||
|
- ⚙️ Multi-network support (Base, Polygon)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪙 Credits
|
||||||
|
|
||||||
|
Developed with 💚 on the Celo blockchain.
|
||||||
|
|
||||||
|
Built using **Next.js**, **Celo ContractKit**, **Gemini AI**, and **Prisma**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License** — feel free to fork and extend 💡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 Contact
|
||||||
|
|
||||||
|
For questions or support, please open an issue on [GitHub](https://github.com/Rio-awsm/micro-job-ai-agent-web3/issues).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">Made with ❤️ for the Celo ecosystem</p>
|
||||||
41
dmtp/client/.gitignore
vendored
Normal file
41
dmtp/client/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
dmtp/client/README.md
Normal file
36
dmtp/client/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
dmtp/client/components.json
Normal file
22
dmtp/client/components.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
62
dmtp/client/components/auth/AuthGuard.tsx
Normal file
62
dmtp/client/components/auth/AuthGuard.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useWalletConnection } from '@/hooks/useWalletConnection';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface AuthGuardProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that requires authentication
|
||||||
|
* Shows fallback or warning if user is not authenticated
|
||||||
|
*/
|
||||||
|
export function AuthGuard({ children, fallback }: AuthGuardProps) {
|
||||||
|
const { isConnected } = useWalletConnection();
|
||||||
|
const { isAuthenticated, authenticate, isAuthenticating } = useAuth();
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return (
|
||||||
|
fallback || (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="max-w-md mx-auto bg-green-50 border border-green-200 p-6">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
🔌 Wallet Not Connected
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
Please connect your wallet to access this feature.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
fallback || (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="max-w-md mx-auto bg-green-50 border border-green-200 p-6">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-4">
|
||||||
|
🔐 Authentication Required
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-700 mb-4">
|
||||||
|
Please sign a message to verify your wallet ownership.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => authenticate()}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Authenticating...' : 'Sign Message'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
70
dmtp/client/components/layout/Navbar.tsx
Normal file
70
dmtp/client/components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { WalletButton } from '@/components/wallet/WalletButton';
|
||||||
|
import { useWalletConnection } from '@/hooks/useWalletConnection';
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
const { isConnected } = useWalletConnection();
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="fixed top-0 left-0 right-0 z-50 bg-white border-b-2 border-gray-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/" className="flex items-center gap-2 font-bold text-xl">
|
||||||
|
<div className="px-2 bg-primary flex items-center justify-center border-2 border-primary">
|
||||||
|
<span className="text-white text-2xl font-bold">D.M.T.P</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center gap-1">
|
||||||
|
<Link href="/tasks" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
|
||||||
|
Marketplace
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link href="/profile" className="text-sm font-medium px-4 py-2 hover:bg-gray-100 transition-colors text-gray-700 hover:text-primary">
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Actions */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="hidden sm:inline-flex">
|
||||||
|
<WalletButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button className="md:hidden p-2 hover:bg-gray-100 rounded" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||||
|
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden pb-4 space-y-1 border-t border-gray-200 pt-2">
|
||||||
|
<Link href="/tasks" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
|
||||||
|
Marketplace
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link href="/profile" className="block px-4 py-2 hover:bg-gray-100 font-medium text-gray-700">
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<WalletButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
dmtp/client/components/layout/footer.tsx
Normal file
132
dmtp/client/components/layout/footer.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Github, Linkedin, Mail, Twitter } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="bg-gray-50 border-t-2 border-gray-200 mt-0">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||||
|
{/* Brand */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="w-8 h-8 bg-primary flex items-center justify-center border-2 border-primary">
|
||||||
|
<span className="text-white text-sm font-bold">C</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-lg text-gray-900">
|
||||||
|
D.M.T.P
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 text-sm">AI-powered microtask marketplace on Celo Sepolia</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold mb-4 text-gray-900">Product</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-600">
|
||||||
|
<li>
|
||||||
|
<Link href="/marketplace" className="hover:text-primary transition-colors">
|
||||||
|
Marketplace
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/dashboard" className="hover:text-primary transition-colors">
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Pricing
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Features
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Company */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold mb-4 text-gray-900">Company</h3>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-600">
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Blog
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Careers
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold mb-4 text-gray-900">Follow Us</h3>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-gray-600 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Twitter className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-gray-600 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-gray-600 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Linkedin className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-gray-600 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Mail className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t-2 border-gray-200 pt-8">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center text-sm text-gray-600">
|
||||||
|
<p>© {currentYear} D.M.T.P. All rights reserved.</p>
|
||||||
|
<div className="flex gap-6 mt-4 md:mt-0">
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-primary transition-colors">
|
||||||
|
Cookie Policy
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
dmtp/client/components/modals/AuthModal.tsx
Normal file
64
dmtp/client/components/modals/AuthModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface AuthModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthModal({ isOpen, onClose, onSuccess }: AuthModalProps) {
|
||||||
|
const { authenticate, isAuthenticating, authError } = useAuth();
|
||||||
|
|
||||||
|
const handleAuthenticate = async () => {
|
||||||
|
const success = await authenticate();
|
||||||
|
if (success) {
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white p-6 max-w-md w-full mx-4">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||||
|
🔐 Authentication Required
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
Your session has expired. Please sign a message with your wallet to continue.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{authError && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 mb-4">
|
||||||
|
{authError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleAuthenticate}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Authenticating...' : 'Sign Message'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="px-4 py-2 bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 mt-4">
|
||||||
|
This signature will not trigger any blockchain transaction or cost gas fees.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
dmtp/client/components/modals/NetworkSwitchModal.tsx
Normal file
76
dmtp/client/components/modals/NetworkSwitchModal.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useWalletConnection } from '@/hooks/useWalletConnection';
|
||||||
|
import { getCurrentNetwork } from '@/lib/celo';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { LoadingSpinner } from '../ui/LoadingSpinner';
|
||||||
|
|
||||||
|
interface NetworkSwitchModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NetworkSwitchModal({ isOpen, onClose }: NetworkSwitchModalProps) {
|
||||||
|
const { switchNetwork } = useWalletConnection();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const network = getCurrentNetwork();
|
||||||
|
|
||||||
|
const handleSwitch = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await switchNetwork();
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white max-w-md w-full p-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Wrong Network</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Please switch to <span className="font-semibold">{network.name}</span> to continue.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 p-4 mb-6">
|
||||||
|
<p className="text-sm text-red-800">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 py-3 px-4 border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSwitch}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 py-3 px-4 bg-blue-600 text-white hover:bg-blue-700 transition-colors font-medium disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<LoadingSpinner size="sm" />
|
||||||
|
Switching...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Switch Network'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
dmtp/client/components/modals/TransactionModal.tsx
Normal file
70
dmtp/client/components/modals/TransactionModal.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTransactions } from '@/hooks/useTransactions';
|
||||||
|
import { getExplorerUrl } from '@/lib/celo';
|
||||||
|
import { LoadingSpinner } from '../ui/LoadingSpinner';
|
||||||
|
|
||||||
|
export function TransactionModal() {
|
||||||
|
const { currentTx, updateTransaction } = useTransactions();
|
||||||
|
|
||||||
|
if (!currentTx) return null;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
updateTransaction(currentTx.hash, { ...currentTx });
|
||||||
|
// You might want to actually close the modal here
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white max-w-md w-full p-6">
|
||||||
|
{/* Status Icon */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
{currentTx.status === 'pending' && (
|
||||||
|
<LoadingSpinner size="lg" />
|
||||||
|
)}
|
||||||
|
{currentTx.status === 'success' && (
|
||||||
|
<div className="text-6xl">✅</div>
|
||||||
|
)}
|
||||||
|
{currentTx.status === 'failed' && (
|
||||||
|
<div className="text-6xl">❌</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h2 className="text-2xl font-bold text-center text-gray-900 mb-2">
|
||||||
|
{currentTx.status === 'pending' && 'Transaction Pending'}
|
||||||
|
{currentTx.status === 'success' && 'Transaction Successful'}
|
||||||
|
{currentTx.status === 'failed' && 'Transaction Failed'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-center text-gray-600 mb-6">{currentTx.description}</p>
|
||||||
|
|
||||||
|
{/* Transaction Hash */}
|
||||||
|
{currentTx.hash && (
|
||||||
|
<div className="bg-gray-50 p-4 mb-6">
|
||||||
|
<p className="text-sm text-gray-600 mb-2">Transaction Hash:</p>
|
||||||
|
<a
|
||||||
|
href={getExplorerUrl(currentTx.hash)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm font-mono text-blue-600 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{currentTx.hash}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
{currentTx.status !== 'pending' && (
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="w-full py-3 bg-blue-600 text-white hover:bg-blue-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
dmtp/client/components/tasks/TaskCard.tsx
Normal file
65
dmtp/client/components/tasks/TaskCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { formatCurrency, formatTimeRemaining } from '@/lib/utils';
|
||||||
|
import { Task, TaskType } from '@/types';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface TaskCardProps {
|
||||||
|
task: Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskCard({ task }: TaskCardProps) {
|
||||||
|
const taskTypeLabels: Record<TaskType, string> = {
|
||||||
|
[TaskType.TEXT_VERIFICATION]: 'Text',
|
||||||
|
[TaskType.IMAGE_LABELING]: 'Image',
|
||||||
|
[TaskType.SURVEY]: 'Survey',
|
||||||
|
[TaskType.CONTENT_MODERATION]: 'Moderation',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/tasks/${task.id}`}>
|
||||||
|
<div className="bg-white border-2 border-gray-200 hover:border-primary transition-colors p-6 cursor-pointer">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<h3 className="text-base font-bold text-gray-900 line-clamp-2">
|
||||||
|
{task.title}
|
||||||
|
</h3>
|
||||||
|
{task.paymentAmount >= 5 && (
|
||||||
|
<span className="text-xs bg-primary text-white px-2 py-1 font-semibold ml-2 flex-shrink-0">
|
||||||
|
HIGH
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-gray-600 mb-4 line-clamp-2 leading-relaxed">{task.description}</p>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4 pb-4 border-b border-gray-200">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-500 mb-1">Payment</div>
|
||||||
|
<div className="font-bold text-primary text-base">
|
||||||
|
{formatCurrency(task.paymentAmount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-500 mb-1">Available</div>
|
||||||
|
<div className="font-semibold text-gray-900 text-base">
|
||||||
|
{task.spotsRemaining}/{task.maxSubmissions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-700 border border-gray-200 font-medium">
|
||||||
|
{taskTypeLabels[task.taskType]}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 font-medium">
|
||||||
|
{formatTimeRemaining(task.expiresAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
dmtp/client/components/theme-provider.tsx
Normal file
11
dmtp/client/components/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
13
dmtp/client/components/ui/LoadingSpinner.tsx
Normal file
13
dmtp/client/components/ui/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export function LoadingSpinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
|
||||||
|
const sizes = {
|
||||||
|
sm: 'w-4 h-4 border-2',
|
||||||
|
md: 'w-6 h-6 border-3',
|
||||||
|
lg: 'w-8 h-8 border-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${sizes[size]} border-primary border-t-transparent rounded-full animate-spin`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
dmtp/client/components/ui/button.tsx
Normal file
60
dmtp/client/components/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-semibold transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 border-2 border-primary",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 border-2 border-destructive",
|
||||||
|
outline:
|
||||||
|
"border-2 bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-2 border-secondary",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-5 py-2 has-[>svg]:px-4",
|
||||||
|
sm: "h-9 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-11 px-7 has-[>svg]:px-5",
|
||||||
|
icon: "size-10",
|
||||||
|
"icon-sm": "size-9",
|
||||||
|
"icon-lg": "size-11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
92
dmtp/client/components/ui/card.tsx
Normal file
92
dmtp/client/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 border-2 py-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
102
dmtp/client/components/wallet/WalletButton.tsx
Normal file
102
dmtp/client/components/wallet/WalletButton.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useCUSDBalance } from '@/hooks/useCUSDBalance';
|
||||||
|
import { useWalletConnection } from '@/hooks/useWalletConnection';
|
||||||
|
import { formatAddress } from '@/lib/celo';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { NetworkSwitchModal } from '../modals/NetworkSwitchModal';
|
||||||
|
|
||||||
|
export function WalletButton() {
|
||||||
|
const { address, isConnected, isConnecting, connect, disconnect, chainId } = useWalletConnection();
|
||||||
|
const { authenticate, isAuthenticating, clearAuth, isAuthenticated } = useAuth();
|
||||||
|
const { data: balance } = useCUSDBalance(address);
|
||||||
|
const [showNetworkModal, setShowNetworkModal] = useState(false);
|
||||||
|
|
||||||
|
const expectedChainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '11142220');
|
||||||
|
const isWrongNetwork = isConnected && chainId !== expectedChainId;
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
try {
|
||||||
|
// Step 1: Connect wallet
|
||||||
|
await connect();
|
||||||
|
|
||||||
|
// Step 2: Authenticate
|
||||||
|
await authenticate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection/Authentication error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
// Clear authentication data
|
||||||
|
clearAuth();
|
||||||
|
|
||||||
|
// Disconnect wallet
|
||||||
|
disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show "Re-authenticate" button if connected but not authenticated
|
||||||
|
if (isConnected && address && !isAuthenticated && !isWrongNetwork) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => authenticate()}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Authenticating...' : '🔐 Sign to Authenticate'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWrongNetwork) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNetworkModal(true)}
|
||||||
|
className="px-6 py-2 bg-red-600 text-white hover:bg-red-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Wrong Network
|
||||||
|
</button>
|
||||||
|
<NetworkSwitchModal
|
||||||
|
isOpen={showNetworkModal}
|
||||||
|
onClose={() => setShowNetworkModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected && address) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Balance */}
|
||||||
|
<div className="hidden md:block px-4 py-2 bg-green-50 text-green-900 text-sm font-medium">
|
||||||
|
{parseFloat(balance || '0').toFixed(2)} cUSD
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
|
<div className="px-4 py-2 bg-blue-50 text-blue-900 text-sm font-medium">
|
||||||
|
{formatAddress(address)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disconnect */}
|
||||||
|
<button
|
||||||
|
onClick={handleDisconnect}
|
||||||
|
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={isConnecting || isAuthenticating}
|
||||||
|
className="px-6 py-2 bg-green-600 text-white hover:bg-green-700 transition-colors font-medium disabled:bg-gray-400 animate-pulse"
|
||||||
|
>
|
||||||
|
{isConnecting || isAuthenticating ? 'Connecting...' : 'Connect Wallet'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
dmtp/client/hooks/useAuth.ts
Normal file
98
dmtp/client/hooks/useAuth.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { AuthService } from '@/lib/auth';
|
||||||
|
import { useWalletConnection } from './useWalletConnection';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const { address, isConnected, signMessage } = useWalletConnection();
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||||
|
const [authError, setAuthError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Check authentication status on mount and when wallet changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
const authenticated = AuthService.isAuthenticated();
|
||||||
|
setIsAuthenticated(authenticated);
|
||||||
|
|
||||||
|
// If wallet is connected but not authenticated, show warning
|
||||||
|
if (isConnected && !authenticated) {
|
||||||
|
console.warn('⚠️ Wallet connected but not authenticated');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
|
||||||
|
// Check auth status every 30 seconds
|
||||||
|
const interval = setInterval(checkAuth, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isConnected, address]);
|
||||||
|
|
||||||
|
const authenticate = async () => {
|
||||||
|
if (!isConnected || !address) {
|
||||||
|
setAuthError('Please connect your wallet first');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAuthenticating(true);
|
||||||
|
setAuthError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear any old auth before registering to avoid sending expired credentials
|
||||||
|
const oldAuthExists = AuthService.isAuthenticated();
|
||||||
|
if (!oldAuthExists) {
|
||||||
|
AuthService.clearAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Register user if needed
|
||||||
|
try {
|
||||||
|
await api.users.register({
|
||||||
|
walletAddress: address,
|
||||||
|
role: 'worker',
|
||||||
|
});
|
||||||
|
console.log('✅ User registered successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
// User might already exist, which is fine
|
||||||
|
if (error.response?.status !== 409) {
|
||||||
|
console.log('Registration note:', error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Generate and sign authentication message
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const message = AuthService.generateAuthMessage(address, timestamp);
|
||||||
|
const signature = await signMessage(message);
|
||||||
|
|
||||||
|
// Step 3: Store authentication
|
||||||
|
AuthService.storeAuth(address, signature, message, timestamp);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
|
||||||
|
console.log('✅ Authentication successful');
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Authentication error:', error);
|
||||||
|
setAuthError(error.message || 'Authentication failed');
|
||||||
|
AuthService.clearAuth();
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAuth = () => {
|
||||||
|
AuthService.clearAuth();
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setAuthError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated,
|
||||||
|
isAuthenticating,
|
||||||
|
authError,
|
||||||
|
authenticate,
|
||||||
|
clearAuth,
|
||||||
|
};
|
||||||
|
}
|
||||||
38
dmtp/client/hooks/useCUSDBalance.ts
Normal file
38
dmtp/client/hooks/useCUSDBalance.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getCUSDContract, getProvider } from '@/lib/contracts';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
export function useCUSDBalance(address: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['cusd-balance', address],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!address) return '0';
|
||||||
|
|
||||||
|
const provider = getProvider();
|
||||||
|
const cUSDContract = getCUSDContract(provider);
|
||||||
|
|
||||||
|
const balance = await cUSDContract.balanceOf(address);
|
||||||
|
return ethers.formatEther(balance);
|
||||||
|
},
|
||||||
|
enabled: !!address,
|
||||||
|
refetchInterval: 10000, // Refetch every 10 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCUSDAllowance(owner: string | null, spender: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['cusd-allowance', owner, spender],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!owner) return '0';
|
||||||
|
|
||||||
|
const provider = getProvider();
|
||||||
|
const cUSDContract = getCUSDContract(provider);
|
||||||
|
|
||||||
|
const allowance = await cUSDContract.allowance(owner, spender);
|
||||||
|
return ethers.formatEther(allowance);
|
||||||
|
},
|
||||||
|
enabled: !!owner,
|
||||||
|
});
|
||||||
|
}
|
||||||
123
dmtp/client/hooks/useTaskContract.ts
Normal file
123
dmtp/client/hooks/useTaskContract.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { parseErrorMessage } from '@/lib/celo';
|
||||||
|
import { getCUSDContract, getTaskEscrowContract } from '@/lib/contracts';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useWalletConnection } from './useWalletConnection';
|
||||||
|
|
||||||
|
export function useTaskContract() {
|
||||||
|
const { signer, address } = useWalletConnection();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create task on blockchain
|
||||||
|
*/
|
||||||
|
const createTask = async (paymentAmount: number, durationInDays: number) => {
|
||||||
|
if (!signer || !address) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
|
||||||
|
if (!contractAddress) {
|
||||||
|
throw new Error('Contract address not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Approve cUSD spending
|
||||||
|
console.log('📝 Approving cUSD spending...');
|
||||||
|
const cUSDContract = getCUSDContract(signer);
|
||||||
|
const amount = ethers.parseEther(paymentAmount.toString());
|
||||||
|
|
||||||
|
const approveTx = await cUSDContract.approve(contractAddress, amount);
|
||||||
|
await approveTx.wait();
|
||||||
|
console.log('✅ cUSD approved');
|
||||||
|
|
||||||
|
// Step 2: Create task
|
||||||
|
console.log('📝 Creating task on blockchain...');
|
||||||
|
const taskContract = getTaskEscrowContract(signer);
|
||||||
|
const createTx = await taskContract.createTask(amount, durationInDays);
|
||||||
|
|
||||||
|
console.log('⏳ Waiting for confirmation...');
|
||||||
|
const receipt = await createTx.wait();
|
||||||
|
|
||||||
|
// Parse event to get taskId
|
||||||
|
const event = receipt.logs.find((log: any) => {
|
||||||
|
try {
|
||||||
|
const parsed = taskContract.interface.parseLog(log);
|
||||||
|
return parsed?.name === 'TaskCreated';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let taskId = 0;
|
||||||
|
if (event) {
|
||||||
|
const parsedEvent = taskContract.interface.parseLog(event);
|
||||||
|
taskId = Number(parsedEvent?.args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Task created! Task ID:', taskId);
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
return {
|
||||||
|
taskId,
|
||||||
|
txHash: receipt.hash,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = parseErrorMessage(err);
|
||||||
|
setError(errorMessage);
|
||||||
|
setIsLoading(false);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check task status on blockchain
|
||||||
|
*/
|
||||||
|
const checkTaskStatus = async (taskId: number) => {
|
||||||
|
if (!signer) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const taskContract = getTaskEscrowContract(signer);
|
||||||
|
const task = await taskContract.getTask(taskId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskId: Number(task.taskId),
|
||||||
|
requester: task.requester,
|
||||||
|
worker: task.worker,
|
||||||
|
paymentAmount: ethers.formatEther(task.paymentAmount),
|
||||||
|
status: Number(task.status),
|
||||||
|
createdAt: Number(task.createdAt),
|
||||||
|
expiresAt: Number(task.expiresAt),
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = parseErrorMessage(err);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current task counter
|
||||||
|
*/
|
||||||
|
const getTaskCounter = async () => {
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_CELO_RPC_URL);
|
||||||
|
const taskContract = getTaskEscrowContract(provider);
|
||||||
|
const counter = await taskContract.taskCounter();
|
||||||
|
return Number(counter);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createTask,
|
||||||
|
checkTaskStatus,
|
||||||
|
getTaskCounter,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
52
dmtp/client/hooks/useTransactions.ts
Normal file
52
dmtp/client/hooks/useTransactions.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
hash: string;
|
||||||
|
status: 'pending' | 'success' | 'failed';
|
||||||
|
description: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionState {
|
||||||
|
transactions: Transaction[];
|
||||||
|
currentTx: Transaction | null;
|
||||||
|
|
||||||
|
addTransaction: (tx: Omit<Transaction, 'timestamp'>) => void;
|
||||||
|
updateTransaction: (hash: string, updates: Partial<Transaction>) => void;
|
||||||
|
clearTransactions: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTransactions = create<TransactionState>((set) => ({
|
||||||
|
transactions: [],
|
||||||
|
currentTx: null,
|
||||||
|
|
||||||
|
addTransaction: (tx) => {
|
||||||
|
const transaction: Transaction = {
|
||||||
|
...tx,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
transactions: [transaction, ...state.transactions],
|
||||||
|
currentTx: transaction,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTransaction: (hash, updates) => {
|
||||||
|
set((state) => ({
|
||||||
|
transactions: state.transactions.map((tx) =>
|
||||||
|
tx.hash === hash ? { ...tx, ...updates } : tx
|
||||||
|
),
|
||||||
|
currentTx:
|
||||||
|
state.currentTx?.hash === hash
|
||||||
|
? { ...state.currentTx, ...updates }
|
||||||
|
: state.currentTx,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTransactions: () => {
|
||||||
|
set({ transactions: [], currentTx: null });
|
||||||
|
},
|
||||||
|
}));
|
||||||
5
dmtp/client/hooks/useWallet.ts
Normal file
5
dmtp/client/hooks/useWallet.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// Re-export useWalletConnection as useWallet for backwards compatibility
|
||||||
|
export { useWalletConnection as useWallet } from './useWalletConnection';
|
||||||
|
|
||||||
189
dmtp/client/hooks/useWalletConnection.ts
Normal file
189
dmtp/client/hooks/useWalletConnection.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getCurrentNetwork, parseErrorMessage } from '@/lib/celo';
|
||||||
|
import { getWalletProvider, isWalletAvailable } from '@/lib/minipay';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface WalletState {
|
||||||
|
address: string | null;
|
||||||
|
chainId: number | null;
|
||||||
|
isConnected: boolean;
|
||||||
|
isConnecting: boolean;
|
||||||
|
provider: ethers.BrowserProvider | null;
|
||||||
|
signer: ethers.Signer | null;
|
||||||
|
error: string | null;
|
||||||
|
walletType: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
connect: () => Promise<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
switchNetwork: () => Promise<void>;
|
||||||
|
signMessage: (message: string) => Promise<string>;
|
||||||
|
initialize: () => Promise<void>; // Add this
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWalletConnection = create<WalletState>((set, get) => ({
|
||||||
|
address: null,
|
||||||
|
chainId: null,
|
||||||
|
isConnected: false,
|
||||||
|
isConnecting: false,
|
||||||
|
provider: null,
|
||||||
|
signer: null,
|
||||||
|
error: null,
|
||||||
|
walletType: null,
|
||||||
|
|
||||||
|
initialize: async () => {
|
||||||
|
// Check if wallet was previously connected
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
if (!isWalletAvailable()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const accounts = await provider.listAccounts();
|
||||||
|
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
// Auto-connect if previously connected
|
||||||
|
await get().connect();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Not previously connected');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
connect: async () => {
|
||||||
|
set({ isConnecting: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isWalletAvailable()) {
|
||||||
|
throw new Error('No wallet detected. Please install MiniPay or MetaMask.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
|
||||||
|
const accounts = await provider.send('eth_requestAccounts', []);
|
||||||
|
const address = accounts[0];
|
||||||
|
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = Number(network.chainId);
|
||||||
|
|
||||||
|
const expectedChainId = getCurrentNetwork().chainId;
|
||||||
|
if (chainId !== expectedChainId) {
|
||||||
|
await get().switchNetwork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
const walletType = getWalletProvider();
|
||||||
|
|
||||||
|
set({
|
||||||
|
address,
|
||||||
|
chainId,
|
||||||
|
provider,
|
||||||
|
signer,
|
||||||
|
isConnected: true,
|
||||||
|
isConnecting: false,
|
||||||
|
walletType,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Connected to ${walletType}:`, address);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = parseErrorMessage(error);
|
||||||
|
set({
|
||||||
|
error: errorMessage,
|
||||||
|
isConnecting: false,
|
||||||
|
});
|
||||||
|
console.error('Wallet connection error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect: () => {
|
||||||
|
set({
|
||||||
|
address: null,
|
||||||
|
chainId: null,
|
||||||
|
provider: null,
|
||||||
|
signer: null,
|
||||||
|
isConnected: false,
|
||||||
|
walletType: null,
|
||||||
|
});
|
||||||
|
console.log('🔌 Wallet disconnected');
|
||||||
|
},
|
||||||
|
|
||||||
|
switchNetwork: async () => {
|
||||||
|
try {
|
||||||
|
const targetNetwork = getCurrentNetwork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: `0x${targetNetwork.chainId.toString(16)}` }],
|
||||||
|
});
|
||||||
|
} catch (switchError: any) {
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
chainId: `0x${targetNetwork.chainId.toString(16)}`,
|
||||||
|
chainName: targetNetwork.name,
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'CELO',
|
||||||
|
symbol: 'CELO',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
rpcUrls: [targetNetwork.rpcUrl],
|
||||||
|
blockExplorerUrls: [targetNetwork.blockExplorer],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw switchError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await get().connect();
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = parseErrorMessage(error);
|
||||||
|
set({ error: errorMessage });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
signMessage: async (message: string) => {
|
||||||
|
const { signer } = get();
|
||||||
|
|
||||||
|
if (!signer) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signature = await signer.signMessage(message);
|
||||||
|
return signature;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(parseErrorMessage(error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Initialize on client side
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Initialize wallet connection on mount
|
||||||
|
useWalletConnection.getState().initialize();
|
||||||
|
|
||||||
|
// Listen for account changes
|
||||||
|
if (window.ethereum) {
|
||||||
|
window.ethereum.on('accountsChanged', (accounts: string[]) => {
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
useWalletConnection.getState().disconnect();
|
||||||
|
} else {
|
||||||
|
useWalletConnection.getState().connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ethereum.on('chainChanged', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
106
dmtp/client/lib/api.ts
Normal file
106
dmtp/client/lib/api.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { AuthService } from './auth';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
|
export const apiClient = axios.create({
|
||||||
|
baseURL: `${API_BASE_URL}/api/v1`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store for authentication callbacks
|
||||||
|
let authModalCallbacks: Array<() => void> = [];
|
||||||
|
|
||||||
|
export const triggerAuthModal = (callback?: () => void) => {
|
||||||
|
if (callback) {
|
||||||
|
authModalCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
// Dispatch a custom event that components can listen to
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('auth-required'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onAuthSuccess = () => {
|
||||||
|
authModalCallbacks.forEach(cb => cb());
|
||||||
|
authModalCallbacks = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add auth interceptor
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
apiClient.interceptors.request.use((config) => {
|
||||||
|
const walletAddress = localStorage.getItem('walletAddress');
|
||||||
|
const signature = localStorage.getItem('signature');
|
||||||
|
const message = localStorage.getItem('message');
|
||||||
|
const timestamp = localStorage.getItem('timestamp');
|
||||||
|
|
||||||
|
if (walletAddress && signature && message && timestamp) {
|
||||||
|
config.headers['X-Wallet-Address'] = walletAddress;
|
||||||
|
config.headers['X-Signature'] = signature;
|
||||||
|
// Base64 encode the message to handle newlines and special characters
|
||||||
|
config.headers['X-Message'] = btoa(encodeURIComponent(message));
|
||||||
|
config.headers['X-Timestamp'] = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add response interceptor to handle auth errors
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Clear invalid auth data
|
||||||
|
AuthService.clearAuth();
|
||||||
|
|
||||||
|
// Trigger re-authentication
|
||||||
|
console.error('Authentication expired. Please reconnect your wallet.');
|
||||||
|
triggerAuthModal();
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
tasks: {
|
||||||
|
list: async (params?: any) => {
|
||||||
|
const response = await apiClient.get('/tasks/list', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getById: async (taskId: string) => {
|
||||||
|
const response = await apiClient.get(`/tasks/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
create: async (data: any) => {
|
||||||
|
const response = await apiClient.post('/tasks/create', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submissions: {
|
||||||
|
submit: async (data: any) => {
|
||||||
|
const response = await apiClient.post('/submissions/submit', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getStatus: async (submissionId: string) => {
|
||||||
|
const response = await apiClient.get(`/submissions/${submissionId}/status`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
mySubmissions: async () => {
|
||||||
|
const response = await apiClient.get('/submissions/my/submissions');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
register: async (data: any) => {
|
||||||
|
const response = await apiClient.post('/users/register', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getProfile: async () => {
|
||||||
|
const response = await apiClient.get('/users/profile');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
73
dmtp/client/lib/auth.ts
Normal file
73
dmtp/client/lib/auth.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Authentication utilities for wallet-based auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
private static readonly AUTH_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes, matching server
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated with valid credentials
|
||||||
|
*/
|
||||||
|
static isAuthenticated(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
const walletAddress = localStorage.getItem('walletAddress');
|
||||||
|
const signature = localStorage.getItem('signature');
|
||||||
|
const timestamp = localStorage.getItem('timestamp');
|
||||||
|
|
||||||
|
if (!walletAddress || !signature || !timestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if timestamp is still valid
|
||||||
|
const authTimestamp = parseInt(timestamp);
|
||||||
|
const now = Date.now();
|
||||||
|
const age = now - authTimestamp;
|
||||||
|
|
||||||
|
return age <= this.AUTH_EXPIRY_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear authentication data
|
||||||
|
*/
|
||||||
|
static clearAuth(): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
localStorage.removeItem('walletAddress');
|
||||||
|
localStorage.removeItem('signature');
|
||||||
|
localStorage.removeItem('message');
|
||||||
|
localStorage.removeItem('timestamp');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored wallet address
|
||||||
|
*/
|
||||||
|
static getWalletAddress(): string | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
return localStorage.getItem('walletAddress');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store authentication data
|
||||||
|
*/
|
||||||
|
static storeAuth(
|
||||||
|
walletAddress: string,
|
||||||
|
signature: string,
|
||||||
|
message: string,
|
||||||
|
timestamp: number
|
||||||
|
): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
localStorage.setItem('walletAddress', walletAddress);
|
||||||
|
localStorage.setItem('signature', signature);
|
||||||
|
localStorage.setItem('message', message);
|
||||||
|
localStorage.setItem('timestamp', timestamp.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate authentication message for signing
|
||||||
|
*/
|
||||||
|
static generateAuthMessage(walletAddress: string, timestamp: number): string {
|
||||||
|
return `Sign this message to authenticate with Celo Task Marketplace.\n\nWallet: ${walletAddress}\nTimestamp: ${timestamp}\n\nThis request will not trigger a blockchain transaction or cost any gas fees.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
dmtp/client/lib/celo.ts
Normal file
66
dmtp/client/lib/celo.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
// Network configurations
|
||||||
|
export const CELO_NETWORKS = {
|
||||||
|
mainnet: {
|
||||||
|
chainId: 42220,
|
||||||
|
name: 'Celo Mainnet',
|
||||||
|
rpcUrl: 'https://forno.celo.org',
|
||||||
|
blockExplorer: 'https://celoscan.io',
|
||||||
|
cUSDAddress: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
|
||||||
|
},
|
||||||
|
sepolia: {
|
||||||
|
chainId: 11142220,
|
||||||
|
name: 'Celo Sepolia Testnet',
|
||||||
|
rpcUrl: 'https://forno.celo-sepolia.celo-testnet.org',
|
||||||
|
blockExplorer: 'https://sepolia.celoscan.io',
|
||||||
|
cUSDAddress: '0x874069fa1eb16d44d622f2e0ca25eea172369bc1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get current network config
|
||||||
|
export function getCurrentNetwork() {
|
||||||
|
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '11142220');
|
||||||
|
|
||||||
|
switch (chainId) {
|
||||||
|
case 42220:
|
||||||
|
return CELO_NETWORKS.mainnet;
|
||||||
|
case 11142220:
|
||||||
|
return CELO_NETWORKS.sepolia;
|
||||||
|
default:
|
||||||
|
return CELO_NETWORKS.sepolia;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cUSD token address
|
||||||
|
export function getCUSDAddress(): string {
|
||||||
|
return getCurrentNetwork().cUSDAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get block explorer URL
|
||||||
|
export function getExplorerUrl(txHash: string): string {
|
||||||
|
return `${getCurrentNetwork().blockExplorer}/tx/${txHash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format address
|
||||||
|
export function formatAddress(address: string): string {
|
||||||
|
if (!address) return '';
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if address is valid
|
||||||
|
export function isValidAddress(address: string): boolean {
|
||||||
|
try {
|
||||||
|
return ethers.isAddress(address);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse error message
|
||||||
|
export function parseErrorMessage(error: any): string {
|
||||||
|
if (error.reason) return error.reason;
|
||||||
|
if (error.message) return error.message;
|
||||||
|
if (typeof error === 'string') return error;
|
||||||
|
return 'Transaction failed';
|
||||||
|
}
|
||||||
49
dmtp/client/lib/contracts.ts
Normal file
49
dmtp/client/lib/contracts.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { getCUSDAddress, getCurrentNetwork } from './celo';
|
||||||
|
|
||||||
|
// Simplified cUSD ABI (ERC20)
|
||||||
|
export const CUSD_ABI = [
|
||||||
|
'function balanceOf(address) view returns (uint256)',
|
||||||
|
'function decimals() view returns (uint8)',
|
||||||
|
'function approve(address spender, uint256 amount) returns (bool)',
|
||||||
|
'function allowance(address owner, address spender) view returns (uint256)',
|
||||||
|
'function transfer(address to, uint256 amount) returns (bool)',
|
||||||
|
];
|
||||||
|
|
||||||
|
// TaskEscrow contract ABI
|
||||||
|
export const TASK_ESCROW_ABI = [
|
||||||
|
'function createTask(uint256 paymentAmount, uint256 durationInDays) returns (uint256)',
|
||||||
|
'function approveSubmission(uint256 taskId)',
|
||||||
|
'function rejectSubmission(uint256 taskId)',
|
||||||
|
'function getTask(uint256 taskId) view returns (tuple(uint256 taskId, address requester, address worker, uint256 paymentAmount, uint8 status, uint256 createdAt, uint256 expiresAt))',
|
||||||
|
'function taskCounter() view returns (uint256)',
|
||||||
|
'event TaskCreated(uint256 indexed taskId, address indexed requester, uint256 paymentAmount, uint256 expiresAt)',
|
||||||
|
'event PaymentReleased(uint256 indexed taskId, address indexed worker, uint256 workerAmount, uint256 platformFee)',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cUSD contract instance
|
||||||
|
*/
|
||||||
|
export function getCUSDContract(signerOrProvider: ethers.Signer | ethers.Provider) {
|
||||||
|
return new ethers.Contract(getCUSDAddress(), CUSD_ABI, signerOrProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TaskEscrow contract instance
|
||||||
|
*/
|
||||||
|
export function getTaskEscrowContract(signerOrProvider: ethers.Signer | ethers.Provider) {
|
||||||
|
const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
|
||||||
|
|
||||||
|
if (!contractAddress) {
|
||||||
|
throw new Error('Contract address not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ethers.Contract(contractAddress, TASK_ESCROW_ABI, signerOrProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider
|
||||||
|
*/
|
||||||
|
export function getProvider(): ethers.JsonRpcProvider {
|
||||||
|
return new ethers.JsonRpcProvider(getCurrentNetwork().rpcUrl);
|
||||||
|
}
|
||||||
57
dmtp/client/lib/minipay.ts
Normal file
57
dmtp/client/lib/minipay.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ethereum?: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MiniPayProvider {
|
||||||
|
isMiniPay: boolean;
|
||||||
|
isMetaMask?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running inside MiniPay app
|
||||||
|
*/
|
||||||
|
export function isMiniPay(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
return Boolean(window.ethereum?.isMiniPay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if MetaMask is available
|
||||||
|
*/
|
||||||
|
export function isMetaMask(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
return Boolean(window.ethereum?.isMetaMask && !window.ethereum?.isMiniPay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet provider name
|
||||||
|
*/
|
||||||
|
export function getWalletProvider(): string {
|
||||||
|
if (isMiniPay()) return 'MiniPay';
|
||||||
|
if (isMetaMask()) return 'MetaMask';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any wallet is available
|
||||||
|
*/
|
||||||
|
export function isWalletAvailable(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
return Boolean(window.ethereum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet installation URL
|
||||||
|
*/
|
||||||
|
export function getWalletInstallUrl(): string {
|
||||||
|
// Check if mobile
|
||||||
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return 'https://minipay.opera.com/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'https://metamask.io/download/';
|
||||||
|
}
|
||||||
35
dmtp/client/lib/utils.ts
Normal file
35
dmtp/client/lib/utils.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatAddress = (address: string): string => {
|
||||||
|
if (!address) return '';
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCurrency = (amount: number): string => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTimeRemaining = (expiresAt: string): string => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const expiry = new Date(expiresAt).getTime();
|
||||||
|
const diff = expiry - now;
|
||||||
|
|
||||||
|
if (diff <= 0) return 'Expired';
|
||||||
|
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
if (days > 0) return `${days}d ${hours}h`;
|
||||||
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
return `${minutes}m`;
|
||||||
|
};
|
||||||
7
dmtp/client/next.config.ts
Normal file
7
dmtp/client/next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
12385
dmtp/client/package-lock.json
generated
Normal file
12385
dmtp/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
dmtp/client/package.json
Normal file
42
dmtp/client/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --webpack",
|
||||||
|
"build": "next build --webpack",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@celo/contractkit": "^10.0.2",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@rainbow-me/rainbowkit": "^2.2.9",
|
||||||
|
"@tanstack/react-query": "^5.90.5",
|
||||||
|
"@wagmi/connectors": "^6.1.0",
|
||||||
|
"@wagmi/core": "^2.22.1",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"ethers": "^6.15.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
|
"next": "16.0.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-dom": "19.2.0",
|
||||||
|
"recharts": "^3.3.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"viem": "^2.38.4",
|
||||||
|
"wagmi": "^2.18.2",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
dmtp/client/postcss.config.mjs
Normal file
7
dmtp/client/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
37
dmtp/client/providers/AuthProvider.tsx
Normal file
37
dmtp/client/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { AuthModal } from '@/components/modals/AuthModal';
|
||||||
|
import { onAuthSuccess } from '@/lib/api';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global authentication handler that shows auth modal when needed
|
||||||
|
*/
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleAuthRequired = () => {
|
||||||
|
setShowAuthModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('auth-required', handleAuthRequired);
|
||||||
|
return () => window.removeEventListener('auth-required', handleAuthRequired);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAuthSuccess = () => {
|
||||||
|
setShowAuthModal(false);
|
||||||
|
onAuthSuccess(); // Notify any pending requests
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<AuthModal
|
||||||
|
isOpen={showAuthModal}
|
||||||
|
onClose={() => setShowAuthModal(false)}
|
||||||
|
onSuccess={handleAuthSuccess}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
dmtp/client/providers/QueryProvider.tsx
Normal file
20
dmtp/client/providers/QueryProvider.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 60 * 1000, // 1 minute
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
|
}
|
||||||
1
dmtp/client/public/file.svg
Normal file
1
dmtp/client/public/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
dmtp/client/public/globe.svg
Normal file
1
dmtp/client/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
dmtp/client/public/next.svg
Normal file
1
dmtp/client/public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
dmtp/client/public/vercel.svg
Normal file
1
dmtp/client/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
dmtp/client/public/window.svg
Normal file
1
dmtp/client/public/window.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
44
dmtp/client/tsconfig.json
Normal file
44
dmtp/client/tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts",
|
||||||
|
".next\\dev/types/**/*.ts",
|
||||||
|
".next\\dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
66
dmtp/client/types/index.ts
Normal file
66
dmtp/client/types/index.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export enum TaskType {
|
||||||
|
TEXT_VERIFICATION = 'text_verification',
|
||||||
|
IMAGE_LABELING = 'image_labeling',
|
||||||
|
SURVEY = 'survey',
|
||||||
|
CONTENT_MODERATION = 'content_moderation',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskStatus {
|
||||||
|
OPEN = 'open',
|
||||||
|
IN_PROGRESS = 'in_progress',
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
EXPIRED = 'expired',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VerificationStatus {
|
||||||
|
PENDING = 'pending',
|
||||||
|
APPROVED = 'approved',
|
||||||
|
REJECTED = 'rejected',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
taskType: TaskType;
|
||||||
|
paymentAmount: number;
|
||||||
|
status: TaskStatus;
|
||||||
|
verificationCriteria: any;
|
||||||
|
maxSubmissions: number;
|
||||||
|
submissionCount: number;
|
||||||
|
spotsRemaining: number;
|
||||||
|
expiresAt: string;
|
||||||
|
timeRemaining: number;
|
||||||
|
isExpiringSoon: boolean;
|
||||||
|
requester: {
|
||||||
|
walletAddress: string;
|
||||||
|
reputationScore: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Submission {
|
||||||
|
id: string;
|
||||||
|
taskId: string;
|
||||||
|
verificationStatus: VerificationStatus;
|
||||||
|
submissionData: any;
|
||||||
|
aiVerificationResult?: any;
|
||||||
|
paymentTransactionHash?: string;
|
||||||
|
createdAt: string;
|
||||||
|
task: {
|
||||||
|
title: string;
|
||||||
|
paymentAmount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
walletAddress: string;
|
||||||
|
totalEarnings: number;
|
||||||
|
reputationScore: number;
|
||||||
|
stats: {
|
||||||
|
submissionsTotal: number;
|
||||||
|
submissionsApproved: number;
|
||||||
|
submissionsRejected: number;
|
||||||
|
approvalRate: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
9
dmtp/client_admin/.env.example
Normal file
9
dmtp/client_admin/.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copy this file to .env and fill values
|
||||||
|
CELO_RPC_URL=https://forno.celo-sepolia.celo-testnet.org
|
||||||
|
CONTRACT_ADDRESS=0xa520d207c91C0FE0e9cFe8D63AbE02fd18B2254e
|
||||||
|
PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
|
||||||
|
CUSD_SEPOLIA_ADDRESS=0x845D9D0B4Be004Dcbc17b11160B0C18abBD5FEBD
|
||||||
|
PORT=4000
|
||||||
|
BACKEND_URL=http://localhost:3001
|
||||||
|
|
||||||
|
BACKEND_URL=http://localhost:3001
|
||||||
5
dmtp/client_admin/.gitignore
vendored
Normal file
5
dmtp/client_admin/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
|
||||||
|
/generated/prisma
|
||||||
32
dmtp/client_admin/README.md
Normal file
32
dmtp/client_admin/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# client_admin
|
||||||
|
|
||||||
|
Small admin dashboard for creating tasks on the `TaskEscrow` contract.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Copy `.env.example` to `.env` and fill in values (PRIVATE_KEY, CONTRACT_ADDRESS if different).
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd client_admin
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the server:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Open dashboard: http://localhost:4000
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- GET `/` - dashboard UI
|
||||||
|
- POST `/api/create-task` - create task on blockchain. JSON body: `{ paymentAmount: "0.01", durationInDays: 7, workerAddress?: "0x..." }`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This server uses the ABI located at `../server/artifacts/contracts/TaskEscrow.sol/TaskEscrow.json`. Ensure the artifact exists (deploy the contract if necessary).
|
||||||
|
- The server signs transactions using `PRIVATE_KEY` in `.env`. Be careful with private keys.
|
||||||
953
dmtp/client_admin/package-lock.json
generated
Normal file
953
dmtp/client_admin/package-lock.json
generated
Normal file
@@ -0,0 +1,953 @@
|
|||||||
|
{
|
||||||
|
"name": "client_admin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "client_admin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"ethers": "^6.0.0",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@adraffy/ens-normalize": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||||
|
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aes-js": {
|
||||||
|
"version": "4.0.0-beta.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
|
||||||
|
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "1.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
|
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"content-type": "~1.0.5",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "1.2.0",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.4.24",
|
||||||
|
"on-finished": "2.4.1",
|
||||||
|
"qs": "6.13.0",
|
||||||
|
"raw-body": "2.5.2",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/destroy": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ethers": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/ethers-io/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@adraffy/ens-normalize": "1.10.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.2",
|
||||||
|
"@types/node": "22.7.5",
|
||||||
|
"aes-js": "4.0.0-beta.5",
|
||||||
|
"tslib": "2.7.0",
|
||||||
|
"ws": "8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "4.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.8",
|
||||||
|
"array-flatten": "1.1.1",
|
||||||
|
"body-parser": "1.20.3",
|
||||||
|
"content-disposition": "0.5.4",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"cookie": "0.7.1",
|
||||||
|
"cookie-signature": "1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"finalhandler": "1.3.1",
|
||||||
|
"fresh": "0.5.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"merge-descriptors": "1.0.3",
|
||||||
|
"methods": "~1.1.2",
|
||||||
|
"on-finished": "2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"path-to-regexp": "0.1.12",
|
||||||
|
"proxy-addr": "~2.0.7",
|
||||||
|
"qs": "6.13.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"send": "0.19.0",
|
||||||
|
"serve-static": "1.16.2",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"utils-merge": "1.0.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"on-finished": "2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "2.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||||
|
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.4.24",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "0.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
|
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "1.2.0",
|
||||||
|
"encodeurl": "~1.0.2",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"fresh": "0.5.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"on-finished": "2.4.1",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"statuses": "2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/encodeurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "1.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
|
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"send": "0.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/utils-merge": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
dmtp/client_admin/package.json
Normal file
14
dmtp/client_admin/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "client_admin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"ethers": "^6.0.0",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
449
dmtp/client_admin/src/public/index.html
Normal file
449
dmtp/client_admin/src/public/index.html
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Admin Dashboard - Create Task</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: #ff9500;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-shadow: 0 0 20px rgba(255, 149, 0, 0.3);
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 30px rgba(255, 149, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 149, 0, 0.15);
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ff9500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 2px solid rgba(255, 149, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
input::placeholder,
|
||||||
|
select::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
input:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #ff9500;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.2),
|
||||||
|
inset 0 0 10px rgba(255, 149, 0, 0.05);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.form-row .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #ff9500 0%, #ff7700 100%);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 149, 0, 0.3);
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(255, 149, 0, 0.5);
|
||||||
|
background: linear-gradient(135deg, #ffaa00 0%, #ff8800 100%);
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 149, 0, 0.1);
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #ff9500;
|
||||||
|
background: rgba(255, 149, 0, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.result.success {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
.result.error {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
.result strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #ff9500;
|
||||||
|
}
|
||||||
|
.result.success strong {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.result.error strong {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.result p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid #ff9500;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>✨ Task Admin</h1>
|
||||||
|
<p>Create tasks on blockchain</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="background: rgba(255, 149, 0, 0.05); border: 1px solid rgba(255, 149, 0, 0.2); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; font-size: 0.9rem;">
|
||||||
|
<div style="margin-bottom: 0.5rem;"><strong>Wallet Info</strong></div>
|
||||||
|
<div id="walletInfo" style="color: #ccc; font-family: monospace; line-height: 1.5;">Loading...</div>
|
||||||
|
<button id="approveBtn" style="margin-top: 0.75rem; padding: 0.5rem; background: rgba(255, 149, 0, 0.2); color: #ff9500; font-size: 0.85rem;">Approve cUSD</button>
|
||||||
|
</div>
|
||||||
|
<label for="taskName">Task Name</label>
|
||||||
|
<input
|
||||||
|
id="taskName"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., Image Classification Task"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="taskType">Task Type</label>
|
||||||
|
<select id="taskType">
|
||||||
|
<option value="image-classification">Image Classification</option>
|
||||||
|
<option value="text-labeling">Text Labeling</option>
|
||||||
|
<option value="data-entry">Data Entry</option>
|
||||||
|
<option value="content-moderation">Content Moderation</option>
|
||||||
|
<option value="transcription">Transcription</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Task Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="Describe what needs to be done..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="paymentAmount">Payment (cUSD)</label>
|
||||||
|
<input
|
||||||
|
id="paymentAmount"
|
||||||
|
type="number"
|
||||||
|
placeholder="0.01"
|
||||||
|
value="0.01"
|
||||||
|
step="0.001"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="duration">Duration (days)</label>
|
||||||
|
<input
|
||||||
|
id="duration"
|
||||||
|
type="number"
|
||||||
|
placeholder="7"
|
||||||
|
value="7"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="worker">Worker Address (optional)</label>
|
||||||
|
<input id="worker" type="text" placeholder="0x..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="submit">Create Task on Blockchain</button>
|
||||||
|
|
||||||
|
<div id="result" class="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load wallet info on page load
|
||||||
|
async function loadWalletInfo() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/wallet-info");
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.error("Wallet info response status:", resp.status);
|
||||||
|
throw new Error(`HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.error) {
|
||||||
|
console.error("Wallet info error:", data.error);
|
||||||
|
document.getElementById("walletInfo").textContent = "Error: " + data.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const info = document.getElementById("walletInfo");
|
||||||
|
info.innerHTML = `
|
||||||
|
<div>Address: ${data.walletAddress}</div>
|
||||||
|
<div>CELO: ${parseFloat(data.celoBalance).toFixed(4)}</div>
|
||||||
|
<div>cUSD: ${parseFloat(data.cusdBalance).toFixed(2)}</div>
|
||||||
|
<div>Allowance: ${parseFloat(data.contractAllowance).toFixed(2)}</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("loadWalletInfo error:", err);
|
||||||
|
document.getElementById("walletInfo").textContent = "Error loading wallet info: " + err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve button
|
||||||
|
document.getElementById("approveBtn").addEventListener("click", async () => {
|
||||||
|
const btn = document.getElementById("approveBtn");
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Approving...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/approve-cusd", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ amount: "1000000" }),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.error) {
|
||||||
|
alert("Approval failed: " + data.error);
|
||||||
|
} else if (data.success && data.txHash) {
|
||||||
|
alert("Approved! Tx: " + data.txHash.substring(0, 20) + "...");
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
await loadWalletInfo();
|
||||||
|
} else {
|
||||||
|
alert("Approval response: " + JSON.stringify(data));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error: " + err.message);
|
||||||
|
console.error("Approve error:", err);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = "Approve cUSD";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = document.getElementById("submit");
|
||||||
|
const result = document.getElementById("result");
|
||||||
|
|
||||||
|
function showResult(message, isSuccess = true, details = null) {
|
||||||
|
result.style.display = "block";
|
||||||
|
result.className = isSuccess ? "result success" : "result error";
|
||||||
|
result.innerHTML = `<strong>${
|
||||||
|
isSuccess ? "✓ Success" : "✗ Error"
|
||||||
|
}</strong><p>${message}</p>`;
|
||||||
|
if (details) {
|
||||||
|
result.innerHTML += `<p style="margin-top: 0.5rem; opacity: 0.8;">${details}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit.addEventListener("click", async () => {
|
||||||
|
const taskName = document.getElementById("taskName").value;
|
||||||
|
const taskType = document.getElementById("taskType").value;
|
||||||
|
const description = document.getElementById("description").value;
|
||||||
|
const paymentAmount = document.getElementById("paymentAmount").value;
|
||||||
|
const durationInDays = document.getElementById("duration").value;
|
||||||
|
const worker = document.getElementById("worker").value;
|
||||||
|
|
||||||
|
if (!taskName || !paymentAmount || !durationInDays) {
|
||||||
|
showResult(
|
||||||
|
"Please fill in task name, payment, and duration",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.style.display = "block";
|
||||||
|
result.className = "result";
|
||||||
|
result.innerHTML =
|
||||||
|
'<span class="loading"></span> Sending transaction...';
|
||||||
|
submit.disabled = true;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
taskName,
|
||||||
|
taskType,
|
||||||
|
description,
|
||||||
|
paymentAmount: parseFloat(paymentAmount),
|
||||||
|
durationInDays: parseInt(durationInDays),
|
||||||
|
workerAddress: worker || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/create-task", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.error) {
|
||||||
|
showResult(json.error, false, json.details);
|
||||||
|
} else {
|
||||||
|
const txInfo = json.txHash ? `Tx: ${json.txHash.substring(0, 20)}...<br/>` : "No tx hash";
|
||||||
|
showResult(
|
||||||
|
`Task created on blockchain!`,
|
||||||
|
true,
|
||||||
|
`${txInfo}TaskId: ${json.taskId || "unknown"}<br/>Syncing to database...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now sync to backend database with complete metadata
|
||||||
|
try {
|
||||||
|
const syncResp = await fetch("/api/sync-task", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
taskId: json.taskId,
|
||||||
|
txHash: json.txHash,
|
||||||
|
metadata: {
|
||||||
|
name: taskName,
|
||||||
|
type: taskType,
|
||||||
|
description,
|
||||||
|
paymentAmount: parseFloat(paymentAmount),
|
||||||
|
durationInDays: parseInt(durationInDays),
|
||||||
|
maxSubmissions: 10,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const syncJson = await syncResp.json();
|
||||||
|
console.log("Sync response:", syncJson);
|
||||||
|
|
||||||
|
if (syncResp.ok && syncJson.success) {
|
||||||
|
showResult(
|
||||||
|
`Task created and synced!`,
|
||||||
|
true,
|
||||||
|
`BlockchainId: ${json.taskId}<br/>DB ID: ${syncJson.data?.task?.id || "synced"}<br/>Status: Live in database`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const errorMsg = syncJson.error?.message || syncJson.error || "Unknown error";
|
||||||
|
showResult(
|
||||||
|
`Task created on blockchain but sync failed`,
|
||||||
|
false,
|
||||||
|
`BlockchainId: ${json.taskId}<br/>Error: ${errorMsg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (syncErr) {
|
||||||
|
showResult(
|
||||||
|
`Task created on blockchain but sync failed`,
|
||||||
|
false,
|
||||||
|
`BlockchainId: ${json.taskId}<br/>Error: ${syncErr.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
document.getElementById("taskName").value = "";
|
||||||
|
document.getElementById("description").value = "";
|
||||||
|
document.getElementById("paymentAmount").value = "0.01";
|
||||||
|
document.getElementById("duration").value = "7";
|
||||||
|
document.getElementById("worker").value = "";
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
await loadWalletInfo();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showResult("Request failed: " + err.message, false);
|
||||||
|
} finally {
|
||||||
|
submit.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load wallet info when page loads
|
||||||
|
loadWalletInfo();
|
||||||
|
// Refresh every 30 seconds
|
||||||
|
setInterval(loadWalletInfo, 30000);
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
354
dmtp/client_admin/src/server.js
Normal file
354
dmtp/client_admin/src/server.js
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import express from "express";
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(express.static(join(process.cwd(), "src", "public")));
|
||||||
|
|
||||||
|
const RPC_URL =
|
||||||
|
process.env.CELO_RPC_URL || "https://forno.celo-sepolia.celo-testnet.org";
|
||||||
|
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
|
||||||
|
const PRIVATE_KEY = process.env.PRIVATE_KEY;
|
||||||
|
|
||||||
|
if (!CONTRACT_ADDRESS) {
|
||||||
|
console.error("CONTRACT_ADDRESS not set in .env");
|
||||||
|
}
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
console.error("PRIVATE_KEY not set in .env");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load ABI from artifacts folder (reuse server artifact)
|
||||||
|
const artifactPath = join(
|
||||||
|
process.cwd(),
|
||||||
|
"..",
|
||||||
|
"server",
|
||||||
|
"artifacts",
|
||||||
|
"contracts",
|
||||||
|
"TaskEscrow.sol",
|
||||||
|
"TaskEscrow.json"
|
||||||
|
);
|
||||||
|
let TaskEscrowABI;
|
||||||
|
try {
|
||||||
|
TaskEscrowABI = JSON.parse(readFileSync(artifactPath, "utf8")).abi;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"Failed to load TaskEscrow ABI from artifacts. Make sure path exists:",
|
||||||
|
artifactPath
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(RPC_URL);
|
||||||
|
const wallet = new ethers.Wallet(PRIVATE_KEY || ethers.ZeroHash, provider);
|
||||||
|
const contract = new ethers.Contract(CONTRACT_ADDRESS, TaskEscrowABI, wallet);
|
||||||
|
|
||||||
|
// Simple health endpoint
|
||||||
|
app.get("/api/health", (req, res) => {
|
||||||
|
res.json({ status: "ok", contract: CONTRACT_ADDRESS });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check wallet balance and allowance
|
||||||
|
app.get("/api/wallet-info", async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log("Fetching wallet info...");
|
||||||
|
const balance = await provider.getBalance(wallet.address);
|
||||||
|
console.log("CELO balance:", ethers.formatEther(balance));
|
||||||
|
|
||||||
|
// Get cUSD contract
|
||||||
|
const cusdAddress = process.env.CUSD_SEPOLIA_ADDRESS;
|
||||||
|
const cusdABI = [
|
||||||
|
"function balanceOf(address) view returns (uint256)",
|
||||||
|
"function allowance(address owner, address spender) view returns (uint256)",
|
||||||
|
];
|
||||||
|
|
||||||
|
let cusdBalance = BigInt(0);
|
||||||
|
let allowance = BigInt(0);
|
||||||
|
|
||||||
|
if (cusdAddress) {
|
||||||
|
try {
|
||||||
|
console.log("Fetching cUSD info from:", cusdAddress);
|
||||||
|
const cusd = new ethers.Contract(cusdAddress, cusdABI, provider);
|
||||||
|
cusdBalance = await cusd.balanceOf(wallet.address);
|
||||||
|
allowance = await cusd.allowance(wallet.address, CONTRACT_ADDRESS);
|
||||||
|
console.log("cUSD balance:", ethers.formatEther(cusdBalance));
|
||||||
|
console.log("Allowance:", ethers.formatEther(allowance));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching cUSD info:", err.message);
|
||||||
|
// Continue with zero values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
walletAddress: wallet.address,
|
||||||
|
celoBalance: ethers.formatEther(balance),
|
||||||
|
cusdBalance: ethers.formatEther(cusdBalance),
|
||||||
|
contractAllowance: ethers.formatEther(allowance),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Sending wallet info:", response);
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Wallet info error:", error.message);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Approve cUSD spending
|
||||||
|
app.post("/api/approve-cusd", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { amount } = req.body;
|
||||||
|
const approveAmount = ethers.parseEther(String(amount || "1000000")); // Default to 1M cUSD
|
||||||
|
|
||||||
|
const cusdAddress = process.env.CUSD_SEPOLIA_ADDRESS;
|
||||||
|
if (!cusdAddress) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "CUSD_SEPOLIA_ADDRESS not configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cusdABI = [
|
||||||
|
"function approve(address spender, uint256 amount) returns (bool)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const cusd = new ethers.Contract(cusdAddress, cusdABI, wallet);
|
||||||
|
console.log(
|
||||||
|
`Approving ${ethers.formatEther(
|
||||||
|
approveAmount
|
||||||
|
)} cUSD for contract ${CONTRACT_ADDRESS}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const tx = await cusd.approve(CONTRACT_ADDRESS, approveAmount);
|
||||||
|
console.log("Approve tx sent:", tx.hash);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
txHash: receipt.transactionHash,
|
||||||
|
approvedAmount: ethers.formatEther(approveAmount),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Approval error:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: error.message,
|
||||||
|
details: error.reason || error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create task endpoint
|
||||||
|
app.post("/api/create-task", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
taskName,
|
||||||
|
taskType,
|
||||||
|
description,
|
||||||
|
paymentAmount,
|
||||||
|
durationInDays,
|
||||||
|
workerAddress,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!paymentAmount || !durationInDays) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "paymentAmount and durationInDays are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log task metadata (in production, you'd store this in DB)
|
||||||
|
console.log(`Creating task:`, {
|
||||||
|
name: taskName || "unnamed",
|
||||||
|
type: taskType || "unknown",
|
||||||
|
description: description || "no description",
|
||||||
|
payment: paymentAmount,
|
||||||
|
duration: durationInDays,
|
||||||
|
worker: workerAddress || "unassigned",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert paymentAmount (cUSD) to wei - must be a string
|
||||||
|
let amountWei;
|
||||||
|
try {
|
||||||
|
amountWei = ethers.parseEther(String(paymentAmount));
|
||||||
|
console.log(`Amount in wei: ${amountWei.toString()}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to parse amount:", err.message);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Invalid payment amount",
|
||||||
|
details: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`About to call createTask on contract ${CONTRACT_ADDRESS}`);
|
||||||
|
|
||||||
|
// Check wallet balance first
|
||||||
|
const balance = await provider.getBalance(wallet.address);
|
||||||
|
console.log("Wallet CELO balance:", ethers.formatEther(balance));
|
||||||
|
|
||||||
|
if (balance < ethers.parseEther("0.001")) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Insufficient CELO balance for gas",
|
||||||
|
details: `Balance: ${ethers.formatEther(
|
||||||
|
balance
|
||||||
|
)} CELO. Need at least 0.001 CELO`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await contract.createTask(amountWei, Number(durationInDays));
|
||||||
|
console.log("Transaction sent:", tx.hash);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
if (!receipt) {
|
||||||
|
console.error("Transaction receipt is null/undefined!");
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Transaction failed - no receipt returned",
|
||||||
|
details:
|
||||||
|
"The transaction may have failed. Check if you have sufficient cUSD allowance.",
|
||||||
|
txHash: tx.hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Transaction confirmed:", {
|
||||||
|
hash: receipt.hash,
|
||||||
|
transactionHash: receipt.transactionHash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
status: receipt.status,
|
||||||
|
logsCount: receipt.logs.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to parse event TaskCreated
|
||||||
|
let taskId = null;
|
||||||
|
console.log("Parsing logs...");
|
||||||
|
for (const log of receipt.logs) {
|
||||||
|
try {
|
||||||
|
const parsed = contract.interface.parseLog(log);
|
||||||
|
console.log("Parsed log:", parsed.name);
|
||||||
|
if (parsed.name === "TaskCreated") {
|
||||||
|
taskId = parsed.args[0].toString();
|
||||||
|
console.log("Found TaskCreated event with taskId:", taskId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not parse log:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalTxHash = receipt.hash || receipt.transactionHash || tx.hash;
|
||||||
|
console.log("Final txHash:", finalTxHash);
|
||||||
|
console.log("Task creation complete. TaskId:", taskId);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
txHash: finalTxHash,
|
||||||
|
taskId,
|
||||||
|
metadata: {
|
||||||
|
name: taskName,
|
||||||
|
type: taskType,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Create task error:", error);
|
||||||
|
|
||||||
|
// Provide helpful error messages
|
||||||
|
let errorMsg = String(error);
|
||||||
|
let details = error.reason || error.message || "";
|
||||||
|
|
||||||
|
if (error.code === "INSUFFICIENT_FUNDS") {
|
||||||
|
errorMsg = "Insufficient balance to create task";
|
||||||
|
details =
|
||||||
|
"The wallet doesn't have enough CELO to pay for the transaction";
|
||||||
|
} else if (error.code === "CALL_EXCEPTION") {
|
||||||
|
errorMsg = "Contract call failed";
|
||||||
|
details = error.reason || "Check that the contract address is correct";
|
||||||
|
} else if (errorMsg.includes("Insufficient allowance")) {
|
||||||
|
errorMsg = "Insufficient cUSD allowance";
|
||||||
|
details = "Please approve the contract to spend cUSD first";
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
error: errorMsg,
|
||||||
|
details: details,
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync task to database (backend server)
|
||||||
|
app.post("/api/sync-task", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { taskId, txHash, metadata } = req.body;
|
||||||
|
|
||||||
|
if (!taskId || !txHash) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "taskId and txHash are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||||
|
console.log(`Syncing task ${taskId} to backend: ${backendUrl}`);
|
||||||
|
|
||||||
|
const syncPayload = {
|
||||||
|
contractTaskId: parseInt(taskId),
|
||||||
|
transactionHash: txHash,
|
||||||
|
paymentAmount: parseFloat(metadata?.paymentAmount || "0"),
|
||||||
|
taskName: metadata?.name || "Unnamed Task",
|
||||||
|
taskType: metadata?.type || "text-labeling",
|
||||||
|
description: metadata?.description || "Task created via admin dashboard",
|
||||||
|
maxSubmissions: metadata?.maxSubmissions || 10,
|
||||||
|
durationInDays: metadata?.durationInDays || 7,
|
||||||
|
verificationCriteria: {
|
||||||
|
transactionHash: txHash,
|
||||||
|
blockchainCreated: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Sync payload:", syncPayload);
|
||||||
|
|
||||||
|
const syncResponse = await fetch(`${backendUrl}/api/v1/tasks/sync`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(syncPayload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncData = await syncResponse.text();
|
||||||
|
console.log(`Sync response status: ${syncResponse.status}`);
|
||||||
|
console.log("Sync response body:", syncData);
|
||||||
|
|
||||||
|
if (!syncResponse.ok) {
|
||||||
|
console.error("Sync error response:", syncData);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Failed to sync task to backend",
|
||||||
|
details: syncData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncedTask = JSON.parse(syncData);
|
||||||
|
console.log("Task synced successfully:", syncedTask);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Task synced to backend database",
|
||||||
|
data: syncedTask,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Sync task error:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: error.message,
|
||||||
|
details: "Failed to sync task to backend",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve dashboard page
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.sendFile(join(process.cwd(), "src", "public", "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 4000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Admin dashboard listening on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
18
dmtp/server/.gitignore
vendored
Normal file
18
dmtp/server/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
|
||||||
|
/generated/prisma
|
||||||
|
dist/
|
||||||
|
# Hardhat
|
||||||
|
artifacts/
|
||||||
|
cache/
|
||||||
|
typechain-types/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Hardhat network files
|
||||||
|
ignition/deployments/
|
||||||
|
.openzeppelin/
|
||||||
198
dmtp/server/TROUBLESHOOTING.md
Normal file
198
dmtp/server/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Blockchain Integration - Troubleshooting Guide
|
||||||
|
|
||||||
|
## Summary of Fixes
|
||||||
|
|
||||||
|
### 1. Token Deployment Issue ✅
|
||||||
|
|
||||||
|
**Problem**: Invalid BytesLike value error when deploying test token
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- Used compiled `MockERC20.sol` artifact instead of hardcoded bytecode
|
||||||
|
- Added `resolveJsonModule: true` to `tsconfig.json`
|
||||||
|
- Deployed test cUSD token: `0x845D9D0B4Be004Dcbc17b11160B0C18abBD5FEBD`
|
||||||
|
|
||||||
|
### 2. Contract Mismatch Issue ✅
|
||||||
|
|
||||||
|
**Problem**: TaskEscrow contract was deployed with wrong cUSD address
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- Created diagnostic script (`check-contract-config.ts`)
|
||||||
|
- Redeployed TaskEscrow with correct cUSD token
|
||||||
|
- New TaskEscrow address: `0xa520d207c91C0FE0e9cFe8D63AbE02fd18B2254e`
|
||||||
|
|
||||||
|
### 3. Approval Issue ✅
|
||||||
|
|
||||||
|
**Problem**: Contract couldn't transfer cUSD (no allowance)
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- Created `approve-cusd.ts` script
|
||||||
|
- Added `checkAllowance()` and `approveCUSD()` methods to blockchain service
|
||||||
|
- Approved contract to spend cUSD tokens
|
||||||
|
|
||||||
|
### 4. AI Verification JSON Parsing ✅
|
||||||
|
|
||||||
|
**Problem**: Gemini returning incomplete/malformed JSON responses
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- Added `responseMimeType: "application/json"` to Gemini API calls
|
||||||
|
- Improved JSON extraction with regex fallback
|
||||||
|
- Added better error handling and logging
|
||||||
|
- Implemented fallback parsing for malformed responses
|
||||||
|
|
||||||
|
### 5. Task Not Found Error ⚠️
|
||||||
|
|
||||||
|
**Problem**: "Task does not exist" error when approving submissions
|
||||||
|
**Root Cause**: Multiple possible causes:
|
||||||
|
|
||||||
|
1. Different project directories (`D:\new-celo` vs current workspace)
|
||||||
|
2. Old tasks pointing to old contract addresses
|
||||||
|
3. Database/blockchain sync issues
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
- Added `getTask()` method to verify task existence
|
||||||
|
- Added `getTaskCounter()` to check blockchain state
|
||||||
|
- Created diagnostic scripts:
|
||||||
|
- `diagnose-task-mismatch.ts` - Check DB vs blockchain
|
||||||
|
- `cleanup-old-tasks.ts` - Mark invalid tasks as expired
|
||||||
|
- `show-env-info.ts` - Show complete environment info
|
||||||
|
|
||||||
|
## Deployed Contracts
|
||||||
|
|
||||||
|
```
|
||||||
|
cUSD Token: 0x845D9D0B4Be004Dcbc17b11160B0C18abBD5FEBD
|
||||||
|
TaskEscrow: 0xa520d207c91C0FE0e9cFe8D63AbE02fd18B2254e
|
||||||
|
Network: Celo Sepolia Testnet
|
||||||
|
Chain ID: 11142220
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Scripts
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy test cUSD token
|
||||||
|
npx tsx deploy-test-token.ts
|
||||||
|
|
||||||
|
# Deploy TaskEscrow contract
|
||||||
|
npx tsx redeploy-task-escrow.ts
|
||||||
|
|
||||||
|
# Approve cUSD spending
|
||||||
|
npx tsx approve-cusd.ts
|
||||||
|
|
||||||
|
# Create test task
|
||||||
|
npx tsx create-task-with-blockchain.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check contract configuration
|
||||||
|
npx tsx check-contract-config.ts
|
||||||
|
|
||||||
|
# Diagnose task mismatch
|
||||||
|
npx tsx diagnose-task-mismatch.ts
|
||||||
|
|
||||||
|
# Cleanup old/invalid tasks
|
||||||
|
npx tsx cleanup-old-tasks.ts
|
||||||
|
|
||||||
|
# Show environment info
|
||||||
|
npx tsx show-env-info.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Gemini JSON response
|
||||||
|
npx tsx test-gemini-json.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: "Task does not exist" on blockchain
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
|
||||||
|
1. Run `npx tsx show-env-info.ts` to verify contract addresses
|
||||||
|
2. Run `npx tsx diagnose-task-mismatch.ts` to check tasks
|
||||||
|
3. Ensure you're in the correct project directory
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
|
||||||
|
- If using wrong contract: Update `CONTRACT_ADDRESS` in `.env`
|
||||||
|
- If tasks are old: Run `npx tsx cleanup-old-tasks.ts`
|
||||||
|
- If starting fresh: Create new task with `npx tsx create-task-with-blockchain.ts`
|
||||||
|
|
||||||
|
### Issue: "Insufficient allowance"
|
||||||
|
|
||||||
|
**Check**: Run `npx tsx check-contract-config.ts`
|
||||||
|
|
||||||
|
**Fix**: Run `npx tsx approve-cusd.ts`
|
||||||
|
|
||||||
|
### Issue: Gemini JSON parsing errors
|
||||||
|
|
||||||
|
**Check**: Look for "Invalid JSON response from Gemini" in logs
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
|
||||||
|
- Already implemented: JSON mode in API calls
|
||||||
|
- Fallback parsing with regex
|
||||||
|
- Logs now show raw response for debugging
|
||||||
|
|
||||||
|
### Issue: Different project directories
|
||||||
|
|
||||||
|
**Check**: `process.cwd()` and paths in error messages
|
||||||
|
|
||||||
|
**Fix**: Ensure you're running commands from the correct directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd C:\Users\RAJ\OneDrive\Desktop\micro-job-ai-agent-web3\server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables Required
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=<your_prisma_accelerate_url>
|
||||||
|
|
||||||
|
# Blockchain
|
||||||
|
PRIVATE_KEY=<your_private_key>
|
||||||
|
CELO_RPC_URL=https://forno.celo-sepolia.celo-testnet.org
|
||||||
|
CHAIN_ID=11142220
|
||||||
|
CONTRACT_ADDRESS=0xa520d207c91C0FE0e9cFe8D63AbE02fd18B2254e
|
||||||
|
CUSD_SEPOLIA_ADDRESS=0x845D9D0B4Be004Dcbc17b11160B0C18abBD5FEBD
|
||||||
|
|
||||||
|
# AI
|
||||||
|
GEMINI_API_KEY=<your_gemini_api_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Verify environment: `npx tsx show-env-info.ts`
|
||||||
|
2. ✅ Check task status: `npx tsx diagnose-task-mismatch.ts`
|
||||||
|
3. ✅ Test AI verification: `npx tsx test-gemini-json.ts`
|
||||||
|
4. Create new task if needed: `npx tsx create-task-with-blockchain.ts`
|
||||||
|
5. Test full workflow: Submit task → AI verifies → Payment released
|
||||||
|
|
||||||
|
## Architecture Improvements Made
|
||||||
|
|
||||||
|
### Blockchain Service
|
||||||
|
|
||||||
|
- ✅ Added task verification before operations
|
||||||
|
- ✅ Added detailed logging
|
||||||
|
- ✅ Added helper methods: `getTask()`, `getTaskCounter()`, `getContractAddress()`
|
||||||
|
- ✅ Better error messages with context
|
||||||
|
|
||||||
|
### AI Verification Service
|
||||||
|
|
||||||
|
- ✅ JSON mode for Gemini API
|
||||||
|
- ✅ Fallback parsing for malformed responses
|
||||||
|
- ✅ Better error logging
|
||||||
|
- ✅ Response validation
|
||||||
|
|
||||||
|
### Tooling
|
||||||
|
|
||||||
|
- ✅ Comprehensive diagnostic scripts
|
||||||
|
- ✅ Automated cleanup tools
|
||||||
|
- ✅ Environment validation
|
||||||
|
- ✅ Easy testing scripts
|
||||||
91
dmtp/server/approve-cusd.ts
Normal file
91
dmtp/server/approve-cusd.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve TaskEscrow contract to spend cUSD tokens
|
||||||
|
* This needs to be done once before creating tasks
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function approveCUSD() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 Approving TaskEscrow to spend cUSD...\n');
|
||||||
|
|
||||||
|
// Initialize provider and signer
|
||||||
|
const provider = new ethers.JsonRpcProvider(
|
||||||
|
process.env.CELO_RPC_URL || 'https://forno.celo-sepolia.celo-testnet.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = process.env.PRIVATE_KEY;
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error('PRIVATE_KEY not configured in .env');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(privateKey, provider);
|
||||||
|
console.log(`📝 Approving from: ${signer.address}`);
|
||||||
|
|
||||||
|
// Get contract addresses
|
||||||
|
const cUSDAddress = process.env.CUSD_SEPOLIA_ADDRESS;
|
||||||
|
const taskEscrowAddress = process.env.CONTRACT_ADDRESS;
|
||||||
|
|
||||||
|
if (!cUSDAddress) {
|
||||||
|
throw new Error('CUSD_SEPOLIA_ADDRESS not configured in .env');
|
||||||
|
}
|
||||||
|
if (!taskEscrowAddress) {
|
||||||
|
throw new Error('CONTRACT_ADDRESS not configured in .env');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`💰 cUSD Token: ${cUSDAddress}`);
|
||||||
|
console.log(`📋 TaskEscrow Contract: ${taskEscrowAddress}\n`);
|
||||||
|
|
||||||
|
// Create cUSD contract instance
|
||||||
|
const cUSDContract = new ethers.Contract(
|
||||||
|
cUSDAddress,
|
||||||
|
[
|
||||||
|
'function approve(address spender, uint256 amount) returns (bool)',
|
||||||
|
'function allowance(address owner, address spender) view returns (uint256)',
|
||||||
|
'function balanceOf(address account) view returns (uint256)',
|
||||||
|
],
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check current balance
|
||||||
|
const balance = await cUSDContract.balanceOf(signer.address);
|
||||||
|
console.log(`💵 Your cUSD Balance: ${ethers.formatEther(balance)} cUSD`);
|
||||||
|
|
||||||
|
// Check current allowance
|
||||||
|
const currentAllowance = await cUSDContract.allowance(signer.address, taskEscrowAddress);
|
||||||
|
console.log(`🔓 Current Allowance: ${ethers.formatEther(currentAllowance)} cUSD\n`);
|
||||||
|
|
||||||
|
// Approve a large amount (10 million cUSD) so we don't need to approve again
|
||||||
|
const approvalAmount = ethers.parseEther('10000000');
|
||||||
|
console.log(`⏳ Approving ${ethers.formatEther(approvalAmount)} cUSD...`);
|
||||||
|
|
||||||
|
const tx = await cUSDContract.approve(taskEscrowAddress, approvalAmount);
|
||||||
|
console.log(`📍 Transaction sent: ${tx.hash}`);
|
||||||
|
console.log('⏳ Waiting for confirmation...\n');
|
||||||
|
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
console.log(`✅ Approval confirmed in block ${receipt.blockNumber}\n`);
|
||||||
|
|
||||||
|
// Verify new allowance
|
||||||
|
const newAllowance = await cUSDContract.allowance(signer.address, taskEscrowAddress);
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('✅ SUCCESS! TaskEscrow approved to spend cUSD');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
console.log(`New Allowance: ${ethers.formatEther(newAllowance)} cUSD`);
|
||||||
|
console.log(`Transaction Hash: ${receipt.hash}\n`);
|
||||||
|
|
||||||
|
console.log('🎯 Next Steps:');
|
||||||
|
console.log('You can now create tasks with blockchain integration! 🎉\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('\n❌ Approval failed:', error.message);
|
||||||
|
if (error.data) {
|
||||||
|
console.error('Error data:', error.data);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approveCUSD();
|
||||||
56
dmtp/server/check-contract-config.ts
Normal file
56
dmtp/server/check-contract-config.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
async function checkContractConfig() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Checking TaskEscrow Contract Configuration...\n');
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(
|
||||||
|
process.env.CELO_RPC_URL || 'https://forno.celo-sepolia.celo-testnet.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractAddress = process.env.CONTRACT_ADDRESS;
|
||||||
|
if (!contractAddress) {
|
||||||
|
throw new Error('CONTRACT_ADDRESS not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskEscrowABI = JSON.parse(
|
||||||
|
readFileSync(
|
||||||
|
join(__dirname, './artifacts/contracts/TaskEscrow.sol/TaskEscrow.json'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const contract = new ethers.Contract(
|
||||||
|
contractAddress,
|
||||||
|
TaskEscrowABI.abi,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`📋 TaskEscrow Contract: ${contractAddress}`);
|
||||||
|
|
||||||
|
const cUSDAddress = await contract.cUSD();
|
||||||
|
console.log(`💰 Configured cUSD Token: ${cUSDAddress}`);
|
||||||
|
console.log(`💰 Expected cUSD Token: ${process.env.CUSD_SEPOLIA_ADDRESS}\n`);
|
||||||
|
|
||||||
|
if (cUSDAddress.toLowerCase() !== process.env.CUSD_SEPOLIA_ADDRESS?.toLowerCase()) {
|
||||||
|
console.log('❌ MISMATCH DETECTED!');
|
||||||
|
console.log('\nThe TaskEscrow contract is configured with a different cUSD token address.');
|
||||||
|
console.log('\n🔧 Solutions:');
|
||||||
|
console.log('1. Redeploy TaskEscrow contract with the new cUSD address');
|
||||||
|
console.log('2. Update CUSD_SEPOLIA_ADDRESS in .env to match the contract\'s cUSD address');
|
||||||
|
console.log(` CUSD_SEPOLIA_ADDRESS=${cUSDAddress}\n`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ cUSD addresses match!');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkContractConfig();
|
||||||
12
dmtp/server/check-db.sh
Normal file
12
dmtp/server/check-db.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Check if verification worker is processing jobs
|
||||||
|
|
||||||
|
cd c:\Users\RAJ\OneDrive\Desktop\micro-job-ai-agent-web3\server
|
||||||
|
|
||||||
|
echo "📋 Checking for payments with pending status..."
|
||||||
|
echo ""
|
||||||
|
echo "SELECT id, task_id, worker_id, amount, transaction_hash, status, created_at FROM payments ORDER BY created_at DESC LIMIT 10;" | psql "$DATABASE_URL" 2>/dev/null || echo "⚠️ Could not query database directly"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📝 Checking submissions with pending verification status..."
|
||||||
|
echo "SELECT id, task_id, worker_id, verification_status, created_at FROM submissions ORDER BY created_at DESC LIMIT 10;" | psql "$DATABASE_URL" 2>/dev/null || echo "⚠️ Could not query database directly"
|
||||||
66
dmtp/server/check-stalled-job.ts
Normal file
66
dmtp/server/check-stalled-job.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
import { verificationQueue } from './src/queues/verification.queue';
|
||||||
|
|
||||||
|
async function checkStalledJob() {
|
||||||
|
console.log('🔍 Checking for stalled jobs...\n');
|
||||||
|
|
||||||
|
const submissionId = '337c16f7-081e-4b3c-8aee-4d9ffa0e3682';
|
||||||
|
|
||||||
|
// Check submission status
|
||||||
|
const submission = await prisma.submission.findUnique({
|
||||||
|
where: { id: submissionId },
|
||||||
|
include: {
|
||||||
|
task: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
console.log('❌ Submission not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Submission: ${submission.id}`);
|
||||||
|
console.log(` Status: ${submission.verificationStatus}`);
|
||||||
|
console.log(` Task: ${submission.task.title}`);
|
||||||
|
console.log(` Contract Task ID: ${submission.task.contractTaskId}`);
|
||||||
|
|
||||||
|
// Check queue jobs
|
||||||
|
const jobs = await verificationQueue.getJobs(['active', 'waiting', 'delayed', 'failed', 'completed']);
|
||||||
|
const jobForSubmission = jobs.find(j => j.data.submissionId === submissionId);
|
||||||
|
|
||||||
|
if (jobForSubmission) {
|
||||||
|
console.log(`\n📊 Job found: ${jobForSubmission.id}`);
|
||||||
|
console.log(` State: ${await jobForSubmission.getState()}`);
|
||||||
|
console.log(` Attempts: ${jobForSubmission.attemptsMade}`);
|
||||||
|
console.log(` Data:`, jobForSubmission.data);
|
||||||
|
|
||||||
|
// Get job state
|
||||||
|
const state = await jobForSubmission.getState();
|
||||||
|
|
||||||
|
if (state === 'failed') {
|
||||||
|
console.log(`\n❌ Job failed. Error:`, jobForSubmission.failedReason);
|
||||||
|
console.log('\n🔄 Retrying job...');
|
||||||
|
await jobForSubmission.retry();
|
||||||
|
console.log('✅ Job retried');
|
||||||
|
} else if (state === 'completed') {
|
||||||
|
console.log('\n✅ Job completed');
|
||||||
|
console.log('Result:', jobForSubmission.returnvalue);
|
||||||
|
} else {
|
||||||
|
console.log(`\n⚠️ Job in state: ${state}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ No job found in queue for this submission');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue stats
|
||||||
|
const stats = await verificationQueue.getJobCounts();
|
||||||
|
console.log('\n📊 Queue Stats:', stats);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStalledJob().catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
43
dmtp/server/check-tasks-without-contract.ts
Normal file
43
dmtp/server/check-tasks-without-contract.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
|
||||||
|
async function checkTasksWithoutContract() {
|
||||||
|
console.log('🔍 Checking tasks without blockchain contract...\n');
|
||||||
|
|
||||||
|
const tasksWithoutContract = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
contractTaskId: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
requester: {
|
||||||
|
select: {
|
||||||
|
walletAddress: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${tasksWithoutContract.length} tasks without blockchain contract:\n`);
|
||||||
|
|
||||||
|
for (const task of tasksWithoutContract) {
|
||||||
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||||
|
console.log(`📋 Task: ${task.title}`);
|
||||||
|
console.log(` ID: ${task.id}`);
|
||||||
|
console.log(` Requester: ${task.requester.walletAddress}`);
|
||||||
|
console.log(` Amount: ${task.paymentAmount} cUSD`);
|
||||||
|
console.log(` Status: ${task.status}`);
|
||||||
|
console.log(` Created: ${task.createdAt}`);
|
||||||
|
console.log(` Contract Task ID: ${task.contractTaskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTasksWithoutContract().catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
36
dmtp/server/check-wallet-balance.ts
Normal file
36
dmtp/server/check-wallet-balance.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
|
||||||
|
async function checkWalletBalance() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const walletAddress = '0xA0e793E7257c065b30c46Ef6828F2B3C0de87A8E';
|
||||||
|
|
||||||
|
console.log('💰 Checking cUSD balance...\n');
|
||||||
|
console.log(`Wallet: ${walletAddress}`);
|
||||||
|
|
||||||
|
const balance = await blockchainService.getCUSDBalance(walletAddress);
|
||||||
|
|
||||||
|
console.log(`\nBalance: ${balance} cUSD`);
|
||||||
|
|
||||||
|
if (parseFloat(balance) === 0) {
|
||||||
|
console.log('\n❌ No cUSD balance!');
|
||||||
|
console.log('\n📝 To get cUSD on Celo Sepolia:');
|
||||||
|
console.log('1. Get testnet CELO from: https://faucet.celo.org');
|
||||||
|
console.log('2. Swap CELO for cUSD on Uniswap or Mento');
|
||||||
|
console.log('3. Or use the Celo wallet to get test cUSD');
|
||||||
|
} else {
|
||||||
|
console.log('\n✅ Wallet has cUSD!');
|
||||||
|
console.log('\n📝 Next step:');
|
||||||
|
console.log('The wallet needs to APPROVE the TaskEscrow contract to spend cUSD.');
|
||||||
|
console.log('This is normally done through the frontend when creating a task.');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWalletBalance();
|
||||||
94
dmtp/server/cleanup-old-tasks.ts
Normal file
94
dmtp/server/cleanup-old-tasks.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
|
||||||
|
async function cleanupOldTasks() {
|
||||||
|
try {
|
||||||
|
console.log('🧹 Cleaning up tasks from old contracts...\n');
|
||||||
|
|
||||||
|
const currentContract = blockchainService.getContractAddress();
|
||||||
|
console.log(`📋 Current Contract: ${currentContract}\n`);
|
||||||
|
|
||||||
|
// Find all tasks with contractTaskId
|
||||||
|
const tasks = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
contractTaskId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
in: ['open', 'in_progress'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${tasks.length} active tasks with blockchain integration\n`);
|
||||||
|
|
||||||
|
let invalidTasks = 0;
|
||||||
|
let validTasks = 0;
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
try {
|
||||||
|
const blockchainTask = await blockchainService.getTask(task.contractTaskId!);
|
||||||
|
|
||||||
|
if (!blockchainTask) {
|
||||||
|
console.log(`❌ Task ${task.id} (Contract ID: ${task.contractTaskId}) - NOT FOUND on current contract`);
|
||||||
|
invalidTasks++;
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Task ${task.id} (Contract ID: ${task.contractTaskId}) - Valid`);
|
||||||
|
validTasks++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ Task ${task.id} (Contract ID: ${task.contractTaskId}) - Error checking`);
|
||||||
|
invalidTasks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log(`Summary:`);
|
||||||
|
console.log(` Valid tasks: ${validTasks}`);
|
||||||
|
console.log(` Invalid tasks: ${invalidTasks}`);
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
if (invalidTasks > 0) {
|
||||||
|
console.log('⚠️ Found tasks referencing old contract!\n');
|
||||||
|
console.log('Options:');
|
||||||
|
console.log('1. Mark invalid tasks as "cancelled" (recommended)');
|
||||||
|
console.log('2. Delete invalid tasks completely');
|
||||||
|
console.log('3. Do nothing (manual cleanup)\n');
|
||||||
|
|
||||||
|
// For now, let's mark them as cancelled
|
||||||
|
console.log('Marking invalid tasks as cancelled...\n');
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
try {
|
||||||
|
const blockchainTask = await blockchainService.getTask(task.contractTaskId!);
|
||||||
|
|
||||||
|
if (!blockchainTask) {
|
||||||
|
await prisma.task.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: { status: 'expired' as any },
|
||||||
|
});
|
||||||
|
console.log(`✅ Marked task ${task.id} as expired`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await prisma.task.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: { status: 'expired' as any },
|
||||||
|
});
|
||||||
|
console.log(`✅ Marked task ${task.id} as expired`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ Cleanup complete!');
|
||||||
|
} else {
|
||||||
|
console.log('✅ All tasks are valid - no cleanup needed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOldTasks();
|
||||||
48
dmtp/server/cleanup-test-data.ts
Normal file
48
dmtp/server/cleanup-test-data.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
|
||||||
|
async function cleanupTestData() {
|
||||||
|
console.log('🧹 Cleaning up test data...\n');
|
||||||
|
|
||||||
|
// Delete submissions for tasks without contracts
|
||||||
|
const deletedSubmissions = await prisma.submission.deleteMany({
|
||||||
|
where: {
|
||||||
|
task: {
|
||||||
|
contractTaskId: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Deleted ${deletedSubmissions.count} submissions for tasks without contracts`);
|
||||||
|
|
||||||
|
// Delete payments for tasks without contracts
|
||||||
|
const deletedPayments = await prisma.payment.deleteMany({
|
||||||
|
where: {
|
||||||
|
task: {
|
||||||
|
contractTaskId: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Deleted ${deletedPayments.count} payments for tasks without contracts`);
|
||||||
|
|
||||||
|
// Delete tasks without contracts
|
||||||
|
const deletedTasks = await prisma.task.deleteMany({
|
||||||
|
where: {
|
||||||
|
contractTaskId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Deleted ${deletedTasks.count} tasks without blockchain contracts`);
|
||||||
|
|
||||||
|
console.log('\n✅ Cleanup complete!');
|
||||||
|
console.log('\n📝 Next steps:');
|
||||||
|
console.log('1. Create a new task through the API with a REAL wallet address');
|
||||||
|
console.log('2. Make sure you have cUSD in your wallet');
|
||||||
|
console.log('3. Submit work to the task');
|
||||||
|
console.log('4. Watch the payment process automatically!');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupTestData().catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
12
dmtp/server/contracts/MockERC20.sol
Normal file
12
dmtp/server/contracts/MockERC20.sol
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
|
contract MockERC20 is ERC20 {
|
||||||
|
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
|
||||||
|
|
||||||
|
function mint(address to, uint256 amount) external {
|
||||||
|
_mint(to, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
288
dmtp/server/contracts/TaskEscrow.sol
Normal file
288
dmtp/server/contracts/TaskEscrow.sol
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/Pausable.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title TaskEscrow
|
||||||
|
* @dev Escrow smart contract for AI-powered micro-task marketplace on CELO Sepolia Testnet.
|
||||||
|
* @notice This contract securely holds cUSD payments for tasks, releasing to workers upon AI-verification approval.
|
||||||
|
*/
|
||||||
|
contract TaskEscrow is Ownable, ReentrancyGuard, Pausable {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
// ============ Constants & Parameters ============
|
||||||
|
IERC20 public immutable cUSD;
|
||||||
|
|
||||||
|
uint256 public constant PLATFORM_FEE_BPS = 500; // 5%
|
||||||
|
uint256 public constant BPS_DENOMINATOR = 10000;
|
||||||
|
|
||||||
|
uint256 public platformFeesAccumulated;
|
||||||
|
uint256 public taskCounter;
|
||||||
|
|
||||||
|
// ============ Enums ============
|
||||||
|
enum TaskStatus {
|
||||||
|
Open,
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Struct ============
|
||||||
|
struct Task {
|
||||||
|
uint256 taskId;
|
||||||
|
address requester;
|
||||||
|
address worker;
|
||||||
|
uint256 paymentAmount;
|
||||||
|
TaskStatus status;
|
||||||
|
uint256 createdAt;
|
||||||
|
uint256 expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Mappings ============
|
||||||
|
mapping(uint256 => Task) public tasks;
|
||||||
|
mapping(uint256 => bool) public taskExists;
|
||||||
|
|
||||||
|
// ============ Events ============
|
||||||
|
event TaskCreated(
|
||||||
|
uint256 indexed taskId,
|
||||||
|
address indexed requester,
|
||||||
|
uint256 payment,
|
||||||
|
uint256 expiresAt
|
||||||
|
);
|
||||||
|
event WorkerAssigned(uint256 indexed taskId, address indexed worker);
|
||||||
|
event PaymentReleased(
|
||||||
|
uint256 indexed taskId,
|
||||||
|
address indexed worker,
|
||||||
|
uint256 workerAmount,
|
||||||
|
uint256 platformFee
|
||||||
|
);
|
||||||
|
event TaskCancelled(
|
||||||
|
uint256 indexed taskId,
|
||||||
|
address indexed requester,
|
||||||
|
uint256 refunded
|
||||||
|
);
|
||||||
|
event TaskExpired(
|
||||||
|
uint256 indexed taskId,
|
||||||
|
address indexed requester,
|
||||||
|
uint256 refunded
|
||||||
|
);
|
||||||
|
event PlatformFeesWithdrawn(address indexed owner, uint256 amount);
|
||||||
|
event DebugLog(
|
||||||
|
string action,
|
||||||
|
address sender,
|
||||||
|
uint256 taskId,
|
||||||
|
uint256 timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============ Modifiers ============
|
||||||
|
modifier validTask(uint256 _taskId) {
|
||||||
|
require(taskExists[_taskId], "Invalid task ID");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier onlyRequester(uint256 _taskId) {
|
||||||
|
require(tasks[_taskId].requester == msg.sender, "Not requester");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier taskIsOpen(uint256 _taskId) {
|
||||||
|
require(tasks[_taskId].status == TaskStatus.Open, "Not open");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier taskInProgress(uint256 _taskId) {
|
||||||
|
require(
|
||||||
|
tasks[_taskId].status == TaskStatus.InProgress,
|
||||||
|
"Not in progress"
|
||||||
|
);
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Constructor ============
|
||||||
|
/**
|
||||||
|
* @param _cUSDAddress Valid cUSD contract address on Celo Sepolia.
|
||||||
|
*/
|
||||||
|
constructor(address _cUSDAddress) Ownable(msg.sender) {
|
||||||
|
require(_cUSDAddress != address(0), "Invalid cUSD address");
|
||||||
|
cUSD = IERC20(_cUSDAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Core Logic ============
|
||||||
|
|
||||||
|
function createTask(
|
||||||
|
uint256 _paymentAmount,
|
||||||
|
uint256 _durationInDays
|
||||||
|
) external whenNotPaused nonReentrant returns (uint256) {
|
||||||
|
require(_paymentAmount > 0, "Invalid amount");
|
||||||
|
require(
|
||||||
|
_durationInDays > 0 && _durationInDays <= 90,
|
||||||
|
"Invalid duration"
|
||||||
|
);
|
||||||
|
|
||||||
|
taskCounter++;
|
||||||
|
uint256 newTaskId = taskCounter;
|
||||||
|
uint256 expiry = block.timestamp + (_durationInDays * 1 days);
|
||||||
|
|
||||||
|
cUSD.safeTransferFrom(msg.sender, address(this), _paymentAmount);
|
||||||
|
|
||||||
|
tasks[newTaskId] = Task({
|
||||||
|
taskId: newTaskId,
|
||||||
|
requester: msg.sender,
|
||||||
|
worker: address(0),
|
||||||
|
paymentAmount: _paymentAmount,
|
||||||
|
status: TaskStatus.Open,
|
||||||
|
createdAt: block.timestamp,
|
||||||
|
expiresAt: expiry
|
||||||
|
});
|
||||||
|
|
||||||
|
taskExists[newTaskId] = true;
|
||||||
|
|
||||||
|
emit TaskCreated(newTaskId, msg.sender, _paymentAmount, expiry);
|
||||||
|
emit DebugLog("createTask", msg.sender, newTaskId, block.timestamp);
|
||||||
|
|
||||||
|
return newTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignWorker(
|
||||||
|
uint256 _taskId,
|
||||||
|
address _worker
|
||||||
|
) external validTask(_taskId) taskIsOpen(_taskId) whenNotPaused {
|
||||||
|
Task storage task = tasks[_taskId];
|
||||||
|
require(
|
||||||
|
msg.sender == task.requester || msg.sender == owner(),
|
||||||
|
"Not authorized"
|
||||||
|
);
|
||||||
|
require(_worker != address(0), "Invalid worker");
|
||||||
|
require(_worker != task.requester, "Requester cannot be worker");
|
||||||
|
|
||||||
|
task.worker = _worker;
|
||||||
|
task.status = TaskStatus.InProgress;
|
||||||
|
|
||||||
|
emit WorkerAssigned(_taskId, _worker);
|
||||||
|
emit DebugLog("assignWorker", msg.sender, _taskId, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveSubmission(
|
||||||
|
uint256 _taskId
|
||||||
|
)
|
||||||
|
external
|
||||||
|
onlyOwner
|
||||||
|
validTask(_taskId)
|
||||||
|
taskInProgress(_taskId)
|
||||||
|
nonReentrant
|
||||||
|
{
|
||||||
|
Task storage task = tasks[_taskId];
|
||||||
|
uint256 total = task.paymentAmount;
|
||||||
|
uint256 platformFee = (total * PLATFORM_FEE_BPS) / BPS_DENOMINATOR;
|
||||||
|
uint256 workerShare = total - platformFee;
|
||||||
|
|
||||||
|
task.status = TaskStatus.Completed;
|
||||||
|
platformFeesAccumulated += platformFee;
|
||||||
|
cUSD.safeTransfer(task.worker, workerShare);
|
||||||
|
|
||||||
|
emit PaymentReleased(_taskId, task.worker, workerShare, platformFee);
|
||||||
|
emit DebugLog(
|
||||||
|
"approveSubmission",
|
||||||
|
msg.sender,
|
||||||
|
_taskId,
|
||||||
|
block.timestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectSubmission(
|
||||||
|
uint256 _taskId
|
||||||
|
)
|
||||||
|
external
|
||||||
|
onlyOwner
|
||||||
|
validTask(_taskId)
|
||||||
|
taskInProgress(_taskId)
|
||||||
|
nonReentrant
|
||||||
|
{
|
||||||
|
Task storage task = tasks[_taskId];
|
||||||
|
task.status = TaskStatus.Cancelled;
|
||||||
|
|
||||||
|
cUSD.safeTransfer(task.requester, task.paymentAmount);
|
||||||
|
|
||||||
|
emit TaskCancelled(_taskId, task.requester, task.paymentAmount);
|
||||||
|
emit DebugLog("rejectSubmission", msg.sender, _taskId, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTask(
|
||||||
|
uint256 _taskId
|
||||||
|
)
|
||||||
|
external
|
||||||
|
validTask(_taskId)
|
||||||
|
taskIsOpen(_taskId)
|
||||||
|
onlyRequester(_taskId)
|
||||||
|
nonReentrant
|
||||||
|
{
|
||||||
|
Task storage task = tasks[_taskId];
|
||||||
|
task.status = TaskStatus.Cancelled;
|
||||||
|
|
||||||
|
cUSD.safeTransfer(task.requester, task.paymentAmount);
|
||||||
|
|
||||||
|
emit TaskCancelled(_taskId, task.requester, task.paymentAmount);
|
||||||
|
emit DebugLog("cancelTask", msg.sender, _taskId, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function claimExpiredTask(
|
||||||
|
uint256 _taskId
|
||||||
|
) external validTask(_taskId) nonReentrant {
|
||||||
|
Task storage task = tasks[_taskId];
|
||||||
|
require(block.timestamp >= task.expiresAt, "Not expired");
|
||||||
|
require(
|
||||||
|
task.status == TaskStatus.Open ||
|
||||||
|
task.status == TaskStatus.InProgress,
|
||||||
|
"Already finalized"
|
||||||
|
);
|
||||||
|
require(msg.sender == task.requester, "Only requester");
|
||||||
|
|
||||||
|
task.status = TaskStatus.Expired;
|
||||||
|
cUSD.safeTransfer(task.requester, task.paymentAmount);
|
||||||
|
|
||||||
|
emit TaskExpired(_taskId, task.requester, task.paymentAmount);
|
||||||
|
emit DebugLog("claimExpiredTask", msg.sender, _taskId, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withdrawPlatformFees() external onlyOwner nonReentrant {
|
||||||
|
uint256 amount = platformFeesAccumulated;
|
||||||
|
require(amount > 0, "No fees");
|
||||||
|
platformFeesAccumulated = 0;
|
||||||
|
cUSD.safeTransfer(owner(), amount);
|
||||||
|
|
||||||
|
emit PlatformFeesWithdrawn(owner(), amount);
|
||||||
|
emit DebugLog("withdrawPlatformFees", msg.sender, 0, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Views ============
|
||||||
|
function getTask(
|
||||||
|
uint256 _taskId
|
||||||
|
) external view validTask(_taskId) returns (Task memory) {
|
||||||
|
return tasks[_taskId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTaskExpired(
|
||||||
|
uint256 _taskId
|
||||||
|
) external view validTask(_taskId) returns (bool) {
|
||||||
|
return block.timestamp >= tasks[_taskId].expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContractBalance() external view returns (uint256) {
|
||||||
|
return cUSD.balanceOf(address(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Admin Controls ============
|
||||||
|
function pause() external onlyOwner {
|
||||||
|
_pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpause() external onlyOwner {
|
||||||
|
_unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
132
dmtp/server/create-task-with-blockchain.ts
Normal file
132
dmtp/server/create-task-with-blockchain.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
import { TaskType } from './src/types/database.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script creates a task directly using the blockchain service,
|
||||||
|
* bypassing the API authentication requirements.
|
||||||
|
*
|
||||||
|
* This is useful for testing the complete payment flow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function createTaskDirectly() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Creating task with blockchain integration...\n');
|
||||||
|
|
||||||
|
// Task creator wallet (must be registered first)
|
||||||
|
const requesterWallet = '0xA0e793E7257c065b30c46Ef6828F2B3C0de87A8E';
|
||||||
|
|
||||||
|
// Check if user exists, if not create them
|
||||||
|
let requester = await prisma.user.findUnique({
|
||||||
|
where: { walletAddress: requesterWallet.toLowerCase() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!requester) {
|
||||||
|
console.log('📝 Creating user...');
|
||||||
|
requester = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
walletAddress: requesterWallet.toLowerCase(),
|
||||||
|
reputationScore: 100,
|
||||||
|
role: 'worker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ User created: ${requester.id}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ User found: ${requester.id}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task details
|
||||||
|
const taskData = {
|
||||||
|
title: 'Verify this text - BLOCKCHAIN TASK 3',
|
||||||
|
description: 'Check if text makes sense. This task has proper blockchain integration!',
|
||||||
|
taskType: TaskType.TEXT_VERIFICATION,
|
||||||
|
paymentAmount: 0.01,
|
||||||
|
verificationCriteria: {
|
||||||
|
aiPrompt: 'Verify if the text makes sense',
|
||||||
|
requiredFields: ['text'],
|
||||||
|
},
|
||||||
|
maxSubmissions: 1,
|
||||||
|
expiresAt: new Date('2025-11-01T00:00:00Z'),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💰 Task Details:');
|
||||||
|
console.log(` Title: ${taskData.title}`);
|
||||||
|
console.log(` Payment: ${taskData.paymentAmount} cUSD`);
|
||||||
|
console.log(` Type: ${taskData.taskType}`);
|
||||||
|
console.log(` Max Submissions: ${taskData.maxSubmissions}`);
|
||||||
|
console.log(` Expires: ${taskData.expiresAt}\n`);
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
const durationMs = taskData.expiresAt.getTime() - Date.now();
|
||||||
|
const durationInDays = Math.ceil(durationMs / (1000 * 60 * 60 * 24));
|
||||||
|
console.log(`⏰ Duration: ${durationInDays} days\n`);
|
||||||
|
|
||||||
|
// Step 1: Create task on blockchain
|
||||||
|
console.log('⛓️ Creating task on blockchain...');
|
||||||
|
const blockchainResult = await blockchainService.createTask(
|
||||||
|
taskData.paymentAmount.toString(),
|
||||||
|
durationInDays
|
||||||
|
);
|
||||||
|
console.log(`✅ Blockchain task created!`);
|
||||||
|
console.log(` Contract Task ID: ${blockchainResult.taskId}`);
|
||||||
|
console.log(` Transaction Hash: ${blockchainResult.txHash}\n`);
|
||||||
|
|
||||||
|
// Step 2: Store in database
|
||||||
|
console.log('💾 Storing task in database...');
|
||||||
|
const task = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
requesterId: requester.id,
|
||||||
|
title: taskData.title,
|
||||||
|
description: taskData.description,
|
||||||
|
taskType: taskData.taskType,
|
||||||
|
paymentAmount: taskData.paymentAmount,
|
||||||
|
verificationCriteria: taskData.verificationCriteria,
|
||||||
|
maxSubmissions: taskData.maxSubmissions,
|
||||||
|
expiresAt: taskData.expiresAt,
|
||||||
|
contractTaskId: blockchainResult.taskId,
|
||||||
|
status: 'open',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Task created in database!`);
|
||||||
|
console.log(` Task ID: ${task.id}`);
|
||||||
|
console.log(` Contract Task ID: ${task.contractTaskId}\n`);
|
||||||
|
|
||||||
|
// Update user stats
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: requester.id },
|
||||||
|
data: {
|
||||||
|
totalTasksCreated: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('✅ SUCCESS! Task created with blockchain integration!');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
console.log('📋 Task Summary:');
|
||||||
|
console.log(` Database ID: ${task.id}`);
|
||||||
|
console.log(` Blockchain Contract ID: ${task.contractTaskId}`);
|
||||||
|
console.log(` Status: ${task.status}`);
|
||||||
|
console.log(` Payment Amount: ${task.paymentAmount} cUSD`);
|
||||||
|
console.log(` Transaction Hash: ${blockchainResult.txHash}\n`);
|
||||||
|
|
||||||
|
console.log('🎯 Next Steps:');
|
||||||
|
console.log('1. View this task at: http://localhost:3000/tasks');
|
||||||
|
console.log(`2. Submit work to task ID: ${task.id}`);
|
||||||
|
console.log('3. Verification will run automatically');
|
||||||
|
console.log('4. Payment will be released on approval! 🎉\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('\n❌ Error creating task:', error.message);
|
||||||
|
console.error('Stack:', error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTaskDirectly();
|
||||||
76
dmtp/server/debug-payment.js
Normal file
76
dmtp/server/debug-payment.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Debug script to check payment status for address 0xb12653F335f5C1B56A30afA840d394E90718633A
|
||||||
|
import { prisma } from "./src/database/connections.ts";
|
||||||
|
|
||||||
|
async function debugPayment() {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
"🔍 Checking payment status for wallet: 0xa0e793e7257c065b30c46ef6828f2b3c0de87a8e"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find user by wallet
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { walletAddress: "0xa0e793e7257c065b30c46ef6828f2b3c0de87a8e" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error("❌ User not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ User found: ${user.id}`);
|
||||||
|
|
||||||
|
// Find all submissions for this worker
|
||||||
|
const submissions = await prisma.submission.findMany({
|
||||||
|
where: { workerId: user.id },
|
||||||
|
include: {
|
||||||
|
task: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
paymentAmount: true,
|
||||||
|
contractTaskId: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n📝 Found ${submissions.length} submissions:`);
|
||||||
|
|
||||||
|
for (const submission of submissions) {
|
||||||
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||||
|
console.log(`📋 Submission ID: ${submission.id}`);
|
||||||
|
console.log(` Task: ${submission.task.title}`);
|
||||||
|
console.log(` Verification Status: ${submission.verificationStatus}`);
|
||||||
|
console.log(` Task Status: ${submission.task.status}`);
|
||||||
|
console.log(` Contract Task ID: ${submission.task.contractTaskId}`);
|
||||||
|
console.log(` Amount: ${submission.task.paymentAmount} cUSD`);
|
||||||
|
|
||||||
|
// Find payment record
|
||||||
|
const payment = await prisma.payment.findFirst({
|
||||||
|
where: {
|
||||||
|
taskId: submission.taskId,
|
||||||
|
workerId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payment) {
|
||||||
|
console.log(` 💳 Payment Status: ${payment.status}`);
|
||||||
|
console.log(` 💳 Transaction Hash: ${payment.transactionHash}`);
|
||||||
|
console.log(` 💳 Amount: ${payment.amount}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ No payment record found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error:", error);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPayment();
|
||||||
99
dmtp/server/deploy-test-token.ts
Normal file
99
dmtp/server/deploy-test-token.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import MockERC20Artifact from './artifacts/contracts/MockERC20.sol/MockERC20.json';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function deployTestToken() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Deploying Test Token to Celo Sepolia...\n');
|
||||||
|
|
||||||
|
// Initialize provider and signer
|
||||||
|
const provider = new ethers.JsonRpcProvider(
|
||||||
|
process.env.CELO_RPC_URL || 'https://forno.celo-sepolia.celo-testnet.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = process.env.PRIVATE_KEY;
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error('PRIVATE_KEY not configured in .env');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(privateKey, provider);
|
||||||
|
console.log(`📝 Deploying from: ${signer.address}`);
|
||||||
|
|
||||||
|
// Check balance
|
||||||
|
const balance = await provider.getBalance(signer.address);
|
||||||
|
const balanceInCELO = ethers.formatEther(balance);
|
||||||
|
console.log(`💰 Account balance: ${balanceInCELO} CELO\n`);
|
||||||
|
|
||||||
|
if (parseFloat(balanceInCELO) === 0) {
|
||||||
|
console.log('❌ Insufficient balance! Get testnet CELO from:');
|
||||||
|
console.log(' https://faucet.celo.org');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy token with name and symbol
|
||||||
|
const tokenName = 'Test USD';
|
||||||
|
const tokenSymbol = 'tUSD';
|
||||||
|
console.log(`📦 Deploying ${tokenName} (${tokenSymbol})...\n`);
|
||||||
|
|
||||||
|
const TestToken = new ethers.ContractFactory(
|
||||||
|
MockERC20Artifact.abi,
|
||||||
|
MockERC20Artifact.bytecode,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('⏳ Sending deployment transaction...');
|
||||||
|
const token = await TestToken.deploy(tokenName, tokenSymbol);
|
||||||
|
|
||||||
|
console.log(`📍 Deployment transaction sent: ${token.deploymentTransaction()?.hash}`);
|
||||||
|
console.log('⏳ Waiting for confirmation...\n');
|
||||||
|
|
||||||
|
await token.waitForDeployment();
|
||||||
|
const tokenAddress = await token.getAddress();
|
||||||
|
|
||||||
|
console.log('✅ Token deployed successfully!\n');
|
||||||
|
console.log('═══════════════════════════════════════');
|
||||||
|
console.log(`Token Address: ${tokenAddress}`);
|
||||||
|
console.log('═══════════════════════════════════════\n');
|
||||||
|
|
||||||
|
// Verify deployment - create a properly typed contract instance
|
||||||
|
const deployedToken = new ethers.Contract(tokenAddress, MockERC20Artifact.abi, signer);
|
||||||
|
|
||||||
|
// Mint initial supply (1 million tokens)
|
||||||
|
const initialSupply = ethers.parseEther('1000000');
|
||||||
|
console.log('⏳ Minting initial supply...');
|
||||||
|
const mintTx = await deployedToken.mint(signer.address, initialSupply);
|
||||||
|
await mintTx.wait();
|
||||||
|
console.log('✅ Minted 1,000,000 tokens\n');
|
||||||
|
|
||||||
|
const name = await deployedToken.name();
|
||||||
|
const symbol = await deployedToken.symbol();
|
||||||
|
const totalSupply = await deployedToken.totalSupply();
|
||||||
|
const deployerBalance = await deployedToken.balanceOf(signer.address);
|
||||||
|
|
||||||
|
console.log('📊 Token Details:');
|
||||||
|
console.log(` Name: ${name}`);
|
||||||
|
console.log(` Symbol: ${symbol}`);
|
||||||
|
console.log(` Total Supply: ${ethers.formatEther(totalSupply)} ${symbol}`);
|
||||||
|
console.log(` Deployer Balance: ${ethers.formatEther(deployerBalance)} ${symbol}\n`);
|
||||||
|
|
||||||
|
// Output for .env
|
||||||
|
console.log('📝 Update your .env file with:');
|
||||||
|
console.log(` CUSD_SEPOLIA_ADDRESS=${tokenAddress}\n`);
|
||||||
|
|
||||||
|
console.log('📄 To verify on Celoscan:');
|
||||||
|
console.log(` 1. Go to https://sepolia.celoscan.io/address/${tokenAddress}#code`);
|
||||||
|
console.log(` 2. Click "Verify and Publish"`);
|
||||||
|
console.log(` 3. Use contract: contracts/MockERC20.sol:MockERC20`);
|
||||||
|
console.log(` 4. Constructor arguments: "${tokenName}", "${tokenSymbol}"\n`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Deployment failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deployTestToken();
|
||||||
98
dmtp/server/diagnose-task-mismatch.ts
Normal file
98
dmtp/server/diagnose-task-mismatch.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
|
||||||
|
async function diagnoseTaskMismatch() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Diagnosing Task Mismatch between Database and Blockchain...\n');
|
||||||
|
|
||||||
|
// Get contract info
|
||||||
|
const contractAddress = blockchainService.getContractAddress();
|
||||||
|
console.log(`📋 Contract Address: ${contractAddress}`);
|
||||||
|
|
||||||
|
const taskCounter = await blockchainService.getTaskCounter();
|
||||||
|
console.log(`📊 Blockchain Task Counter: ${taskCounter}\n`);
|
||||||
|
|
||||||
|
// Get tasks from database with contractTaskId
|
||||||
|
const tasksInDb = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
contractTaskId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
contractTaskId: true,
|
||||||
|
status: true,
|
||||||
|
paymentAmount: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`💾 Found ${tasksInDb.length} tasks in database with contractTaskId\n`);
|
||||||
|
|
||||||
|
if (tasksInDb.length === 0) {
|
||||||
|
console.log('⚠️ No tasks with blockchain integration found in database');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('Checking each task on blockchain...\n');
|
||||||
|
|
||||||
|
for (const task of tasksInDb) {
|
||||||
|
console.log(`\n📋 Task: ${task.title}`);
|
||||||
|
console.log(` DB ID: ${task.id}`);
|
||||||
|
console.log(` Contract Task ID: ${task.contractTaskId}`);
|
||||||
|
console.log(` DB Status: ${task.status}`);
|
||||||
|
console.log(` Payment: ${task.paymentAmount} cUSD`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blockchainTask = await blockchainService.getTask(task.contractTaskId!);
|
||||||
|
|
||||||
|
if (!blockchainTask) {
|
||||||
|
console.log(` ❌ NOT FOUND on blockchain`);
|
||||||
|
console.log(` Issue: Task ${task.contractTaskId} does not exist on contract ${contractAddress}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ✅ FOUND on blockchain`);
|
||||||
|
console.log(` Blockchain Status: ${['Open', 'InProgress', 'Completed', 'Cancelled', 'Expired'][blockchainTask.status]}`);
|
||||||
|
console.log(` Requester: ${blockchainTask.requester}`);
|
||||||
|
console.log(` Worker: ${blockchainTask.worker === '0x0000000000000000000000000000000000000000' ? 'None' : blockchainTask.worker}`);
|
||||||
|
console.log(` Payment: ${blockchainTask.paymentAmount} cUSD`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(` ❌ ERROR checking blockchain: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' ' + '─'.repeat(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('\n💡 Recommendations:');
|
||||||
|
console.log('1. If tasks are NOT FOUND on blockchain:');
|
||||||
|
console.log(' - The contract address may have changed');
|
||||||
|
console.log(' - Check CONTRACT_ADDRESS in .env matches the deployed contract');
|
||||||
|
console.log(' - You may need to create new tasks with the current contract\n');
|
||||||
|
|
||||||
|
console.log('2. If tasks exist but have wrong status:');
|
||||||
|
console.log(' - Database and blockchain are out of sync');
|
||||||
|
console.log(' - Verify worker assignment happened on blockchain\n');
|
||||||
|
|
||||||
|
console.log('3. To fix:');
|
||||||
|
console.log(' - Option A: Update CONTRACT_ADDRESS to the old contract');
|
||||||
|
console.log(' - Option B: Create new tasks with current contract');
|
||||||
|
console.log(' - Option C: Clear old tasks and start fresh\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnoseTaskMismatch();
|
||||||
68
dmtp/server/hardhat.config.ts
Normal file
68
dmtp/server/hardhat.config.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import "@nomicfoundation/hardhat-toolbox";
|
||||||
|
import "@nomicfoundation/hardhat-verify";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import { HardhatUserConfig } from "hardhat/config";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const config: HardhatUserConfig = {
|
||||||
|
solidity: {
|
||||||
|
version: "0.8.20",
|
||||||
|
settings: {
|
||||||
|
optimizer: {
|
||||||
|
enabled: true,
|
||||||
|
runs: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
// Celo Sepolia Testnet
|
||||||
|
sepolia: {
|
||||||
|
url: "https://forno.celo-sepolia.celo-testnet.org",
|
||||||
|
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||||
|
chainId: 11142220,
|
||||||
|
},
|
||||||
|
// Celo Mainnet
|
||||||
|
celo: {
|
||||||
|
url: "https://forno.celo.org",
|
||||||
|
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
|
||||||
|
chainId: 42220,
|
||||||
|
},
|
||||||
|
// Local Hardhat network
|
||||||
|
hardhat: {
|
||||||
|
chainId: 31337,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
etherscan: {
|
||||||
|
apiKey: {
|
||||||
|
sepolia: process.env.CELOSCAN_API_KEY || "",
|
||||||
|
celo: process.env.CELOSCAN_API_KEY || "",
|
||||||
|
},
|
||||||
|
customChains: [
|
||||||
|
{
|
||||||
|
network: "Celo Sepolia Testnet",
|
||||||
|
chainId: 11142220,
|
||||||
|
urls: {
|
||||||
|
apiURL: "https://api-sepolia.celoscan.io/api",
|
||||||
|
browserURL: "https://sepolia.celoscan.io",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
network: "celo",
|
||||||
|
chainId: 42220,
|
||||||
|
urls: {
|
||||||
|
apiURL: "https://api.celoscan.io/api",
|
||||||
|
browserURL: "https://celoscan.io",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
sources: "./contracts",
|
||||||
|
tests: "./test",
|
||||||
|
cache: "./cache",
|
||||||
|
artifacts: "./artifacts",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
10215
dmtp/server/package-lock.json
generated
Normal file
10215
dmtp/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
66
dmtp/server/package.json
Normal file
66
dmtp/server/package.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
|
"prisma:push": "prisma db push",
|
||||||
|
"db:seed": "tsx src/database/seed.ts",
|
||||||
|
"compile": "hardhat compile",
|
||||||
|
"test": "mocha -r ts-node/register test/**/*.test.ts",
|
||||||
|
"test:coverage": "hardhat coverage",
|
||||||
|
"deploy:sepolia": "hardhat run scripts/deploy.ts --network sepolia",
|
||||||
|
"deploy:local": "hardhat run scripts/deploy.ts --network hardhat",
|
||||||
|
"interact": "hardhat run scripts/interact.ts --network sepolia",
|
||||||
|
"verify": "hardhat verify --network sepolia",
|
||||||
|
"node": "hardhat node",
|
||||||
|
"test:ai": "mocha -r ts-node/register test/ai-verification.test.ts",
|
||||||
|
"dev": "nodemon --exec tsx src/server.ts",
|
||||||
|
"build": "tsc && npm run copy:js && prisma generate",
|
||||||
|
"copy:js": "rsync -av --include='*/' --include='*.js' --exclude='*' src/ dist/",
|
||||||
|
"start": "node dist/server.js",
|
||||||
|
"start:worker": "node dist/workers/index.js",
|
||||||
|
"start:all": "npm run build && concurrently \"npm run start\" \"npm run start:worker\"",
|
||||||
|
"worker": "ts-node src/workers/index.ts",
|
||||||
|
"dev:worker": "nodemon src/workers/index.ts",
|
||||||
|
"dev:all": "concurrently \"npm run dev\" \"npm run dev:worker\""
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@bull-board/api": "^6.14.0",
|
||||||
|
"@bull-board/express": "^6.14.0",
|
||||||
|
"@google/generative-ai": "^0.24.1",
|
||||||
|
"@openzeppelin/contracts": "^5.4.0",
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/bull": "^3.15.9",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.4",
|
||||||
|
"@types/morgan": "^1.9.10",
|
||||||
|
"bull": "^4.16.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-validator": "^7.3.0",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
|
"redis": "^5.9.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomicfoundation/hardhat-ethers": "^3.1.0",
|
||||||
|
"@nomicfoundation/hardhat-toolbox": "^6.1.0",
|
||||||
|
"@nomicfoundation/hardhat-verify": "^2.1.1",
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"ethers": "^6.15.0",
|
||||||
|
"hardhat": "^2.26.3",
|
||||||
|
"nodemon": "^3.0.0",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
|
"tsx": "^4.20.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
dmtp/server/prisma.config.ts
Normal file
12
dmtp/server/prisma.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig, env } from "prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
engine: "classic",
|
||||||
|
datasource: {
|
||||||
|
url: env("DATABASE_URL"),
|
||||||
|
},
|
||||||
|
});
|
||||||
140
dmtp/server/prisma/migrations/20251026032137_init/migration.sql
Normal file
140
dmtp/server/prisma/migrations/20251026032137_init/migration.sql
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "UserRole" AS ENUM ('requester', 'worker');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "TaskType" AS ENUM ('text_verification', 'image_labeling', 'survey', 'content_moderation');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "TaskStatus" AS ENUM ('open', 'in_progress', 'completed', 'expired');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "VerificationStatus" AS ENUM ('pending', 'approved', 'rejected');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "PaymentStatus" AS ENUM ('pending', 'completed', 'failed');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "tasks" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"requester_id" TEXT NOT NULL,
|
||||||
|
"title" VARCHAR(100) NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"task_type" "TaskType" NOT NULL,
|
||||||
|
"payment_amount" DECIMAL(10,2) NOT NULL,
|
||||||
|
"status" "TaskStatus" NOT NULL DEFAULT 'open',
|
||||||
|
"verification_criteria" JSONB NOT NULL,
|
||||||
|
"max_submissions" INTEGER NOT NULL,
|
||||||
|
"contract_task_id" INTEGER,
|
||||||
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"wallet_address" TEXT NOT NULL,
|
||||||
|
"phone_number" TEXT,
|
||||||
|
"role" "UserRole" NOT NULL,
|
||||||
|
"reputation_score" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"total_earnings" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
"total_tasks_created" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"total_tasks_completed" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "submissions" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"task_id" TEXT NOT NULL,
|
||||||
|
"worker_id" TEXT NOT NULL,
|
||||||
|
"submission_data" JSONB NOT NULL,
|
||||||
|
"ai_verification_result" JSONB,
|
||||||
|
"verification_status" "VerificationStatus" NOT NULL DEFAULT 'pending',
|
||||||
|
"payment_transaction_hash" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "submissions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "payments" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"task_id" TEXT NOT NULL,
|
||||||
|
"worker_id" TEXT NOT NULL,
|
||||||
|
"amount" DECIMAL(10,2) NOT NULL,
|
||||||
|
"transaction_hash" TEXT NOT NULL,
|
||||||
|
"status" "PaymentStatus" NOT NULL DEFAULT 'pending',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tasks_requester_id_idx" ON "tasks"("requester_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tasks_status_idx" ON "tasks"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tasks_task_type_idx" ON "tasks"("task_type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tasks_expires_at_idx" ON "tasks"("expires_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "tasks_contract_task_id_idx" ON "tasks"("contract_task_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_wallet_address_key" ON "users"("wallet_address");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "users_wallet_address_idx" ON "users"("wallet_address");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "users_role_idx" ON "users"("role");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "submissions_task_id_idx" ON "submissions"("task_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "submissions_worker_id_idx" ON "submissions"("worker_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "submissions_verification_status_idx" ON "submissions"("verification_status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "submissions_task_id_worker_id_key" ON "submissions"("task_id", "worker_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payments_task_id_idx" ON "payments"("task_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payments_worker_id_idx" ON "payments"("worker_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payments_transaction_hash_idx" ON "payments"("transaction_hash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "payments_status_idx" ON "payments"("status");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_requester_id_fkey" FOREIGN KEY ("requester_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "submissions" ADD CONSTRAINT "submissions_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "submissions" ADD CONSTRAINT "submissions_worker_id_fkey" FOREIGN KEY ("worker_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "payments" ADD CONSTRAINT "payments_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "payments" ADD CONSTRAINT "payments_worker_id_fkey" FOREIGN KEY ("worker_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
3
dmtp/server/prisma/migrations/migration_lock.toml
Normal file
3
dmtp/server/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
123
dmtp/server/prisma/schema.prisma
Normal file
123
dmtp/server/prisma/schema.prisma
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Task {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
requesterId String @map("requester_id")
|
||||||
|
title String @db.VarChar(100)
|
||||||
|
description String
|
||||||
|
taskType TaskType @map("task_type")
|
||||||
|
paymentAmount Decimal @map("payment_amount") @db.Decimal(10, 2)
|
||||||
|
status TaskStatus @default(open)
|
||||||
|
verificationCriteria Json @map("verification_criteria")
|
||||||
|
maxSubmissions Int @map("max_submissions")
|
||||||
|
contractTaskId Int? @map("contract_task_id")
|
||||||
|
expiresAt DateTime @map("expires_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
payments Payment[]
|
||||||
|
submissions Submission[]
|
||||||
|
requester User @relation("TaskRequester", fields: [requesterId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([requesterId])
|
||||||
|
@@index([status])
|
||||||
|
@@index([taskType])
|
||||||
|
@@index([expiresAt])
|
||||||
|
@@index([contractTaskId])
|
||||||
|
@@map("tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
walletAddress String @unique @map("wallet_address")
|
||||||
|
phoneNumber String? @map("phone_number")
|
||||||
|
role UserRole
|
||||||
|
reputationScore Int @default(0) @map("reputation_score")
|
||||||
|
totalEarnings Decimal @default(0) @map("total_earnings") @db.Decimal(10, 2)
|
||||||
|
totalTasksCreated Int @default(0) @map("total_tasks_created")
|
||||||
|
totalTasksCompleted Int @default(0) @map("total_tasks_completed")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
payments Payment[]
|
||||||
|
submissions Submission[]
|
||||||
|
createdTasks Task[] @relation("TaskRequester")
|
||||||
|
|
||||||
|
@@index([walletAddress])
|
||||||
|
@@index([role])
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Submission {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
taskId String @map("task_id")
|
||||||
|
workerId String @map("worker_id")
|
||||||
|
submissionData Json @map("submission_data")
|
||||||
|
aiVerificationResult Json? @map("ai_verification_result")
|
||||||
|
verificationStatus VerificationStatus @default(pending) @map("verification_status")
|
||||||
|
paymentTransactionHash String? @map("payment_transaction_hash")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||||
|
worker User @relation(fields: [workerId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([taskId, workerId])
|
||||||
|
@@index([taskId])
|
||||||
|
@@index([workerId])
|
||||||
|
@@index([verificationStatus])
|
||||||
|
@@map("submissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Payment {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
taskId String @map("task_id")
|
||||||
|
workerId String @map("worker_id")
|
||||||
|
amount Decimal @db.Decimal(10, 2)
|
||||||
|
transactionHash String @map("transaction_hash")
|
||||||
|
status PaymentStatus @default(pending)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||||
|
worker User @relation(fields: [workerId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([taskId])
|
||||||
|
@@index([workerId])
|
||||||
|
@@index([transactionHash])
|
||||||
|
@@index([status])
|
||||||
|
@@map("payments")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
requester
|
||||||
|
worker
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TaskType {
|
||||||
|
text_verification
|
||||||
|
image_labeling
|
||||||
|
survey
|
||||||
|
content_moderation
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TaskStatus {
|
||||||
|
open
|
||||||
|
in_progress
|
||||||
|
completed
|
||||||
|
expired
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VerificationStatus {
|
||||||
|
pending
|
||||||
|
approved
|
||||||
|
rejected
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentStatus {
|
||||||
|
pending
|
||||||
|
completed
|
||||||
|
failed
|
||||||
|
}
|
||||||
76
dmtp/server/redeploy-task-escrow.ts
Normal file
76
dmtp/server/redeploy-task-escrow.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
async function redeployTaskEscrow() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Redeploying TaskEscrow Contract...\n');
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(
|
||||||
|
process.env.CELO_RPC_URL || 'https://forno.celo-sepolia.celo-testnet.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = process.env.PRIVATE_KEY;
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error('PRIVATE_KEY not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = new ethers.Wallet(privateKey, provider);
|
||||||
|
console.log(`📝 Deploying from: ${signer.address}`);
|
||||||
|
|
||||||
|
const balance = await provider.getBalance(signer.address);
|
||||||
|
console.log(`💰 Balance: ${ethers.formatEther(balance)} CELO\n`);
|
||||||
|
|
||||||
|
const cUSDAddress = process.env.CUSD_SEPOLIA_ADDRESS;
|
||||||
|
if (!cUSDAddress) {
|
||||||
|
throw new Error('CUSD_SEPOLIA_ADDRESS not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`💰 Using cUSD Token: ${cUSDAddress}\n`);
|
||||||
|
|
||||||
|
// Load contract artifact
|
||||||
|
const TaskEscrowArtifact = JSON.parse(
|
||||||
|
readFileSync(
|
||||||
|
join(__dirname, './artifacts/contracts/TaskEscrow.sol/TaskEscrow.json'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const TaskEscrow = new ethers.ContractFactory(
|
||||||
|
TaskEscrowArtifact.abi,
|
||||||
|
TaskEscrowArtifact.bytecode,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('⏳ Deploying TaskEscrow contract...');
|
||||||
|
const taskEscrow = await TaskEscrow.deploy(cUSDAddress);
|
||||||
|
|
||||||
|
console.log(`📍 Deployment transaction: ${taskEscrow.deploymentTransaction()?.hash}`);
|
||||||
|
console.log('⏳ Waiting for confirmation...\n');
|
||||||
|
|
||||||
|
await taskEscrow.waitForDeployment();
|
||||||
|
const contractAddress = await taskEscrow.getAddress();
|
||||||
|
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('✅ TaskEscrow Contract Deployed!');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
console.log(`Contract Address: ${contractAddress}`);
|
||||||
|
console.log(`cUSD Token: ${cUSDAddress}\n`);
|
||||||
|
|
||||||
|
console.log('📝 Update your .env file with:');
|
||||||
|
console.log(` CONTRACT_ADDRESS=${contractAddress}\n`);
|
||||||
|
|
||||||
|
console.log('🎯 Next Steps:');
|
||||||
|
console.log('1. Update CONTRACT_ADDRESS in your .env file');
|
||||||
|
console.log('2. Run: npx tsx approve-cusd.ts');
|
||||||
|
console.log('3. Run: npx tsx create-task-with-blockchain.ts\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Deployment failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redeployTaskEscrow();
|
||||||
59
dmtp/server/scripts/deploy.js
Normal file
59
dmtp/server/scripts/deploy.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const hardhat_1 = require("hardhat");
|
||||||
|
async function main() {
|
||||||
|
console.log("🚀 Starting deployment to Celo Sepolia...\n");
|
||||||
|
// Get deployer account
|
||||||
|
const [deployer] = await hardhat_1.ethers.getSigners();
|
||||||
|
console.log("📝 Deploying contracts with account:", deployer.address);
|
||||||
|
// Get account balance
|
||||||
|
const balance = await hardhat_1.ethers.provider.getBalance(deployer.address);
|
||||||
|
console.log("💰 Account balance:", hardhat_1.ethers.formatEther(balance), "CELO\n");
|
||||||
|
// cUSD token address on Sepolia testnet
|
||||||
|
const CUSD_SEPOLIA = "0x874069fa1eb16d44d622f2e0ca25eea172369bc1";
|
||||||
|
console.log("📄 cUSD Token Address:", CUSD_SEPOLIA);
|
||||||
|
// Deploy TaskEscrow contract
|
||||||
|
console.log("\n⏳ Deploying TaskEscrow contract...");
|
||||||
|
const TaskEscrow = await hardhat_1.ethers.getContractFactory("TaskEscrow");
|
||||||
|
const taskEscrow = await TaskEscrow.deploy(CUSD_SEPOLIA);
|
||||||
|
await taskEscrow.waitForDeployment();
|
||||||
|
const taskEscrowAddress = await taskEscrow.getAddress();
|
||||||
|
console.log("✅ TaskEscrow deployed to:", taskEscrowAddress);
|
||||||
|
// Verify contract details
|
||||||
|
console.log("\n📋 Contract Details:");
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log("Network: Celo Sepolia");
|
||||||
|
console.log("Contract: TaskEscrow");
|
||||||
|
console.log("Address:", taskEscrowAddress);
|
||||||
|
console.log("Owner:", deployer.address);
|
||||||
|
console.log("cUSD Token:", CUSD_SEPOLIA);
|
||||||
|
console.log("Platform Fee: 5%");
|
||||||
|
console.log("-----------------------------------\n");
|
||||||
|
// Save deployment info
|
||||||
|
const deploymentInfo = {
|
||||||
|
network: "sepolia",
|
||||||
|
contractName: "TaskEscrow",
|
||||||
|
contractAddress: taskEscrowAddress,
|
||||||
|
deployer: deployer.address,
|
||||||
|
cUSDAddress: CUSD_SEPOLIA,
|
||||||
|
deployedAt: new Date().toISOString(),
|
||||||
|
blockNumber: await hardhat_1.ethers.provider.getBlockNumber(),
|
||||||
|
};
|
||||||
|
console.log("💾 Deployment Info:");
|
||||||
|
console.log(JSON.stringify(deploymentInfo, null, 2));
|
||||||
|
console.log("\n🔍 Verify contract on Celoscan:");
|
||||||
|
console.log(`npx hardhat verify --network sepolia ${taskEscrowAddress} ${CUSD_SEPOLIA}`);
|
||||||
|
console.log("\n✨ Deployment completed successfully!\n");
|
||||||
|
// Return addresses for use in scripts
|
||||||
|
return {
|
||||||
|
taskEscrowAddress,
|
||||||
|
cUSDAddress: CUSD_SEPOLIA,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Execute deployment
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("❌ Deployment failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
72
dmtp/server/scripts/deploy.ts
Normal file
72
dmtp/server/scripts/deploy.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("🚀 Starting deployment to Celo Sepolia...\n");
|
||||||
|
|
||||||
|
// Get deployer account
|
||||||
|
const [deployer] = await ethers.getSigners();
|
||||||
|
console.log("📝 Deploying contracts with account:", deployer.address);
|
||||||
|
|
||||||
|
// Get account balance
|
||||||
|
const balance = await ethers.provider.getBalance(deployer.address);
|
||||||
|
console.log("💰 Account balance:", ethers.formatEther(balance), "CELO\n");
|
||||||
|
|
||||||
|
// cUSD token address on Sepolia testnet
|
||||||
|
const CUSD_SEPOLIA = "0x874069fa1eb16d44d622f2e0ca25eea172369bc1";
|
||||||
|
|
||||||
|
console.log("📄 cUSD Token Address:", CUSD_SEPOLIA);
|
||||||
|
|
||||||
|
// Deploy TaskEscrow contract
|
||||||
|
console.log("\n⏳ Deploying TaskEscrow contract...");
|
||||||
|
const TaskEscrow = await ethers.getContractFactory("TaskEscrow");
|
||||||
|
const taskEscrow = await TaskEscrow.deploy(CUSD_SEPOLIA);
|
||||||
|
|
||||||
|
await taskEscrow.waitForDeployment();
|
||||||
|
const taskEscrowAddress = await taskEscrow.getAddress();
|
||||||
|
|
||||||
|
console.log("✅ TaskEscrow deployed to:", taskEscrowAddress);
|
||||||
|
|
||||||
|
// Verify contract details
|
||||||
|
console.log("\n📋 Contract Details:");
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log("Network: Celo Sepolia");
|
||||||
|
console.log("Contract: TaskEscrow");
|
||||||
|
console.log("Address:", taskEscrowAddress);
|
||||||
|
console.log("Owner:", deployer.address);
|
||||||
|
console.log("cUSD Token:", CUSD_SEPOLIA);
|
||||||
|
console.log("Platform Fee: 5%");
|
||||||
|
console.log("-----------------------------------\n");
|
||||||
|
|
||||||
|
// Save deployment info
|
||||||
|
const deploymentInfo = {
|
||||||
|
network: "sepolia",
|
||||||
|
contractName: "TaskEscrow",
|
||||||
|
contractAddress: taskEscrowAddress,
|
||||||
|
deployer: deployer.address,
|
||||||
|
cUSDAddress: CUSD_SEPOLIA,
|
||||||
|
deployedAt: new Date().toISOString(),
|
||||||
|
blockNumber: await ethers.provider.getBlockNumber(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("💾 Deployment Info:");
|
||||||
|
console.log(JSON.stringify(deploymentInfo, null, 2));
|
||||||
|
|
||||||
|
console.log("\n🔍 Verify contract on Celoscan:");
|
||||||
|
console.log(`npx hardhat verify --network sepolia ${taskEscrowAddress} ${CUSD_SEPOLIA}`);
|
||||||
|
|
||||||
|
console.log("\n✨ Deployment completed successfully!\n");
|
||||||
|
|
||||||
|
// Return addresses for use in scripts
|
||||||
|
return {
|
||||||
|
taskEscrowAddress,
|
||||||
|
cUSDAddress: CUSD_SEPOLIA,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute deployment
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("❌ Deployment failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
72
dmtp/server/scripts/interact.js
Normal file
72
dmtp/server/scripts/interact.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const hardhat_1 = require("hardhat");
|
||||||
|
async function main() {
|
||||||
|
console.log("🔧 Interacting with TaskEscrow contract...\n");
|
||||||
|
// Replace with your deployed contract address
|
||||||
|
const TASK_ESCROW_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
|
||||||
|
const CUSD_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";
|
||||||
|
const [deployer, worker] = await hardhat_1.ethers.getSigners();
|
||||||
|
// Get contract instances
|
||||||
|
const TaskEscrow = await hardhat_1.ethers.getContractAt("TaskEscrow", TASK_ESCROW_ADDRESS);
|
||||||
|
const cUSD = await hardhat_1.ethers.getContractAt("IERC20", CUSD_ADDRESS);
|
||||||
|
console.log("📝 Contract Address:", TASK_ESCROW_ADDRESS);
|
||||||
|
console.log("👤 Deployer:", deployer.address);
|
||||||
|
console.log("👷 Worker:", worker.address, "\n");
|
||||||
|
// Check cUSD balance
|
||||||
|
const balance = await cUSD.balanceOf(deployer.address);
|
||||||
|
console.log("💰 Deployer cUSD Balance:", hardhat_1.ethers.formatEther(balance), "\n");
|
||||||
|
// Example: Create a task
|
||||||
|
console.log("📝 Creating a task...");
|
||||||
|
const paymentAmount = hardhat_1.ethers.parseEther("5"); // 5 cUSD
|
||||||
|
const durationInDays = 7;
|
||||||
|
// Approve TaskEscrow to spend cUSD
|
||||||
|
console.log("✅ Approving cUSD spending...");
|
||||||
|
const approveTx = await cUSD.approve(TASK_ESCROW_ADDRESS, paymentAmount);
|
||||||
|
await approveTx.wait();
|
||||||
|
console.log("✅ Approved!\n");
|
||||||
|
// Create task
|
||||||
|
const createTx = await TaskEscrow.createTask(paymentAmount, durationInDays);
|
||||||
|
const receipt = await createTx.wait();
|
||||||
|
// Get taskId from event
|
||||||
|
const event = receipt?.logs.find((log) => {
|
||||||
|
try {
|
||||||
|
return TaskEscrow.interface.parseLog(log)?.name === "TaskCreated";
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const parsedEvent = TaskEscrow.interface.parseLog(event);
|
||||||
|
const taskId = parsedEvent?.args[0];
|
||||||
|
console.log("✅ Task created! Task ID:", taskId.toString(), "\n");
|
||||||
|
// Get task details
|
||||||
|
const task = await TaskEscrow.getTask(taskId);
|
||||||
|
console.log("📋 Task Details:");
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log("Task ID:", task.taskId.toString());
|
||||||
|
console.log("Requester:", task.requester);
|
||||||
|
console.log("Payment Amount:", hardhat_1.ethers.formatEther(task.paymentAmount), "cUSD");
|
||||||
|
console.log("Status:", task.status);
|
||||||
|
console.log("-----------------------------------\n");
|
||||||
|
// Assign worker
|
||||||
|
console.log("👷 Assigning worker...");
|
||||||
|
const assignTx = await TaskEscrow.assignWorker(taskId, worker.address);
|
||||||
|
await assignTx.wait();
|
||||||
|
console.log("✅ Worker assigned!\n");
|
||||||
|
// Approve submission (as owner)
|
||||||
|
console.log("✅ Approving submission...");
|
||||||
|
const approveTx2 = await TaskEscrow.approveSubmission(taskId);
|
||||||
|
await approveTx2.wait();
|
||||||
|
console.log("✅ Submission approved! Payment released.\n");
|
||||||
|
// Check worker balance
|
||||||
|
const workerBalance = await cUSD.balanceOf(worker.address);
|
||||||
|
console.log("💰 Worker cUSD Balance:", hardhat_1.ethers.formatEther(workerBalance), "\n");
|
||||||
|
console.log("✨ Interaction completed!");
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("❌ Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
87
dmtp/server/scripts/interact.ts
Normal file
87
dmtp/server/scripts/interact.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("🔧 Interacting with TaskEscrow contract...\n");
|
||||||
|
|
||||||
|
// Replace with your deployed contract address
|
||||||
|
const TASK_ESCROW_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
|
||||||
|
const CUSD_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1";
|
||||||
|
|
||||||
|
const [deployer, worker] = await ethers.getSigners();
|
||||||
|
|
||||||
|
// Get contract instances
|
||||||
|
const TaskEscrow = await ethers.getContractAt("TaskEscrow", TASK_ESCROW_ADDRESS);
|
||||||
|
const cUSD = await ethers.getContractAt("IERC20", CUSD_ADDRESS);
|
||||||
|
|
||||||
|
console.log("📝 Contract Address:", TASK_ESCROW_ADDRESS);
|
||||||
|
console.log("👤 Deployer:", deployer.address);
|
||||||
|
console.log("👷 Worker:", worker.address, "\n");
|
||||||
|
|
||||||
|
// Check cUSD balance
|
||||||
|
const balance = await cUSD.balanceOf(deployer.address);
|
||||||
|
console.log("💰 Deployer cUSD Balance:", ethers.formatEther(balance), "\n");
|
||||||
|
|
||||||
|
// Example: Create a task
|
||||||
|
console.log("📝 Creating a task...");
|
||||||
|
const paymentAmount = ethers.parseEther("5"); // 5 cUSD
|
||||||
|
const durationInDays = 7;
|
||||||
|
|
||||||
|
// Approve TaskEscrow to spend cUSD
|
||||||
|
console.log("✅ Approving cUSD spending...");
|
||||||
|
const approveTx = await cUSD.approve(TASK_ESCROW_ADDRESS, paymentAmount);
|
||||||
|
await approveTx.wait();
|
||||||
|
console.log("✅ Approved!\n");
|
||||||
|
|
||||||
|
// Create task
|
||||||
|
const createTx = await TaskEscrow.createTask(paymentAmount, durationInDays);
|
||||||
|
const receipt = await createTx.wait();
|
||||||
|
|
||||||
|
// Get taskId from event
|
||||||
|
const event = receipt?.logs.find((log: any) => {
|
||||||
|
try {
|
||||||
|
return TaskEscrow.interface.parseLog(log)?.name === "TaskCreated";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedEvent = TaskEscrow.interface.parseLog(event as any);
|
||||||
|
const taskId = parsedEvent?.args[0];
|
||||||
|
|
||||||
|
console.log("✅ Task created! Task ID:", taskId.toString(), "\n");
|
||||||
|
|
||||||
|
// Get task details
|
||||||
|
const task = await TaskEscrow.getTask(taskId);
|
||||||
|
console.log("📋 Task Details:");
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log("Task ID:", task.taskId.toString());
|
||||||
|
console.log("Requester:", task.requester);
|
||||||
|
console.log("Payment Amount:", ethers.formatEther(task.paymentAmount), "cUSD");
|
||||||
|
console.log("Status:", task.status);
|
||||||
|
console.log("-----------------------------------\n");
|
||||||
|
|
||||||
|
// Assign worker
|
||||||
|
console.log("👷 Assigning worker...");
|
||||||
|
const assignTx = await TaskEscrow.assignWorker(taskId, worker.address);
|
||||||
|
await assignTx.wait();
|
||||||
|
console.log("✅ Worker assigned!\n");
|
||||||
|
|
||||||
|
// Approve submission (as owner)
|
||||||
|
console.log("✅ Approving submission...");
|
||||||
|
const approveTx2 = await TaskEscrow.approveSubmission(taskId);
|
||||||
|
await approveTx2.wait();
|
||||||
|
console.log("✅ Submission approved! Payment released.\n");
|
||||||
|
|
||||||
|
// Check worker balance
|
||||||
|
const workerBalance = await cUSD.balanceOf(worker.address);
|
||||||
|
console.log("💰 Worker cUSD Balance:", ethers.formatEther(workerBalance), "\n");
|
||||||
|
|
||||||
|
console.log("✨ Interaction completed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("❌ Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
129
dmtp/server/seedTasks.ts
Normal file
129
dmtp/server/seedTasks.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
import { TaskType } from './src/types/database.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds multiple tasks with blockchain integration.
|
||||||
|
*/
|
||||||
|
async function seedTasks() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Starting blockchain task seeding...\n');
|
||||||
|
|
||||||
|
const requesterWallet = '0xA0e793E7257c065b30c46Ef6828F2B3C0de87A8E';
|
||||||
|
|
||||||
|
// Find or create requester
|
||||||
|
let requester = await prisma.user.findUnique({
|
||||||
|
where: { walletAddress: requesterWallet.toLowerCase() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!requester) {
|
||||||
|
console.log('📝 Creating user...');
|
||||||
|
requester = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
walletAddress: requesterWallet.toLowerCase(),
|
||||||
|
reputationScore: 100,
|
||||||
|
role: 'worker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ User created: ${requester.id}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ User found: ${requester.id}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define tasks to seed
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
title: 'Grammar Check - Business Email',
|
||||||
|
description: 'Review and verify that the business email text is grammatically correct and uses proper English.',
|
||||||
|
taskType: TaskType.TEXT_VERIFICATION,
|
||||||
|
paymentAmount: 0.01,
|
||||||
|
verificationCriteria: {
|
||||||
|
aiPrompt: 'Check if the text has correct grammar, proper spelling, and uses professional English language.',
|
||||||
|
requiredFields: ['text'],
|
||||||
|
},
|
||||||
|
maxSubmissions: 3,
|
||||||
|
expiresAt: new Date('2025-11-05T00:00:00Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'English Text Verification - Blog Post',
|
||||||
|
description: 'Verify that the blog post content follows proper English grammar rules and sentence structure.',
|
||||||
|
taskType: TaskType.TEXT_VERIFICATION,
|
||||||
|
paymentAmount: 0.015,
|
||||||
|
verificationCriteria: {
|
||||||
|
aiPrompt: 'Verify the text has correct grammar, punctuation, sentence structure, and uses proper English.',
|
||||||
|
requiredFields: ['text'],
|
||||||
|
},
|
||||||
|
maxSubmissions: 2,
|
||||||
|
expiresAt: new Date('2025-11-08T00:00:00Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Grammar Correction - Product Description',
|
||||||
|
description: 'Check product description text for grammar errors and ensure it uses clear, proper English.',
|
||||||
|
taskType: TaskType.TEXT_VERIFICATION,
|
||||||
|
paymentAmount: 0.02,
|
||||||
|
verificationCriteria: {
|
||||||
|
aiPrompt: 'Ensure the text is grammatically correct, has proper punctuation, and uses clear professional English.',
|
||||||
|
requiredFields: ['text'],
|
||||||
|
},
|
||||||
|
maxSubmissions: 5,
|
||||||
|
expiresAt: new Date('2025-11-12T00:00:00Z'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const taskData of tasks) {
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log(`🧩 Creating task: ${taskData.title}`);
|
||||||
|
console.log(`💰 Payment: ${taskData.paymentAmount} cUSD`);
|
||||||
|
console.log(`⏰ Expires: ${taskData.expiresAt}\n`);
|
||||||
|
|
||||||
|
const durationMs = taskData.expiresAt.getTime() - Date.now();
|
||||||
|
const durationInDays = Math.ceil(durationMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
console.log('⛓️ Creating task on blockchain...');
|
||||||
|
const blockchainResult = await blockchainService.createTask(
|
||||||
|
taskData.paymentAmount.toString(),
|
||||||
|
durationInDays
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Blockchain task created!`);
|
||||||
|
console.log(` Contract Task ID: ${blockchainResult.taskId}`);
|
||||||
|
console.log(` Tx Hash: ${blockchainResult.txHash}\n`);
|
||||||
|
|
||||||
|
const dbTask = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
requesterId: requester.id,
|
||||||
|
title: taskData.title,
|
||||||
|
description: taskData.description,
|
||||||
|
taskType: taskData.taskType,
|
||||||
|
paymentAmount: taskData.paymentAmount,
|
||||||
|
verificationCriteria: taskData.verificationCriteria,
|
||||||
|
maxSubmissions: taskData.maxSubmissions,
|
||||||
|
expiresAt: taskData.expiresAt,
|
||||||
|
contractTaskId: blockchainResult.taskId,
|
||||||
|
status: 'open',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Task stored in DB! ID: ${dbTask.id}`);
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user stats
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: requester.id },
|
||||||
|
data: {
|
||||||
|
totalTasksCreated: { increment: tasks.length },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ All tasks successfully created with blockchain integration!\n');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('\n❌ Error seeding tasks:', error.message);
|
||||||
|
console.error('Stack:', error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedTasks();
|
||||||
99
dmtp/server/show-env-info.ts
Normal file
99
dmtp/server/show-env-info.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { blockchainService } from './src/services/blockchain.service';
|
||||||
|
import { prisma } from './src/database/connections';
|
||||||
|
|
||||||
|
async function showEnvironmentInfo() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Environment & Contract Information\n');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
|
console.log('\n📁 Working Directory:');
|
||||||
|
console.log(` ${process.cwd()}\n`);
|
||||||
|
|
||||||
|
console.log('🌐 Network Configuration:');
|
||||||
|
console.log(` RPC URL: ${process.env.CELO_RPC_URL || 'https://forno.celo-sepolia.celo-testnet.org'}`);
|
||||||
|
console.log(` Chain ID: ${process.env.CHAIN_ID || '11142220'}\n`);
|
||||||
|
|
||||||
|
console.log('📋 Contract Addresses:');
|
||||||
|
const contractAddress = process.env.CONTRACT_ADDRESS;
|
||||||
|
const cUSDAddress = process.env.CUSD_SEPOLIA_ADDRESS;
|
||||||
|
console.log(` TaskEscrow: ${contractAddress || 'NOT SET'}`);
|
||||||
|
console.log(` cUSD Token: ${cUSDAddress || 'NOT SET'}\n`);
|
||||||
|
|
||||||
|
if (!contractAddress) {
|
||||||
|
console.log('❌ CONTRACT_ADDRESS not configured in .env!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⛓️ Blockchain Service:');
|
||||||
|
const actualContract = blockchainService.getContractAddress();
|
||||||
|
console.log(` Connected to: ${actualContract}`);
|
||||||
|
console.log(` Match: ${actualContract.toLowerCase() === contractAddress.toLowerCase() ? '✅' : '❌'}\n`);
|
||||||
|
|
||||||
|
const taskCounter = await blockchainService.getTaskCounter();
|
||||||
|
console.log(` Total tasks on blockchain: ${taskCounter}\n`);
|
||||||
|
|
||||||
|
console.log('💾 Database Statistics:');
|
||||||
|
const totalTasks = await prisma.task.count();
|
||||||
|
const tasksWithContract = await prisma.task.count({
|
||||||
|
where: { contractTaskId: { not: null } },
|
||||||
|
});
|
||||||
|
const activeTasks = await prisma.task.count({
|
||||||
|
where: {
|
||||||
|
status: { in: ['open', 'in_progress'] },
|
||||||
|
contractTaskId: { not: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` Total tasks in DB: ${totalTasks}`);
|
||||||
|
console.log(` Tasks with blockchain: ${tasksWithContract}`);
|
||||||
|
console.log(` Active blockchain tasks: ${activeTasks}\n`);
|
||||||
|
|
||||||
|
if (activeTasks > 0) {
|
||||||
|
console.log('📝 Active Tasks:');
|
||||||
|
const tasks = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
status: { in: ['open', 'in_progress'] },
|
||||||
|
contractTaskId: { not: null },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
contractTaskId: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
take: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
console.log(`\n Task: ${task.title.substring(0, 50)}...`);
|
||||||
|
console.log(` DB ID: ${task.id}`);
|
||||||
|
console.log(` Contract ID: ${task.contractTaskId}`);
|
||||||
|
console.log(` Status: ${task.status}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blockchainTask = await blockchainService.getTask(task.contractTaskId!);
|
||||||
|
if (blockchainTask) {
|
||||||
|
const statuses = ['Open', 'InProgress', 'Completed', 'Cancelled', 'Expired'];
|
||||||
|
console.log(` Blockchain: ✅ ${statuses[blockchainTask.status]}`);
|
||||||
|
} else {
|
||||||
|
console.log(` Blockchain: ❌ NOT FOUND`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(` Blockchain: ❌ Error - ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('\n✅ Environment check complete!\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('\n❌ Error:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showEnvironmentInfo();
|
||||||
64
dmtp/server/src/app.ts
Normal file
64
dmtp/server/src/app.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import cors from 'cors';
|
||||||
|
import express, { Application } from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import { createQueueDashboard } from './dashboard';
|
||||||
|
import { testConnection } from './database/connections';
|
||||||
|
import { ErrorMiddleware } from './middlewares/error.middleware';
|
||||||
|
import routes from './routes';
|
||||||
|
import './workers'; // Start verification worker
|
||||||
|
|
||||||
|
export function createApp(): Application {
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: [
|
||||||
|
process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||||
|
'http://localhost:4000', // Admin dashboard
|
||||||
|
],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
|
allowedHeaders: [
|
||||||
|
'Content-Type',
|
||||||
|
'Authorization',
|
||||||
|
'X-Wallet-Address',
|
||||||
|
'X-Signature',
|
||||||
|
'X-Message',
|
||||||
|
'X-Timestamp',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Body parsing middleware
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Logging middleware
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/admin/queues', createQueueDashboard());
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.use('/api/v1', routes);
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use(ErrorMiddleware.notFound);
|
||||||
|
|
||||||
|
// Global error handler
|
||||||
|
app.use(ErrorMiddleware.handle);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test database connection on startup
|
||||||
|
testConnection().catch((error) => {
|
||||||
|
console.error('Failed to connect to database:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
73
dmtp/server/src/config/ai.config.js
Normal file
73
dmtp/server/src/config/ai.config.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.PROMPTS = exports.aiConfig = void 0;
|
||||||
|
exports.aiConfig = {
|
||||||
|
apiKey: process.env.GEMINI_API_KEY || 'AIzaSyBUjw754YS4sZZEWNYk9z2sC30YfiwubQI',
|
||||||
|
model: process.env.GEMINI_MODEL || 'gemini-2.5-flash',
|
||||||
|
temperature: 0, // Consistent results
|
||||||
|
maxOutputTokens: 1024,
|
||||||
|
cache: {
|
||||||
|
enabled: process.env.REDIS_URL ? true : false,
|
||||||
|
ttl: 3600, // 1 hour cache
|
||||||
|
},
|
||||||
|
retry: {
|
||||||
|
maxRetries: 3,
|
||||||
|
initialDelay: 1000, // 1 second
|
||||||
|
maxDelay: 10000, // 10 seconds
|
||||||
|
backoffMultiplier: 2,
|
||||||
|
},
|
||||||
|
rateLimit: {
|
||||||
|
maxRequests: 60, // 60 requests per minute
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Prompt templates
|
||||||
|
exports.PROMPTS = {
|
||||||
|
TEXT_VERIFICATION: `You are a task verification assistant. Analyze the following submission against the criteria.
|
||||||
|
|
||||||
|
VERIFICATION CRITERIA:
|
||||||
|
{verificationCriteria}
|
||||||
|
|
||||||
|
USER SUBMISSION:
|
||||||
|
{submissionText}
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
1. Check if the submission meets ALL criteria
|
||||||
|
2. Provide a verification score (0-100)
|
||||||
|
3. List any violations or issues
|
||||||
|
4. Give approval recommendation (APPROVE/REJECT)
|
||||||
|
|
||||||
|
OUTPUT FORMAT (JSON only):
|
||||||
|
{
|
||||||
|
"approved": boolean,
|
||||||
|
"score": number,
|
||||||
|
"violations": string[],
|
||||||
|
"reasoning": string
|
||||||
|
}
|
||||||
|
|
||||||
|
Be strict but fair. Only approve submissions that clearly meet criteria. Return ONLY valid JSON, no markdown or additional text.`,
|
||||||
|
IMAGE_VERIFICATION: `You are an image verification expert. Analyze this image against the task requirements.
|
||||||
|
|
||||||
|
TASK DESCRIPTION:
|
||||||
|
{taskDescription}
|
||||||
|
|
||||||
|
VERIFICATION CRITERIA:
|
||||||
|
{verificationCriteria}
|
||||||
|
|
||||||
|
Analyze the image and determine:
|
||||||
|
1. Does it match the task requirements?
|
||||||
|
2. Is the image quality acceptable (not blurry, proper lighting)?
|
||||||
|
3. Are there any inappropriate or irrelevant elements?
|
||||||
|
4. Quality score (0-100)
|
||||||
|
|
||||||
|
OUTPUT FORMAT (JSON only):
|
||||||
|
{
|
||||||
|
"approved": boolean,
|
||||||
|
"score": number,
|
||||||
|
"image_quality": "excellent" | "good" | "poor",
|
||||||
|
"issues": string[],
|
||||||
|
"reasoning": string
|
||||||
|
}
|
||||||
|
|
||||||
|
Return ONLY valid JSON, no markdown or additional text.`,
|
||||||
|
};
|
||||||
81
dmtp/server/src/config/ai.config.ts
Normal file
81
dmtp/server/src/config/ai.config.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { AIServiceConfig } from '../types/ai.types';
|
||||||
|
|
||||||
|
export const aiConfig: AIServiceConfig = {
|
||||||
|
apiKey: process.env.GEMINI_API_KEY || 'AIzaSyBUjw754YS4sZZEWNYk9z2sC30YfiwubQI',
|
||||||
|
model: process.env.GEMINI_MODEL || 'gemini-2.5-flash',
|
||||||
|
temperature: 0, // Consistent results
|
||||||
|
maxOutputTokens: 1024,
|
||||||
|
|
||||||
|
cache: {
|
||||||
|
enabled: process.env.REDIS_URL ? true : false,
|
||||||
|
ttl: 3600, // 1 hour cache
|
||||||
|
},
|
||||||
|
|
||||||
|
retry: {
|
||||||
|
maxRetries: 3,
|
||||||
|
initialDelay: 1000, // 1 second
|
||||||
|
maxDelay: 10000, // 10 seconds
|
||||||
|
backoffMultiplier: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
rateLimit: {
|
||||||
|
maxRequests: 60, // 60 requests per minute
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prompt templates
|
||||||
|
export const PROMPTS = {
|
||||||
|
TEXT_VERIFICATION: `You are a task verification assistant. Analyze the following submission against the criteria.
|
||||||
|
|
||||||
|
VERIFICATION CRITERIA:
|
||||||
|
{verificationCriteria}
|
||||||
|
|
||||||
|
USER SUBMISSION:
|
||||||
|
{submissionText}
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
1. Check if the submission meets ALL criteria
|
||||||
|
2. Provide a verification score (0-100)
|
||||||
|
3. List any violations or issues
|
||||||
|
4. Give approval recommendation (APPROVE/REJECT)
|
||||||
|
|
||||||
|
CRITICAL: Respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations.
|
||||||
|
|
||||||
|
Required JSON format:
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"score": 85,
|
||||||
|
"violations": ["issue 1", "issue 2"],
|
||||||
|
"reasoning": "Brief explanation"
|
||||||
|
}
|
||||||
|
|
||||||
|
Your response:`,
|
||||||
|
|
||||||
|
IMAGE_VERIFICATION: `You are an image verification expert. Analyze this image against the task requirements.
|
||||||
|
|
||||||
|
TASK DESCRIPTION:
|
||||||
|
{taskDescription}
|
||||||
|
|
||||||
|
VERIFICATION CRITERIA:
|
||||||
|
{verificationCriteria}
|
||||||
|
|
||||||
|
Analyze the image and determine:
|
||||||
|
1. Does it match the task requirements?
|
||||||
|
2. Is the image quality acceptable (not blurry, proper lighting)?
|
||||||
|
3. Are there any inappropriate or irrelevant elements?
|
||||||
|
4. Quality score (0-100)
|
||||||
|
|
||||||
|
CRITICAL: Respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations.
|
||||||
|
|
||||||
|
Required JSON format:
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"score": 90,
|
||||||
|
"image_quality": "excellent",
|
||||||
|
"issues": ["issue 1"],
|
||||||
|
"reasoning": "Brief explanation"
|
||||||
|
}
|
||||||
|
|
||||||
|
Your response:`,
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user