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}

} +
+ + Read the Tutorial + + + Contributing Guide + + + Browse GitHub Issues + + {children} +
+
+ ); +} 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) => (