diff --git a/PROGRESS.md b/PROGRESS.md
index bab63fb..44d3a71 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -319,3 +319,11 @@ is closed — see [CONTRIBUTING.md](CONTRIBUTING.md).
- **By:** ALLEN-AYODEJI
- **Closes:** N/A
- **Status:** Merged.
+
+### Frontend — Empty-state call to action for researchers
+
+- **What:** In `frontend/src/components/ui.tsx` and `frontend/src/pages/BountyList.tsx`, introduces `EmptyCTA` when there are no open bounties or filtered results. Explains how bounties work and provides direct action links to `docs/TUTORIAL.md`, `CONTRIBUTING.md`, and the GitHub issues board. Added unit test in `frontend/src/components/ui.test.tsx` (now 22 tests).
+- **Verified:** `oxlint` (0 warnings), `typecheck` (`tsc -b`), `vitest run` (22/22 passed), and `vite build` all pass cleanly. Visual appearance verified in browser with full-page screenshot at `assets/empty-state-cta.png`.
+- **By:** dezzy-music
+- **Closes:** #182
+- **Status:** PR open.
diff --git a/assets/empty-state-cta.png b/assets/empty-state-cta.png
new file mode 100644
index 0000000..0a8b276
Binary files /dev/null and b/assets/empty-state-cta.png differ
diff --git a/frontend/src/components/ui.test.tsx b/frontend/src/components/ui.test.tsx
index 10885f8..e7bf55e 100644
--- a/frontend/src/components/ui.test.tsx
+++ b/frontend/src/components/ui.test.tsx
@@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
-import { ErrorState, StatusBadge } from "./ui";
+import { EmptyCTA, ErrorState, StatusBadge } from "./ui";
describe("StatusBadge", () => {
it("renders the status with a status-specific class", () => {
@@ -19,3 +19,28 @@ describe("ErrorState", () => {
expect(onRetry).toHaveBeenCalledOnce();
});
});
+
+describe("EmptyCTA", () => {
+ it("renders title, description and links for researchers", () => {
+ render(
+
+ );
+ expect(screen.getByText("No open bounties right now")).toBeInTheDocument();
+ expect(screen.getByText("Earn bounties by finding typosquats.")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "Read the Tutorial" })).toHaveAttribute(
+ "href",
+ "https://github.com/DepRadarOrg/DepRadar/blob/main/docs/TUTORIAL.md"
+ );
+ expect(screen.getByRole("link", { name: "Contributing Guide" })).toHaveAttribute(
+ "href",
+ "https://github.com/DepRadarOrg/DepRadar/blob/main/CONTRIBUTING.md"
+ );
+ expect(screen.getByRole("link", { name: "Browse GitHub Issues" })).toHaveAttribute(
+ "href",
+ "https://github.com/DepRadarOrg/DepRadar/issues"
+ );
+ });
+});
diff --git a/frontend/src/components/ui.tsx b/frontend/src/components/ui.tsx
index f0b8e78..1c15539 100644
--- a/frontend/src/components/ui.tsx
+++ b/frontend/src/components/ui.tsx
@@ -91,3 +91,47 @@ export function ErrorState({ error, onRetry }: { error: Error; onRetry?: () => v
export function Empty({ children }: { children: ReactNode }) {
return
{children}
;
}
+
+export function EmptyCTA({
+ title,
+ description,
+ children,
+}: {
+ title: string;
+ description?: string;
+ children?: ReactNode;
+}) {
+ return (
+
+
{title}
+ {description &&
{description}
}
+
+
+ );
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 7347c58..5d51f87 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -796,6 +796,40 @@ a.card:hover {
flex-direction: column;
border-color: rgba(248, 113, 113, 0.5);
}
+.empty-cta {
+ flex-direction: column;
+ max-width: 680px;
+ margin: 0 auto;
+ gap: 1rem;
+}
+.empty-cta-title {
+ font: 600 1.25rem var(--display);
+ color: var(--text-h);
+}
+.empty-cta-desc {
+ font-size: 0.95rem;
+ line-height: 1.5;
+ color: var(--muted);
+ max-width: 540px;
+ margin: 0 auto;
+}
+.empty-cta-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ justify-content: center;
+ margin-top: 0.5rem;
+}
+.btn-primary {
+ background: var(--teal);
+ color: #060a14 !important;
+ border-color: var(--teal);
+ font-weight: 600;
+}
+.btn-primary:hover {
+ background: #2dd4bf;
+ color: #060a14 !important;
+}
.spinner {
width: 16px;
height: 16px;
diff --git a/frontend/src/pages/BountyList.tsx b/frontend/src/pages/BountyList.tsx
index fdbf1eb..2a5e504 100644
--- a/frontend/src/pages/BountyList.tsx
+++ b/frontend/src/pages/BountyList.tsx
@@ -1,7 +1,7 @@
import { useSearchParams } from "react-router-dom";
import { BountyCard, BountyCardSkeleton } from "../components/BountyCard";
import { Radar } from "../components/Radar";
-import { Empty, ErrorState, Skeleton } from "../components/ui";
+import { EmptyCTA, ErrorState, Skeleton } from "../components/ui";
import { listBounties } from "../lib/contract";
import { formatAmount } from "../lib/format";
import { boardStats } from "../lib/insights";
@@ -74,11 +74,14 @@ export function BountyList() {
{state.status === "error" && }
{state.status === "success" &&
(shown.length === 0 ? (
-
- {bounties.length === 0
- ? "No bounties yet — the radar is quiet."
- : `No ${filter.label.toLowerCase()} bounties right now.`}
-
+
) : (
{shown.map((b) => (