diff --git a/app/api/bounties/route.ts b/app/api/bounties/route.ts index 20a5912..b79483a 100644 --- a/app/api/bounties/route.ts +++ b/app/api/bounties/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { getAllBounties } from '@/lib/mock-bounty'; +import { BountyLogic } from '@/lib/logic/bounty-logic'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); @@ -7,7 +8,7 @@ export async function GET(request: Request) { // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)); - const allBounties = getAllBounties(); + const allBounties = getAllBounties().map(b => BountyLogic.processBountyStatus(b)); // Apply filters from params let filtered = allBounties; @@ -28,7 +29,7 @@ export async function GET(request: Request) { if (tags) { const tagArray = tags.split(',').filter(Boolean); if (tagArray.length > 0) { - filtered = filtered.filter(b => + filtered = filtered.filter(b => tagArray.some(tag => b.tags.includes(tag)) ); } diff --git a/app/bounty/[id]/page.tsx b/app/bounty/[id]/page.tsx index 0a558c4..f8f0974 100644 --- a/app/bounty/[id]/page.tsx +++ b/app/bounty/[id]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from "next/navigation" import { Metadata } from "next" import { getBountyById } from "@/lib/mock-bounty" import { truncateAtWordBoundary } from "@/lib/truncate" +import { BountyLogic } from "@/lib/logic/bounty-logic" import { BountyHeader } from "@/components/bounty/bounty-header" import { BountyContent } from "@/components/bounty/bounty-content" import { BountySidebar } from "@/components/bounty/bounty-sidebar" @@ -27,7 +28,11 @@ export async function generateMetadata({ params }: BountyPageProps): Promise { @@ -117,14 +118,14 @@ export default function DiscoverPage() { // Apply sort (only valid sorts for projects) switch (filters.sort) { case "newest": - result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case "recentlyUpdated": - result.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + result.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); break; default: // Fallback to newest for unsupported sort values (e.g. "highestReward") - result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; } @@ -133,7 +134,8 @@ export default function DiscoverPage() { // Filter and sort bounties const filteredBounties = useCallback(() => { - let result = [...mockBounties]; + // Process bounties for dynamic status (e.g. expiration) + let result = rawMockBounties.map(b => BountyLogic.processBountyStatus(b)); // Apply search filter if (filters.search) { @@ -156,10 +158,10 @@ export default function DiscoverPage() { // Apply sort switch (filters.sort) { case "newest": - result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case "recentlyUpdated": - result.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + result.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); break; case "highestReward": result.sort((a, b) => b.reward - a.reward); @@ -212,7 +214,7 @@ export default function DiscoverPage() { Bounties - ({mockBounties.length}) + ({rawMockBounties.length}) diff --git a/lib/api/bounties.ts b/lib/api/bounties.ts index 12caacf..0f9f801 100644 --- a/lib/api/bounties.ts +++ b/lib/api/bounties.ts @@ -27,6 +27,12 @@ export const bountySchema = z.object({ status: bountyStatusSchema, createdAt: z.string(), updatedAt: z.string(), + claimedAt: z.string().optional(), + claimedBy: z.string().optional(), + lastActivityAt: z.string().optional(), + claimExpiresAt: z.string().optional(), + submissionsEndDate: z.string().optional(), + requirements: z.array(z.string()).optional(), scope: z.string().optional(), }); diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts new file mode 100644 index 0000000..8628288 --- /dev/null +++ b/lib/logic/bounty-logic.ts @@ -0,0 +1,105 @@ +import { differenceInDays, isPast, parseISO, isValid } from 'date-fns'; + + +/** + * Interface defining the minimal fields required for status logic. + * Compatible with both `types/bounty.ts` and `lib/types.ts`. + */ +export interface StatusAwareBounty { + status: string; + claimingModel: string; + claimExpiresAt?: string; + lastActivityAt?: string; + claimedBy?: string; + claimedAt?: string; +} + +export class BountyLogic { + /** + * Configuration for inactivity thresholds (in days) + */ + static readonly INACTIVITY_THRESHOLD_DAYS = 7; + + /** + * Processes the bounty status based on its model and timestamps. + * - Checks for inactivity auto-release for single-claim. + * - Checks for expired claims. + * - Returns the potentially modified bounty (this simulates the backend update). + */ + static processBountyStatus(bounty: T): T { + if (bounty.status !== 'claimed' && bounty.status !== 'open') return bounty; + + const now = new Date(); + // Shallow copy works for pure property updates + const newBounty = { ...bounty }; + + // Anti-squatting: Check inactivity for single-claim + if ( + bounty.claimingModel === 'single-claim' && + bounty.status === 'claimed' + ) { + // Helper to get Date object safely + const getDate = (val?: string) => { + if (!val) return null; + const date = parseISO(val); + return isValid(date) ? date : null; + }; + + const expiresAt = getDate(bounty.claimExpiresAt); + + // If claim expired + if (expiresAt && isPast(expiresAt)) { + // Auto-release + newBounty.status = 'open'; + newBounty.claimedBy = undefined; + newBounty.claimedAt = undefined; + newBounty.claimExpiresAt = undefined; + newBounty.lastActivityAt = undefined; + } + + // If inactive for too long + const lastActive = getDate(bounty.lastActivityAt) || getDate(bounty.claimedAt); + if (lastActive) { + const daysInactive = differenceInDays(now, lastActive); + if (daysInactive > this.INACTIVITY_THRESHOLD_DAYS) { + // Auto-release due to inactivity + newBounty.status = 'open'; + newBounty.claimedBy = undefined; + newBounty.claimedAt = undefined; + newBounty.claimExpiresAt = undefined; + newBounty.lastActivityAt = undefined; + } + } + } + + return newBounty; + } + + /** + * Returns metadata about the claim status suitable for UI display. + */ + static getClaimStatusDisplay(bounty: StatusAwareBounty) { + if (bounty.status === 'open') return { label: 'Available', color: 'green' }; + + if (bounty.status === 'claimed') { + if (bounty.claimingModel === 'single-claim') { + return { + label: 'Claimed', + color: 'orange', + details: bounty.claimExpiresAt ? `Expires ${BountyLogic.formatDate(bounty.claimExpiresAt)}` : 'In Progress' + }; + } + if (bounty.claimingModel === 'application') { + return { label: 'Applications Open', color: 'blue' }; + } + } + + return { label: bounty.status, color: 'gray' }; + } + + private static formatDate(dateStr: string) { + const date = parseISO(dateStr); + if (!isValid(date)) return 'Invalid Date'; + return date.toLocaleDateString(); + } +} diff --git a/lib/mock-bounty.ts b/lib/mock-bounty.ts index 57d4a18..d43dcca 100644 --- a/lib/mock-bounty.ts +++ b/lib/mock-bounty.ts @@ -71,6 +71,9 @@ Add a dark mode toggle to the application settings page that persists user prefe difficulty: "beginner", tags: ["ui", "theme", "settings", "dark-mode"], status: "claimed", + claimedBy: "dev_user_123", + claimedAt: "2024-01-01T00:00:00Z", + claimExpiresAt: "2024-01-15T00:00:00Z", // Expired createdAt: "2025-01-10T08:00:00Z", updatedAt: "2025-01-17T11:00:00Z", }, diff --git a/lib/mock-data.ts b/lib/mock-data.ts index 534003c..8af561c 100644 --- a/lib/mock-data.ts +++ b/lib/mock-data.ts @@ -9,8 +9,8 @@ export const mockProjects: Project[] = [ "A comprehensive dashboard for tracking DeFi protocols on Stellar network. Features real-time analytics, portfolio tracking, and yield optimization.", tags: ["DeFi", "Frontend", "Analytics", "Stellar"], status: "active", - createdAt: new Date("2024-01-15"), - updatedAt: new Date("2024-01-20"), + createdAt: "2024-01-15T00:00:00Z", + updatedAt: "2024-01-20T00:00:00Z", creator: "stellar_dev", category: "DeFi", milestones: 5, @@ -23,8 +23,8 @@ export const mockProjects: Project[] = [ "Decentralized NFT marketplace built on Stellar. Supports minting, trading, and royalty management with low transaction fees.", tags: ["NFT", "Smart Contracts", "Full Stack", "Stellar"], status: "active", - createdAt: new Date("2024-01-10"), - updatedAt: new Date("2024-01-22"), + createdAt: "2024-01-10T00:00:00Z", + updatedAt: "2024-01-22T00:00:00Z", creator: "nft_builder", category: "NFT", milestones: 8, @@ -37,8 +37,8 @@ export const mockProjects: Project[] = [ "Secure bridge protocol enabling asset transfers between Stellar and other major blockchains.", tags: ["DeFi", "Smart Contracts", "Security", "Infrastructure"], status: "active", - createdAt: new Date("2024-01-05"), - updatedAt: new Date("2024-01-18"), + createdAt: "2024-01-05T00:00:00Z", + updatedAt: "2024-01-18T00:00:00Z", creator: "bridge_team", category: "Infrastructure", milestones: 6, @@ -51,8 +51,8 @@ export const mockProjects: Project[] = [ "User-friendly mobile wallet for Stellar assets with built-in DEX integration and staking features.", tags: ["Mobile", "Frontend", "Web3", "Stellar"], status: "active", - createdAt: new Date("2023-12-20"), - updatedAt: new Date("2024-01-21"), + createdAt: "2023-12-20T00:00:00Z", + updatedAt: "2024-01-21T00:00:00Z", creator: "mobile_dev", category: "Wallet", milestones: 10, @@ -65,8 +65,8 @@ export const mockProjects: Project[] = [ "Decentralized governance platform for DAOs on Stellar with voting mechanisms and proposal management.", tags: ["Smart Contracts", "Frontend", "Backend", "Web3"], status: "completed", - createdAt: new Date("2023-11-01"), - updatedAt: new Date("2023-12-15"), + createdAt: "2023-11-01T00:00:00Z", + updatedAt: "2023-12-15T00:00:00Z", creator: "dao_builders", category: "Governance", milestones: 4, @@ -79,8 +79,8 @@ export const mockProjects: Project[] = [ "Advanced analytics engine for Stellar blockchain data with customizable dashboards and alerts.", tags: ["Analytics", "Backend", "Infrastructure", "Stellar"], status: "active", - createdAt: new Date("2024-01-12"), - updatedAt: new Date("2024-01-19"), + createdAt: "2024-01-12T00:00:00Z", + updatedAt: "2024-01-19T00:00:00Z", creator: "analytics_pro", category: "Analytics", milestones: 7, @@ -93,8 +93,8 @@ export const mockProjects: Project[] = [ "Comprehensive testing framework for Stellar smart contracts with automated security audits.", tags: ["Testing", "Security", "Smart Contracts", "Infrastructure"], status: "paused", - createdAt: new Date("2023-12-01"), - updatedAt: new Date("2024-01-10"), + createdAt: "2023-12-01T00:00:00Z", + updatedAt: "2024-01-10T00:00:00Z", creator: "test_master", category: "Development Tools", milestones: 5, @@ -107,8 +107,8 @@ export const mockProjects: Project[] = [ "Self-sovereign identity solution on Stellar for secure credential management and verification.", tags: ["Security", "Smart Contracts", "Backend", "Web3"], status: "active", - createdAt: new Date("2024-01-08"), - updatedAt: new Date("2024-01-22"), + createdAt: "2024-01-08T00:00:00Z", + updatedAt: "2024-01-22T00:00:00Z", creator: "identity_dev", category: "Identity", milestones: 6, @@ -121,8 +121,8 @@ export const mockProjects: Project[] = [ "Comprehensive documentation platform with interactive tutorials and code examples for Stellar developers.", tags: ["Documentation", "Frontend", "Design"], status: "completed", - createdAt: new Date("2023-10-15"), - updatedAt: new Date("2023-12-01"), + createdAt: "2023-10-15T00:00:00Z", + updatedAt: "2023-12-01T00:00:00Z", creator: "docs_team", category: "Education", milestones: 3, @@ -135,8 +135,8 @@ export const mockProjects: Project[] = [ "Automated yield optimization protocol that finds the best returns across Stellar DeFi platforms.", tags: ["DeFi", "Smart Contracts", "Backend", "Analytics"], status: "active", - createdAt: new Date("2024-01-14"), - updatedAt: new Date("2024-01-21"), + createdAt: "2024-01-14T00:00:00Z", + updatedAt: "2024-01-21T00:00:00Z", creator: "yield_hunter", category: "DeFi", milestones: 8, @@ -156,11 +156,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "application", status: "open", - createdAt: new Date("2024-01-20"), - updatedAt: new Date("2024-01-20"), + createdAt: "2024-01-20T00:00:00Z", + updatedAt: "2024-01-20T00:00:00Z", creator: "wallet_project", difficulty: "advanced", - deadline: new Date("2024-02-20"), + deadline: "2024-02-20T00:00:00Z", }, { id: "bounty-2", @@ -172,11 +172,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "competition", status: "open", - createdAt: new Date("2024-01-19"), - updatedAt: new Date("2024-01-21"), + createdAt: "2024-01-19T00:00:00Z", + updatedAt: "2024-01-21T00:00:00Z", creator: "defi_startup", difficulty: "intermediate", - deadline: new Date("2024-02-10"), + deadline: "2024-02-10T00:00:00Z", }, { id: "bounty-3", @@ -188,11 +188,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "in-progress", - createdAt: new Date("2024-01-18"), - updatedAt: new Date("2024-01-22"), + createdAt: "2024-01-18T00:00:00Z", + updatedAt: "2024-01-22T00:00:00Z", creator: "security_team", difficulty: "advanced", - deadline: new Date("2024-02-05"), + deadline: "2024-02-05T00:00:00Z", }, { id: "bounty-4", @@ -204,11 +204,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "multi-winner", status: "open", - createdAt: new Date("2024-01-17"), - updatedAt: new Date("2024-01-17"), + createdAt: "2024-01-17T00:00:00Z", + updatedAt: "2024-01-17T00:00:00Z", creator: "mobile_team", difficulty: "intermediate", - deadline: new Date("2024-02-15"), + deadline: "2024-02-15T00:00:00Z", }, { id: "bounty-5", @@ -220,11 +220,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "open", - createdAt: new Date("2024-01-16"), - updatedAt: new Date("2024-01-20"), + createdAt: "2024-01-16T00:00:00Z", + updatedAt: "2024-01-20T00:00:00Z", creator: "dex_protocol", difficulty: "intermediate", - deadline: new Date("2024-02-25"), + deadline: "2024-02-25T00:00:00Z", }, { id: "bounty-6", @@ -236,8 +236,8 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "completed", - createdAt: new Date("2024-01-10"), - updatedAt: new Date("2024-01-15"), + createdAt: "2024-01-10T00:00:00Z", + updatedAt: "2024-01-15T00:00:00Z", creator: "nft_marketplace", difficulty: "advanced", }, @@ -251,11 +251,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "multi-winner", status: "open", - createdAt: new Date("2024-01-15"), - updatedAt: new Date("2024-01-18"), + createdAt: "2024-01-15T00:00:00Z", + updatedAt: "2024-01-18T00:00:00Z", creator: "education_dao", difficulty: "beginner", - deadline: new Date("2024-03-01"), + deadline: "2024-03-01T00:00:00Z", }, { id: "bounty-8", @@ -267,11 +267,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "application", status: "open", - createdAt: new Date("2024-01-14"), - updatedAt: new Date("2024-01-21"), + createdAt: "2024-01-14T00:00:00Z", + updatedAt: "2024-01-21T00:00:00Z", creator: "oracle_network", difficulty: "advanced", - deadline: new Date("2024-02-28"), + deadline: "2024-02-28T00:00:00Z", }, { id: "bounty-9", @@ -283,11 +283,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "in-progress", - createdAt: new Date("2024-01-13"), - updatedAt: new Date("2024-01-19"), + createdAt: "2024-01-13T00:00:00Z", + updatedAt: "2024-01-19T00:00:00Z", creator: "analytics_platform", difficulty: "beginner", - deadline: new Date("2024-02-08"), + deadline: "2024-02-08T00:00:00Z", }, { id: "bounty-10", @@ -299,11 +299,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "open", - createdAt: new Date("2024-01-12"), - updatedAt: new Date("2024-01-22"), + createdAt: "2024-01-12T00:00:00Z", + updatedAt: "2024-01-22T00:00:00Z", creator: "staking_protocol", difficulty: "advanced", - deadline: new Date("2024-02-12"), + deadline: "2024-02-12T00:00:00Z", }, { id: "bounty-11", @@ -315,11 +315,11 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "competition", status: "open", - createdAt: new Date("2024-01-11"), - updatedAt: new Date("2024-01-16"), + createdAt: "2024-01-11T00:00:00Z", + updatedAt: "2024-01-16T00:00:00Z", creator: "api_team", difficulty: "intermediate", - deadline: new Date("2024-02-18"), + deadline: "2024-02-18T00:00:00Z", }, { id: "bounty-12", @@ -331,10 +331,10 @@ export const mockBounties: Bounty[] = [ currency: "USDC", claimingModel: "single-claim", status: "open", - createdAt: new Date("2024-01-09"), - updatedAt: new Date("2024-01-20"), + createdAt: "2024-01-09T00:00:00Z", + updatedAt: "2024-01-20T00:00:00Z", creator: "dapp_builders", difficulty: "intermediate", - deadline: new Date("2024-02-22"), + deadline: "2024-02-22T00:00:00Z", }, ]; diff --git a/lib/types.ts b/lib/types.ts index 978d829..53c7bd1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -24,8 +24,8 @@ export interface Project { description: string; tags: string[]; status: ProjectStatus; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; creator: string; category: string; milestones?: number; @@ -42,11 +42,19 @@ export interface Bounty { currency: string; claimingModel: "single-claim" | "application" | "competition" | "multi-winner"; status: BountyStatus; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; creator: string; + + // Status & Logic fields + claimedAt?: string; + claimedBy?: string; + lastActivityAt?: string; + claimExpiresAt?: string; + submissionsEndDate?: string; + difficulty: "beginner" | "intermediate" | "advanced"; - deadline?: Date; + deadline?: string; } // Available tags diff --git a/types/bounty.ts b/types/bounty.ts index e896c6a..42c4516 100644 --- a/types/bounty.ts +++ b/types/bounty.ts @@ -34,6 +34,12 @@ export interface Bounty { createdAt: string updatedAt: string + // Status & Logic fields + claimedAt?: string + claimedBy?: string // user/wallet ID + lastActivityAt?: string // for anti-squatting + claimExpiresAt?: string + submissionsEndDate?: string // For competitions/applications // Optional: Keep requirements/scope if needed for details view, // but strictly adhering to User's type for now. // I will add them as optional to avoid breaking existing UI logic too much if I can helper it,