Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/api/bounties/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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);

// 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;
Expand All @@ -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))
);
}
Expand Down
7 changes: 6 additions & 1 deletion app/bounty/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,7 +28,11 @@ export async function generateMetadata({ params }: BountyPageProps): Promise<Met

export default async function BountyPage({ params }: BountyPageProps) {
const { id } = await params
const bounty = getBountyById(id)
let bounty = getBountyById(id)

if (bounty) {
bounty = BountyLogic.processBountyStatus(bounty)
}

if (!bounty) {
notFound()
Expand Down
18 changes: 10 additions & 8 deletions app/discover/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { FilterPanel } from "@/components/filters/filter-panel";
import { ProjectCard } from "@/components/cards/project-card";
import { BountyCard } from "@/components/cards/bounty-card";
import { Skeleton } from "@/components/ui/skeleton";
import { mockProjects, mockBounties } from "@/lib/mock-data";
import { mockProjects, mockBounties as rawMockBounties } from "@/lib/mock-data";
import { FilterState, TabType } from "@/lib/types";
import { PackageOpen, Coins } from "lucide-react";
import { BountyLogic } from '@/lib/logic/bounty-logic';

// Validation helpers
const isValidTab = (value: string | null): value is TabType => {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -212,7 +214,7 @@ export default function DiscoverPage() {
<Coins className="mr-2 h-4 w-4" />
Bounties
<span className="ml-2 text-xs opacity-70">
({mockBounties.length})
({rawMockBounties.length})
</span>
</TabsTrigger>
</TabsList>
Expand Down
6 changes: 6 additions & 0 deletions lib/api/bounties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand Down
99 changes: 99 additions & 0 deletions lib/logic/bounty-logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 {
/**
* 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<T extends {
status: string;
claimingModel: ClaimingModel;
claimExpiresAt?: string;
lastActivityAt?: string;
claimedBy?: string;
claimedAt?: string;
}>(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) => {
if (!val) return null;
return 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: {
status: string;
claimingModel: ClaimingModel;
claimExpiresAt?: string;
}) {
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();
}
}
3 changes: 3 additions & 0 deletions lib/mock-bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
Loading