From c296a51690a83db97db036f81376bf7abd9829f1 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 03:16:32 +0100 Subject: [PATCH 1/6] feat-Implement Bounty Status Logic & Anti-Squatting --- app/api/bounties/route.ts | 5 ++- app/bounty/[id]/page.tsx | 7 ++- app/discover/page.tsx | 8 ++-- lib/api/bounties.ts | 6 +++ lib/logic/bounty-logic.ts | 95 +++++++++++++++++++++++++++++++++++++++ lib/mock-bounty.ts | 3 ++ lib/types.ts | 8 ++++ types/bounty.ts | 6 +++ 8 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 lib/logic/bounty-logic.ts 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 { @@ -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) { @@ -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..f282fab --- /dev/null +++ b/lib/logic/bounty-logic.ts @@ -0,0 +1,95 @@ +import { Bounty, BountyStatus, ClaimingModel } from '@/types/bounty'; +import { differenceInDays, isPast, addDays, parseISO } from 'date-fns'; + +export class BountyLogic { + /** + * Configuration for inactivity thresholds (in days) + */ + static readonly INACTIVITY_THRESHOLD_DAYS = 7; + static readonly CLAIM_DURATION_DAYS = 14; + + /** + * 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 + const getDate = (val?: string | Date) => { + if (!val) return null; + return val instanceof Date ? val : parseISO(val); + }; + + 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; + } + + // If inactive for too long + const lastActive = getDate(bounty.lastActivityAt); + 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; + } + } + } + + return newBounty; + } + + /** + * Returns metadata about the claim status suitable for UI display. + */ + static getClaimStatusDisplay(bounty: Bounty) { + 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) { + return new Date(dateStr).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/types.ts b/lib/types.ts index 978d829..9832419 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -45,6 +45,14 @@ export interface Bounty { createdAt: Date; updatedAt: Date; creator: string; + + // Status & Logic fields + claimedAt?: Date; + claimedBy?: string; + lastActivityAt?: Date; + claimExpiresAt?: Date; + submissionsEndDate?: Date; + difficulty: "beginner" | "intermediate" | "advanced"; deadline?: Date; } 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, From 5db20cc4a964d9c1d44ddb6d0dd154feef08c1b5 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 03:28:21 +0100 Subject: [PATCH 2/6] feat: Standardize date fields to ISO 8601 strings across bounty and project data, updating types, mock data, and sorting logic. --- app/discover/page.tsx | 10 ++-- lib/logic/bounty-logic.ts | 16 +++--- lib/mock-data.ts | 110 +++++++++++++++++++------------------- lib/types.ts | 18 +++---- 4 files changed, 79 insertions(+), 75 deletions(-) diff --git a/app/discover/page.tsx b/app/discover/page.tsx index 3bc10fa..41862b6 100644 --- a/app/discover/page.tsx +++ b/app/discover/page.tsx @@ -118,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; } @@ -158,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); diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts index f282fab..d3fdcd1 100644 --- a/lib/logic/bounty-logic.ts +++ b/lib/logic/bounty-logic.ts @@ -1,5 +1,5 @@ -import { Bounty, BountyStatus, ClaimingModel } from '@/types/bounty'; -import { differenceInDays, isPast, addDays, parseISO } from 'date-fns'; +import { differenceInDays, isPast, parseISO } from 'date-fns'; +import { ClaimingModel } from '@/types/bounty'; // Keep if needed for value checking, or remove if just string export class BountyLogic { /** @@ -17,10 +17,10 @@ export class BountyLogic { static processBountyStatus(bounty: T): T { if (bounty.status !== 'claimed' && bounty.status !== 'open') return bounty; @@ -70,7 +70,11 @@ export class BountyLogic { /** * Returns metadata about the claim status suitable for UI display. */ - static getClaimStatusDisplay(bounty: Bounty) { + static getClaimStatusDisplay(bounty: { + status: string; + claimingModel: any; + claimExpiresAt?: string; + }) { if (bounty.status === 'open') return { label: 'Available', color: 'green' }; if (bounty.status === 'claimed') { 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 9832419..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,19 +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?: Date; + claimedAt?: string; claimedBy?: string; - lastActivityAt?: Date; - claimExpiresAt?: Date; - submissionsEndDate?: Date; + lastActivityAt?: string; + claimExpiresAt?: string; + submissionsEndDate?: string; difficulty: "beginner" | "intermediate" | "advanced"; - deadline?: Date; + deadline?: string; } // Available tags From 20cf8b478fb284088c9627f94edd27a4eacab86f Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 03:30:41 +0100 Subject: [PATCH 3/6] refactor: Refine type definitions for `claimingModel` and simplify date parsing in bounty logic. --- lib/logic/bounty-logic.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts index d3fdcd1..88feb3a 100644 --- a/lib/logic/bounty-logic.ts +++ b/lib/logic/bounty-logic.ts @@ -16,7 +16,7 @@ export class BountyLogic { */ static processBountyStatus { + const getDate = (val?: string) => { if (!val) return null; - return val instanceof Date ? val : parseISO(val); + return parseISO(val); }; const expiresAt = getDate(bounty.claimExpiresAt); @@ -72,7 +72,7 @@ export class BountyLogic { */ static getClaimStatusDisplay(bounty: { status: string; - claimingModel: any; + claimingModel: ClaimingModel; claimExpiresAt?: string; }) { if (bounty.status === 'open') return { label: 'Available', color: 'green' }; From 2efd38c50c8dc29389ee05c3c97a8e2c8c759e38 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 03:49:04 +0100 Subject: [PATCH 4/6] Remove `CLAIM_DURATION_DAYS` constant from bounty logic. --- lib/logic/bounty-logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts index 88feb3a..dc6aacb 100644 --- a/lib/logic/bounty-logic.ts +++ b/lib/logic/bounty-logic.ts @@ -6,7 +6,7 @@ export class BountyLogic { * Configuration for inactivity thresholds (in days) */ static readonly INACTIVITY_THRESHOLD_DAYS = 7; - static readonly CLAIM_DURATION_DAYS = 14; + /** * Processes the bounty status based on its model and timestamps. From c0a08a492cd733223bacc44f5aa5c9435a36f4a2 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 04:10:05 +0100 Subject: [PATCH 5/6] refactor: Standardize bounty status logic with `StatusAwareBounty` interface and improve date parsing robustness. --- lib/logic/bounty-logic.ts | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts index dc6aacb..97f6201 100644 --- a/lib/logic/bounty-logic.ts +++ b/lib/logic/bounty-logic.ts @@ -1,5 +1,18 @@ -import { differenceInDays, isPast, parseISO } from 'date-fns'; -import { ClaimingModel } from '@/types/bounty'; // Keep if needed for value checking, or remove if just string +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 { /** @@ -7,21 +20,13 @@ export class BountyLogic { */ 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 { + static processBountyStatus(bounty: T): T { if (bounty.status !== 'claimed' && bounty.status !== 'open') return bounty; const now = new Date(); @@ -33,10 +38,11 @@ export class BountyLogic { bounty.claimingModel === 'single-claim' && bounty.status === 'claimed' ) { - // Helper to get Date object + // Helper to get Date object safely const getDate = (val?: string) => { if (!val) return null; - return parseISO(val); + const date = parseISO(val); + return isValid(date) ? date : null; }; const expiresAt = getDate(bounty.claimExpiresAt); @@ -51,7 +57,7 @@ export class BountyLogic { } // If inactive for too long - const lastActive = getDate(bounty.lastActivityAt); + const lastActive = getDate(bounty.lastActivityAt) || getDate(bounty.claimedAt); if (lastActive) { const daysInactive = differenceInDays(now, lastActive); if (daysInactive > this.INACTIVITY_THRESHOLD_DAYS) { @@ -70,11 +76,7 @@ export class BountyLogic { /** * Returns metadata about the claim status suitable for UI display. */ - static getClaimStatusDisplay(bounty: { - status: string; - claimingModel: ClaimingModel; - claimExpiresAt?: string; - }) { + static getClaimStatusDisplay(bounty: StatusAwareBounty) { if (bounty.status === 'open') return { label: 'Available', color: 'green' }; if (bounty.status === 'claimed') { @@ -94,6 +96,8 @@ export class BountyLogic { } private static formatDate(dateStr: string) { - return new Date(dateStr).toLocaleDateString(); + const date = parseISO(dateStr); + if (!isValid(date)) return 'Invalid Date'; + return date.toLocaleDateString(); } } From 058387f7486397e535a48b6028973c0033fba813 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Thu, 29 Jan 2026 04:26:04 +0100 Subject: [PATCH 6/6] fix: Reset `lastActivityAt` when a bounty claim is cleared or expires. --- lib/logic/bounty-logic.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/logic/bounty-logic.ts b/lib/logic/bounty-logic.ts index 97f6201..8628288 100644 --- a/lib/logic/bounty-logic.ts +++ b/lib/logic/bounty-logic.ts @@ -54,6 +54,7 @@ export class BountyLogic { newBounty.claimedBy = undefined; newBounty.claimedAt = undefined; newBounty.claimExpiresAt = undefined; + newBounty.lastActivityAt = undefined; } // If inactive for too long @@ -66,6 +67,7 @@ export class BountyLogic { newBounty.claimedBy = undefined; newBounty.claimedAt = undefined; newBounty.claimExpiresAt = undefined; + newBounty.lastActivityAt = undefined; } } }