diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..19cf0a4 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-maskable.png b/public/icon-maskable.png new file mode 100644 index 0000000..a116e2b Binary files /dev/null and b/public/icon-maskable.png differ diff --git a/src/app/issues/[id]/opengraph-image.tsx b/src/app/issues/[id]/opengraph-image.tsx new file mode 100644 index 0000000..27f18c8 --- /dev/null +++ b/src/app/issues/[id]/opengraph-image.tsx @@ -0,0 +1,93 @@ +import { ImageResponse } from "next/og"; +import { fetchBounty } from "@/lib/api"; +import { mockBounties } from "@/lib/mock-data"; +import { formatCurrency } from "@/lib/utils"; + +export const alt = "MergeFi bounty"; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +export default async function BountyOpenGraphImage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const { data: bounty } = await fetchBounty( + id, + mockBounties.find((b) => b.id === id), + ); + + const title = bounty?.title ?? "Bounty not found"; + const repo = bounty ? `${bounty.org}/${bounty.repo}` : "MergeFi"; + const reward = bounty ? formatCurrency(bounty.reward, bounty.asset) : ""; + const status = bounty ? bounty.status.replace(/_/g, " ").toUpperCase() : ""; + + return new ImageResponse( +
+ Open a pull request that says Fixes #{bounty?.issueNumber ?? fallbackBounty?.issueNumber} in
+ its description. The reward is released automatically when it merges.
+
diff --git a/src/hooks/useClaimRace.ts b/src/hooks/useClaimRace.ts
index 599ad4d..ba20a67 100644
--- a/src/hooks/useClaimRace.ts
+++ b/src/hooks/useClaimRace.ts
@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { apiPost, fetchBounty } from '@/lib/api';
import { adaptBounty, type RawBounty } from '@/lib/adapters';
+import { generateIdempotencyKey } from '@/lib/utils';
import type { Bounty, ClaimResult } from '@/types/bounty';
interface ApiError {
@@ -16,6 +17,10 @@ export function useClaimRace(bountyId: string, onClaimSuccess?: (bounty: Bounty)
// skip state updates once unmounted. Set to true in the effect body too so
// React Strict Mode's simulated unmount/remount doesn't leave it false.
const isMountedRef = useRef(true);
+ // POST /bounties/:id/claim requires an Idempotency-Key header. One key per
+ // claim intent: reused when a failed request is retried, rotated once the
+ // claim has a definitive outcome.
+ const idempotencyKeyRef = useRef(generateIdempotencyKey());
useEffect(() => {
isMountedRef.current = true;
@@ -35,7 +40,8 @@ export function useClaimRace(bountyId: string, onClaimSuccess?: (bounty: Bounty)
setIsClaiming(true);
try {
- const raw = await apiPost