Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 22 additions & 8 deletions packages/nextjs/app/bounties/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Address } from "~~/components/scaffold-eth";
import { BountyStatus, bountyABI } from "~~/contracts/BountyABI";
import deployedContracts from "~~/contracts/deployedContracts";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
import { formatEthShort } from "~~/utils/format";
import { notification } from "~~/utils/scaffold-eth";

export default function BountyDetailsPage() {
Expand Down Expand Up @@ -244,6 +245,14 @@ export default function BountyDetailsPage() {
const isOwner = connectedAddress === owner?.result;
const currentStatus = status?.result !== undefined ? BountyStatus[status.result] : "Loading...";

// Compute display reward
const rewardWei = (
committedAmount && committedAmount > 0n ? committedAmount : (amount?.result as bigint) || 0n
) as bigint;
const fullEth = formatEther(rewardWei);
const shortEth = formatEthShort(rewardWei, 6);
const isTrimmed = (fullEth.split(".")[1]?.length || 0) > 6;

return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="bg-gray-900 border border-gray-800 p-8">
Expand All @@ -261,13 +270,18 @@ export default function BountyDetailsPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-6 border-t border-gray-800">
<div>
<h3 className="font-roboto text-sm mb-2 text-gray-500">Reward</h3>
<p className="font-roboto font-semibold text-2xl text-[var(--color-secondary)]">
{formatEther(
(committedAmount && committedAmount > 0n
? committedAmount
: (amount?.result as bigint) || 0n) as bigint,
)}{" "}
ETH
<p className="font-roboto font-semibold text-2xl text-[var(--color-secondary)] whitespace-nowrap">
<span className="relative inline-flex items-center gap-1 group" title={`${fullEth} ETH`}>
{shortEth} ETH
{isTrimmed && (
<>
<span className="text-gray-400 align-top text-sm">≈</span>
<span className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-black border border-gray-700 text-white text-xs font-roboto whitespace-nowrap opacity-0 group-hover:opacity-100">
{fullEth} ETH
</span>
</>
)}
</span>
</p>
</div>
<div>
Expand Down Expand Up @@ -320,7 +334,7 @@ export default function BountyDetailsPage() {
{hasMySubmission && connectedAddress && (
<a
href={`/reports/${bountyAddress}?researcher=${connectedAddress}`}
className="px-3 py-1 bg-purple-900/30 text-purple-300 border border-purple-700 text-xs font-roboto hover:bg-purple-900/40"
className="px-3 py-1 bg-secondary/30 text-secondary border border-secondary text-xs font-roboto hover:bg-secondary/50"
>
My Submission
</a>
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/bounties/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export default function CreateBountyPage() {
<button
type="submit"
disabled={isSubmitting}
className="w-full px-8 py-4 bg-[var(--color-secondary)] hover:opacity-90 text-black font-roboto font-medium text-lg transition-all duration-300 hover:scale-105 active:scale-95 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
className="w-full px-8 py-4 cursor-pointer bg-[var(--color-secondary)] hover:opacity-90 text-black font-roboto font-medium text-lg transition-all duration-300 hover:scale-105 active:scale-95 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<div className="h-5 w-5 border-2 border-black border-t-transparent rounded-full animate-spin" />
Expand Down
44 changes: 33 additions & 11 deletions packages/nextjs/app/bounties/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Address } from "~~/components/scaffold-eth";
import { BountyStatus, bountyABI } from "~~/contracts/BountyABI";
import deployedContracts from "~~/contracts/deployedContracts";
import { useScaffoldReadContract, useTargetNetwork } from "~~/hooks/scaffold-eth";
import { formatEthShort } from "~~/utils/format";

type BountyCardProps = {
id: string;
Expand Down Expand Up @@ -72,14 +73,31 @@ const BountyCard = ({
fetchMetadata();
}, [cid, id]);

// Short vs full formatting for reward
const fullEth = formatEther(amount);
const shortEth = formatEthShort(amount, 6);
const isTrimmed = (fullEth.split(".")[1]?.length || 0) > 6;

return (
<div
className={`bg-gray-900 border border-gray-800 hover:border-[var(--color-secondary)]/50 p-6 transition-all duration-300 hover:scale-105 ${isMetadataLoading ? "animate-pulse" : ""}`}
>
<div className="flex justify-between items-start mb-4">
<div className={`px-3 py-1 text-xs font-roboto font-medium ${getSeverityColor(metadata.severity)}`}>
{metadata.severity}
<div className="flex flex-row gap-4">
{" "}
<div className={`px-3 py-1 text-xs font-roboto font-medium ${getSeverityColor(metadata.severity)}`}>
{metadata.severity}
</div>
{hasSubmitted && connectedAddress && (
<Link
href={`/reports/${id}?researcher=${connectedAddress}`}
className="px-2 py-1 bg-secondary/30 text-secondary border border-secondary text-xs font-roboto hover:bg-secondary/50"
>
Submitted
</Link>
)}
</div>

<div className={`px-3 py-1 text-xs font-roboto font-medium ${getStatusColor(BountyStatus[status])}`}>
{BountyStatus[status]}
</div>
Expand All @@ -89,7 +107,19 @@ const BountyCard = ({
<div className="flex justify-between items-center text-sm mb-4 pt-4 border-t border-gray-800">
<div>
<p className="text-gray-500 font-roboto text-xs mb-1">Reward</p>
<p className="font-roboto font-medium text-lg text-[var(--color-secondary)]">{formatEther(amount)} ETH</p>
<p className="font-roboto font-medium text-lg text-[var(--color-secondary)] whitespace-nowrap">
<span className="relative inline-flex items-center gap-1 group" title={`${fullEth} ETH`}>
{shortEth} ETH
{isTrimmed && (
<>
<span className="text-gray-400 align-top text-xs">≈</span>
<span className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-black border border-gray-700 text-white text-xs font-roboto whitespace-nowrap opacity-0 group-hover:opacity-100">
{fullEth} ETH
</span>
</>
)}
</span>
</p>
</div>
<div className="text-right">
<p className="text-gray-500 font-roboto text-xs mb-1">Posted by</p>
Expand All @@ -100,14 +130,6 @@ const BountyCard = ({
<div className="text-gray-400 font-roboto">
Submissions: <span className="text-white font-medium">{submissionCount}</span>
</div>
{hasSubmitted && connectedAddress && (
<Link
href={`/reports/${id}?researcher=${connectedAddress}`}
className="px-2 py-1 bg-purple-900/30 text-purple-300 border border-purple-700 text-xs font-roboto hover:bg-purple-900/40"
>
Submitted
</Link>
)}
</div>
<Link
href={`/bounties/${id}`}
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,13 @@ export default function LeaderboardPage() {
{/* Tabs */}
<div className="mb-6 flex gap-2">
<button
className={`px-4 py-2 border ${tab === "researchers" ? "bg-[var(--color-secondary)] text-black border-[var(--color-secondary)]" : "bg-black/60 text-white border-[var(--color-secondary)]/40"}`}
className={`px-4 py-2 border cursor-pointer ${tab === "researchers" ? "bg-[var(--color-secondary)] text-black border-[var(--color-secondary)]" : "bg-black/60 text-white border-[var(--color-secondary)]/40"}`}
onClick={() => setTab("researchers")}
>
Researchers
</button>
<button
className={`px-4 py-2 border ${tab === "orgs" ? "bg-[var(--color-secondary)] text-black border-[var(--color-secondary)]" : "bg-black/60 text-white border-[var(--color-secondary)]/40"}`}
className={`px-4 py-2 border cursor-pointer ${tab === "orgs" ? "bg-[var(--color-secondary)] text-black border-[var(--color-secondary)]" : "bg-black/60 text-white border-[var(--color-secondary)]/40"}`}
onClick={() => setTab("orgs")}
>
Organisations
Expand Down
26 changes: 20 additions & 6 deletions packages/nextjs/app/reports/[bounty]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { EyeIcon } from "@heroicons/react/24/outline";
import { Address } from "~~/components/scaffold-eth";
import { BountyStatus, SubmissionStatus, bountyABI } from "~~/contracts/BountyABI";
import deployedContracts from "~~/contracts/deployedContracts";
import { formatEthShort } from "~~/utils/format";
import { notification } from "~~/utils/scaffold-eth";

// Consistent badge styles
Expand Down Expand Up @@ -210,6 +211,13 @@ export default function ReportDetailsPage() {
}
};

// Compute display reward
const rewardWei = committedAmount != null ? committedAmount : ((amount?.result as bigint) ?? 0n);

const fullEth = formatEther(rewardWei);
const shortEth = formatEthShort(rewardWei, 6);
const isTrimmed = (fullEth.split(".")[1]?.length || 0) > 6;

return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="bg-gray-900 border border-gray-800 p-8">
Expand Down Expand Up @@ -248,12 +256,18 @@ export default function ReportDetailsPage() {
</div>
<div>
<h3 className="text-gray-500 font-roboto text-sm mb-2">Reward</h3>
<div className="text-[var(--color-secondary)] font-roboto font-semibold">
{committedAmount != null
? `${formatEther(committedAmount)} ETH`
: amount?.result
? `${formatEther(amount.result as bigint)} ETH`
: "0 ETH"}
<div className="text-[var(--color-secondary)] font-roboto font-semibold whitespace-nowrap">
<span className="relative inline-flex items-center gap-1 group" title={`${fullEth} ETH`}>
{shortEth} ETH
{isTrimmed && (
<>
<span className="text-gray-400 align-top text-xs">≈</span>
<span className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-black border border-gray-700 text-white text-xs font-roboto whitespace-nowrap opacity-0 group-hover:opacity-100">
{fullEth} ETH
</span>
</>
)}
</span>
</div>
</div>
<div>
Expand Down
109 changes: 64 additions & 45 deletions packages/nextjs/app/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Address } from "~~/components/scaffold-eth";
import { SubmissionStatus, bountyABI } from "~~/contracts/BountyABI";
import deployedContracts from "~~/contracts/deployedContracts";
import { useScaffoldReadContract, useTargetNetwork } from "~~/hooks/scaffold-eth";
import { formatEthShort } from "~~/utils/format";

type ReportItem = {
bounty: `0x${string}`;
Expand Down Expand Up @@ -301,54 +302,72 @@ export default function ReportsPage() {
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filtered.map(r => (
<div
key={`${r.bounty}-${r.researcher}`}
className="bg-gray-900 border border-gray-800 hover:border-[var(--color-secondary)]/50 p-6 transition-all duration-300 hover:scale-105"
>
<div className="flex justify-between items-start mb-4">
<div
className={
r.subState === 1
? BADGE_PURPLE
: r.subState === 2
? BADGE_GREEN
: r.subState === 3
? BADGE_RED
: BADGE_GRAY
}
>
{SubmissionStatus[r.subState]}
</div>
<div className="flex items-center gap-2">
<span className={BADGE_PRIMARY}>{formatEther(r.amount)} ETH</span>
<span className={r.visibility === 1 ? BADGE_GREEN : BADGE_GRAY}>
{r.visibility === 1 ? "Public" : "Private"}
</span>
</div>
</div>
<div className="mt-2 space-y-3 text-sm">
<div>
<span className="text-gray-500 font-roboto text-xs block mb-1">Bounty</span>
<Address address={r.bounty} format="short" />
</div>
<div>
<span className="text-gray-500 font-roboto text-xs block mb-1">Researcher</span>
<Address address={r.researcher} format="short" />
{filtered.map(r => {
const fullEth = formatEther(r.amount);
const shortEth = formatEthShort(r.amount, 6);
const isTrimmed = (fullEth.split(".")[1]?.length || 0) > 6;

return (
<div
key={`${r.bounty}-${r.researcher}`}
className="bg-gray-900 border border-gray-800 hover:border-[var(--color-secondary)]/50 p-6 transition-all duration-300 hover:scale-105"
>
<div className="flex justify-between items-start mb-4">
<div
className={
r.subState === 1
? BADGE_PURPLE
: r.subState === 2
? BADGE_GREEN
: r.subState === 3
? BADGE_RED
: BADGE_GRAY
}
>
{SubmissionStatus[r.subState]}
</div>
<div className="flex items-center gap-2">
<span className={BADGE_PRIMARY + " whitespace-nowrap"} title={`${fullEth} ETH`}>
<span className="relative inline-flex items-center gap-1 group">
{shortEth} ETH
{isTrimmed && (
<>
<span className="text-gray-400 align-top text-[10px]">≈</span>
<span className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-black border border-gray-700 text-white text-xs font-roboto whitespace-nowrap opacity-0 group-hover:opacity-100">
{fullEth} ETH
</span>
</>
)}
</span>
</span>
<span className={r.visibility === 1 ? BADGE_GREEN : BADGE_GRAY}>
{r.visibility === 1 ? "Public" : "Private"}
</span>
</div>
</div>
<div className="truncate">
<span className="text-gray-500 font-roboto text-xs block mb-1">CID</span>
<span className="font-mono text-xs text-gray-400">{r.reportCid}</span>
<div className="mt-2 space-y-3 text-sm">
<div>
<span className="text-gray-500 font-roboto text-xs block mb-1">Bounty</span>
<Address address={r.bounty} format="short" />
</div>
<div>
<span className="text-gray-500 font-roboto text-xs block mb-1">Researcher</span>
<Address address={r.researcher} format="short" />
</div>
<div className="truncate">
<span className="text-gray-500 font-roboto text-xs block mb-1">CID</span>
<span className="font-mono text-xs text-gray-400">{r.reportCid}</span>
</div>
</div>
<Link
href={`/reports/${r.bounty}?researcher=${r.researcher}`}
className="block w-full text-center px-4 py-2 mt-4 bg-[var(--color-secondary)] hover:opacity-90 text-black font-roboto text-sm font-medium transition-all duration-300 hover:scale-105 active:scale-95"
>
View Report
</Link>
</div>
<Link
href={`/reports/${r.bounty}?researcher=${r.researcher}`}
className="block w-full text-center px-4 py-2 mt-4 bg-[var(--color-secondary)] hover:opacity-90 text-black font-roboto text-sm font-medium transition-all duration-300 hover:scale-105 active:scale-95"
>
View Report
</Link>
</div>
))}
);
})}
</div>
)}
</div>
Expand Down
3 changes: 1 addition & 2 deletions packages/nextjs/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react";
import Image from "next/image";
import { CodeBracketIcon } from "@heroicons/react/24/outline";

/**
* Site footer
Expand Down Expand Up @@ -29,7 +28,7 @@ export const Footer = () => {
rel="noreferrer"
className="flex items-center gap-2 hover:text-[var(--color-secondary)] transition-colors duration-300"
>
<CodeBracketIcon className="h-4 w-4" />
<Image src={"/icons/github-mark-white.svg"} alt="github logo" height={15} width={15} />
<span>GitHub</span>
</a>
<span className="text-gray-600">·</span>
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/public/icons/github-mark-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions packages/nextjs/utils/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { formatEther } from "viem";

/**
* Short ETH formatter that trims to fractionDigits and strips trailing zeros.
* Example: 0.025418709695 -> 0.0254187 (fractionDigits=7)
*/
export function formatEthShort(value: bigint, fractionDigits = 6): string {
const s = formatEther(value);
const [intPart, frac = ""] = s.split(".");
if (!frac) return intPart;
const trimmed = frac.slice(0, Math.max(0, fractionDigits)).replace(/0+$/, "");
return trimmed.length ? `${intPart}.${trimmed}` : intPart;
}