From 5c9c1d2f76fee5134197742cd1c79494d33fba48 Mon Sep 17 00:00:00 2001 From: fanhousanbu Date: Thu, 2 Apr 2026 10:18:04 +0800 Subject: [PATCH 1/8] feat(tasks): add MyTask M6-M8 task market UI Integrate MyTask task market into YAA frontend under /tasks route. - Add TaskContext with contract read/write (createTask, acceptTask, submitWork, approveWork, finalizeTask, cancelTask) - Add task list page with All/Open/Mine/Claimed filter tabs and search - Add create task form with ERC-20 approve + createTask two-step flow - Add task detail page with role-based action buttons (Community/Taskor) - Add TaskEscrowV2 ABI and contract config (viem, env-driven chain) - Add task-types.ts (TaskStatus enum, ParsedTask), date-utils.ts - Inject TaskProvider in app layout; add Tasks nav (desktop + mobile) - Upgrade tsconfig target to ES2020 (BigInt literal support) --- aastar-frontend/app/layout.tsx | 7 +- aastar-frontend/app/tasks/[taskId]/page.tsx | 401 ++++++++++++++++ aastar-frontend/app/tasks/create/page.tsx | 297 ++++++++++++ aastar-frontend/app/tasks/page.tsx | 267 +++++++++++ aastar-frontend/components/Layout.tsx | 22 + aastar-frontend/contexts/TaskContext.tsx | 443 ++++++++++++++++++ aastar-frontend/lib/contracts/task-config.ts | 91 ++++ .../lib/contracts/task-escrow-abi.ts | 243 ++++++++++ aastar-frontend/lib/date-utils.ts | 39 ++ aastar-frontend/lib/task-types.ts | 106 +++++ aastar-frontend/package.json | 3 +- aastar-frontend/tsconfig.json | 2 +- package-lock.json | 275 ++++++++++- 13 files changed, 2190 insertions(+), 6 deletions(-) create mode 100644 aastar-frontend/app/tasks/[taskId]/page.tsx create mode 100644 aastar-frontend/app/tasks/create/page.tsx create mode 100644 aastar-frontend/app/tasks/page.tsx create mode 100644 aastar-frontend/contexts/TaskContext.tsx create mode 100644 aastar-frontend/lib/contracts/task-config.ts create mode 100644 aastar-frontend/lib/contracts/task-escrow-abi.ts create mode 100644 aastar-frontend/lib/date-utils.ts create mode 100644 aastar-frontend/lib/task-types.ts diff --git a/aastar-frontend/app/layout.tsx b/aastar-frontend/app/layout.tsx index 1f5b1d7b..b75bb083 100644 --- a/aastar-frontend/app/layout.tsx +++ b/aastar-frontend/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { Toaster } from "react-hot-toast"; import { ThemeProvider } from "@/lib/theme"; import { DashboardProvider } from "@/contexts/DashboardContext"; +import { TaskProvider } from "@/contexts/TaskContext"; const inter = Inter({ subsets: ["latin"] }); @@ -47,8 +48,10 @@ export default function RootLayout({ - {children} - + + {children} + + diff --git a/aastar-frontend/app/tasks/[taskId]/page.tsx b/aastar-frontend/app/tasks/[taskId]/page.tsx new file mode 100644 index 00000000..07aeb8fd --- /dev/null +++ b/aastar-frontend/app/tasks/[taskId]/page.tsx @@ -0,0 +1,401 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter, useParams } from "next/navigation"; +import Layout from "@/components/Layout"; +import { useTask } from "@/contexts/TaskContext"; +import { useDashboard } from "@/contexts/DashboardContext"; +import { getStoredAuth } from "@/lib/auth"; +import { + type ParsedTask, + TaskStatus, + TASK_STATUS_COLORS, +} from "@/lib/task-types"; +import { DEFAULT_REWARD_TOKEN_SYMBOL } from "@/lib/contracts/task-config"; +import { + ArrowLeftIcon, + UserIcon, + CurrencyDollarIcon, + ClockIcon, + CheckCircleIcon, + ExclamationTriangleIcon, +} from "@heroicons/react/24/outline"; +import { formatDate, formatDateTime } from "@/lib/date-utils"; +import toast from "react-hot-toast"; +import type { WalletClient } from "viem"; + +function AddressRow({ label, addr }: { label: string; addr: string }) { + if (!addr || addr === "0x0000000000000000000000000000000000000000") return null; + return ( +
+ {label} + + {addr.slice(0, 8)}…{addr.slice(-6)} + +
+ ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} + +export default function TaskDetailPage() { + const router = useRouter(); + const { taskId } = useParams<{ taskId: string }>(); + const { getTask, acceptTask, submitWork, approveWork, finalizeTask, cancelTask } = useTask(); + const { data } = useDashboard(); + + const [task, setTask] = useState(null); + const [loading, setLoading] = useState(true); + const [walletClient, setWalletClient] = useState(null); + const [actionLoading, setActionLoading] = useState(false); + const [evidenceUri, setEvidenceUri] = useState(""); + const [showEvidenceForm, setShowEvidenceForm] = useState(false); + + const myAddress = data.account?.address?.toLowerCase() ?? ""; + const isCommunity = task?.community.toLowerCase() === myAddress; + const isTaskor = task?.taskor.toLowerCase() === myAddress; + const isZeroAddress = (addr: string) => + addr === "0x0000000000000000000000000000000000000000"; + + useEffect(() => { + const { token } = getStoredAuth(); + if (!token) router.push("/auth/login"); + }, [router]); + + useEffect(() => { + async function loadWallet() { + if (typeof window === "undefined") return; + const { createWalletClient, custom } = await import("viem"); + const { SUPPORTED_CHAIN } = await import("@/lib/contracts/task-config"); + const provider = (window as Window & { ethereum?: unknown }).ethereum; + if (!provider) return; + setWalletClient( + createWalletClient({ + chain: SUPPORTED_CHAIN, + transport: custom(provider as Parameters[0]), + }) + ); + } + loadWallet(); + }, []); + + useEffect(() => { + if (!taskId) return; + setLoading(true); + getTask(taskId) + .then((t) => setTask(t)) + .finally(() => setLoading(false)); + }, [taskId, getTask]); + + const refresh = async () => { + if (!taskId) return; + const t = await getTask(taskId); + setTask(t); + }; + + async function runAction(fn: () => Promise, successMsg: string) { + if (!walletClient) { + toast.error("No wallet connected"); + return; + } + setActionLoading(true); + const toastId = toast.loading("Sending transaction..."); + try { + const ok = await fn(); + toast.dismiss(toastId); + if (ok) { + toast.success(successMsg); + await refresh(); + } else { + toast.error("Transaction failed"); + } + } catch (err) { + toast.dismiss(toastId); + toast.error(err instanceof Error ? err.message : "Error"); + } finally { + setActionLoading(false); + } + } + + function getTitle(uri: string): string { + try { + return JSON.parse(uri).title ?? "Untitled Task"; + } catch { + return uri.slice(0, 60) || "Untitled Task"; + } + } + + function getDescription(uri: string): string { + try { + return JSON.parse(uri).description ?? ""; + } catch { + return uri; + } + } + + if (loading) { + return ( + +
+
+
+ + ); + } + + if (!task) { + return ( + +
+

Task not found

+ +
+
+ ); + } + + const isOpen = task.status === TaskStatus.Open; + const isAccepted = task.status === TaskStatus.Accepted || task.status === TaskStatus.InProgress; + const isSubmitted = task.status === TaskStatus.Submitted; + const isFinalized = task.status === TaskStatus.Finalized; + const isRefunded = task.status === TaskStatus.Refunded; + + return ( + +
+ {/* Header */} +
+ +
+
+

+ {getTitle(task.metadataUri)} +

+ + {task.statusLabel} + +
+

+ {task.taskTypeLabel} · Posted {formatDate(task.createdAt)} +

+
+
+ + {/* Description */} +
+

+ {getDescription(task.metadataUri) || "No description provided."} +

+
+ + {/* Reward & Deadline */} +
+
+
+
+ + Reward +
+ + {task.rewardFormatted} {DEFAULT_REWARD_TOKEN_SYMBOL} + +
+
+
+ + Deadline +
+ + {formatDateTime(task.deadline)} + {task.isExpired && " (expired)"} + +
+ {task.challengeDeadline && ( +
+
+ + Challenge period ends +
+ + {formatDateTime(task.challengeDeadline)} + +
+ )} +
+
+ + {/* Participants */} +
+ + {!isZeroAddress(task.taskor) && ( + + )} + {!isZeroAddress(task.supplier) && ( + + )} +
+ + {/* Evidence (if submitted) */} + {task.evidenceUri && ( +
+

+ {task.evidenceUri} +

+
+ )} + + {/* Actions */} +
+ {/* Claim task (Open → Accepted) */} + {isOpen && !isCommunity && !task.isExpired && ( + + )} + + {/* Submit evidence (Accepted → Submitted) */} + {isAccepted && isTaskor && ( + <> + {!showEvidenceForm ? ( + + ) : ( +
+

+ Submit your evidence +

+