Skip to content
Open
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
102 changes: 102 additions & 0 deletions soroscan-frontend/__tests__/breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import { Breadcrumbs } from "@/components/ui/breadcrumbs"

// Mock next/navigation
const mockUsePathname = jest.fn()
jest.mock("next/navigation", () => ({
usePathname: () => mockUsePathname(),
}))

// Mock next-intl
jest.mock("next-intl", () => ({
useTranslations: () => (key: string) => {
const messages: Record<string, string> = {
home: "Home",
dashboard: "Dashboard",
contracts: "Contracts",
events: "Events",
}
return messages[key] || key
},
}))

// Mock next/link
jest.mock("next/link", () => {
const MockLink = ({ href, children }: { href: string; children: React.ReactNode }) => (
<a href={href}>{children}</a>
)
MockLink.displayName = "MockLink"
return MockLink
})

describe("Breadcrumbs", () => {
beforeEach(() => {
jest.clearAllMocks()
})

it("renders Home breadcrumb for root path", () => {
mockUsePathname.mockReturnValue("/en")
render(<Breadcrumbs />)

expect(screen.getByText("Home")).toBeInTheDocument()
// Current page is not a link
const homeText = screen.getByText("Home")
expect(homeText.tagName).not.toBe("A")
})

it("renders breadcrumbs for nested paths", () => {
mockUsePathname.mockReturnValue("/en/dashboard/contracts")
render(<Breadcrumbs />)

// Check Home link
const homeLink = screen.getByRole("link", { name: "Home" })
expect(homeLink).toHaveAttribute("href", "/en")

// Check Dashboard link
const dashboardLink = screen.getByRole("link", { name: "Dashboard" })
expect(dashboardLink).toHaveAttribute("href", "/en/dashboard")

// Check Contracts text (current page)
const contractsText = screen.getByText("Contracts")
expect(contractsText).toBeInTheDocument()
expect(contractsText.tagName).not.toBe("A")
})

it("handles dynamic segments by capitalizing them", () => {
mockUsePathname.mockReturnValue("/en/dashboard/contracts/some-contract")
render(<Breadcrumbs />)

expect(screen.getByText("Some-contract")).toBeInTheDocument()
})

it("shortens long segments", () => {
const longId = "CAS3J7H2Z7W7V7X7Y7Z7W7V7X7Y7Z7W7V7X7Y7Z7W7V7X7Y"
mockUsePathname.mockReturnValue(`/en/dashboard/contracts/${longId}`)
render(<Breadcrumbs />)

// Matches the shortened version: CAS3J7...7Y
expect(screen.getByText("CAS3J7...7Y")).toBeInTheDocument()
})

it("renders separators between items", () => {
mockUsePathname.mockReturnValue("/en/dashboard")
render(<Breadcrumbs />)

const listItems = screen.getAllByRole("listitem")
expect(listItems).toHaveLength(2) // Home, Dashboard

// The second list item should contain a separator (ChevronRight)
// We can't easily check for the SVG but we can check the presence of the separator logic
// In our implementation, index > 0 shows the ChevronRight
})

it("handles paths without locale", () => {
mockUsePathname.mockReturnValue("/dashboard/events")
render(<Breadcrumbs />)

expect(screen.getByRole("link", { name: "Home" })).toHaveAttribute("href", "/")
expect(screen.getByRole("link", { name: "Dashboard" })).toHaveAttribute("href", "/dashboard")
expect(screen.getByText("Events")).toBeInTheDocument()
})
})
2 changes: 2 additions & 0 deletions soroscan-frontend/app/contracts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import * as React from "react";
import { Breadcrumbs } from "@/components/ui/breadcrumbs";
import { Card } from "@/components/terminal/Card";
import { Button } from "@/components/terminal/Button";
import { ContractTable } from "./components/ContractTable";
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function ContractsPage() {
return (
<div className="min-h-screen bg-terminal-black p-8">
<div className="max-w-7xl mx-auto space-y-6">
<Breadcrumbs />
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-terminal-mono text-terminal-green mb-2">
Expand Down
2 changes: 2 additions & 0 deletions soroscan-frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useMemo, useState } from "react";
import { Breadcrumbs } from "@/components/ui/breadcrumbs";

type HealthState = "healthy" | "degraded" | "critical";

Expand Down Expand Up @@ -50,6 +51,7 @@ export default function DashboardPage() {
return (
<main className="min-h-screen bg-terminal-black p-8 text-terminal-green font-terminal-mono">
<div className="mx-auto max-w-6xl space-y-8">
<Breadcrumbs />
<header className="space-y-3">
<p className="text-xs tracking-[0.2em] text-terminal-gray">[PERFORMANCE_DASHBOARD]</p>
<h1 className="text-3xl">API Latency, Cache, and System Health</h1>
Expand Down
3 changes: 2 additions & 1 deletion soroscan-frontend/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next"
import "../styles/landing.css"
import { Navbar } from "@/components/terminal/landing/Navbar"
import { Footer } from "@/components/terminal/landing/Footer"
import { Breadcrumbs } from "@/components/ui/breadcrumbs"
import { CodeSnippet } from "@/components/terminal/landing/CodeSnippet"
import { Card } from "@/components/terminal/Card"
import { Button } from "@/components/terminal/Button"
Expand Down Expand Up @@ -79,8 +80,8 @@ export default function DocsPage() {
return (
<div className="min-h-screen font-terminal-mono selection:bg-terminal-green selection:text-terminal-black">
<Navbar />

<main className="container mx-auto px-6 md:px-8 py-12 md:py-16 space-y-20 max-w-5xl">
<Breadcrumbs />

{/* Page header */}
<div className="space-y-4">
Expand Down
5 changes: 3 additions & 2 deletions soroscan-frontend/app/features/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next"
import "../styles/landing.css"
import { Navbar } from "@/components/terminal/landing/Navbar"
import { Footer } from "@/components/terminal/landing/Footer"
import { Breadcrumbs } from "@/components/ui/breadcrumbs"
import { Card } from "@/components/terminal/Card"
import {
Zap, Database, GitBranch, Webhook, Globe, Code2,
Expand Down Expand Up @@ -109,8 +110,8 @@ export default function FeaturesPage() {
return (
<div className="min-h-screen font-terminal-mono selection:bg-terminal-green selection:text-terminal-black">
<Navbar />

<main className="container mx-auto px-6 md:px-8 py-12 md:py-16 space-y-20 max-w-6xl">
<main className="container mx-auto px-6 md:px-8 py-12 md:py-16 space-y-20 max-w-5xl">
<Breadcrumbs />

{/* Page header */}
<div className="space-y-4">
Expand Down
2 changes: 2 additions & 0 deletions soroscan-frontend/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { Breadcrumbs } from "@/components/ui/breadcrumbs";
import ThemeSelector from "./components/ThemeSelector";
import NotificationPrefs from "./components/NotificationPrefs";
import APIKeyManager from "./components/APIKeyManager";
Expand Down Expand Up @@ -44,6 +45,7 @@ export default function SettingsPage() {
return (
<main className="min-h-screen bg-[#0a0e27] text-green-400 p-6 font-mono">
<div className="max-w-2xl mx-auto">
<Breadcrumbs />
{/* Header */}
<div className="mb-6 border-b border-green-500/30 pb-4">
<h1 className="text-green-400 text-xl font-bold tracking-widest">
Expand Down
3 changes: 2 additions & 1 deletion soroscan-frontend/app/webhooks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react"
import { Plus, Activity, CheckCircle2, AlertTriangle } from "lucide-react"
import { Navbar } from "@/components/terminal/landing/Navbar"
import { Footer } from "@/components/terminal/landing/Footer"
import { Breadcrumbs } from "@/components/ui/breadcrumbs"
import { Button } from "@/components/terminal/Button"
import { Modal } from "@/components/terminal/Modal"
import { WebhookTable } from "./components/WebhookTable"
Expand Down Expand Up @@ -79,8 +80,8 @@ export default function WebhooksPage() {
return (
<div className="min-h-screen font-terminal-mono selection:bg-terminal-green selection:text-terminal-black">
<Navbar />

<main className="container mx-auto px-6 md:px-8 py-10 md:py-14 space-y-8 max-w-7xl">
<Breadcrumbs />

{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
Expand Down
101 changes: 101 additions & 0 deletions soroscan-frontend/components/ui/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client"

import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useTranslations } from "next-intl"
import { ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"

export interface BreadcrumbItem {
label: string
href: string
isCurrent: boolean
}

export function Breadcrumbs({ className }: { className?: string }) {
const pathname = usePathname()
const t = useTranslations("Navigation")

// Get all segments and filter out empty ones
const segments = pathname.split("/").filter(Boolean)

// Check if the first segment is a locale (en or es)
const locales = ["en", "es"]
const hasLocale = locales.includes(segments[0])

// Extract path segments excluding the locale
const pathSegments = hasLocale ? segments.slice(1) : segments

// Construct the breadcrumb items
const breadcrumbs: BreadcrumbItem[] = [
{
label: t("home"),
href: hasLocale ? `/${segments[0]}` : "/",
isCurrent: pathSegments.length === 0,
},
...pathSegments.map((segment, index) => {
// Build href segment by segment
const currentPathSegments = pathSegments.slice(0, index + 1)
const href = hasLocale
? `/${segments[0]}/${currentPathSegments.join("/")}`
: `/${currentPathSegments.join("/")}`

const isCurrent = index === pathSegments.length - 1

// Try to get translation, fallback to capitalized segment
let label = segment
const knownKeys = ["dashboard", "contracts", "events", "webhooks", "docs", "features", "login"]

if (knownKeys.includes(segment.toLowerCase())) {
try {
label = t(segment.toLowerCase())
} catch {
label = segment.charAt(0).toUpperCase() + segment.slice(1)
}
} else {
// Handle potential IDs or unknown segments
if (segment.length > 20) {
label = `${segment.substring(0, 6)}...${segment.substring(segment.length - 4)}`
} else {
label = segment.charAt(0).toUpperCase() + segment.slice(1)
}
}

return {
label,
href,
isCurrent,
}
}),
]

return (
<nav aria-label="Breadcrumb" className={cn("flex py-4 px-1", className)}>
<ol className="flex items-center flex-wrap gap-2 text-sm text-terminal-gray font-terminal-mono">
{breadcrumbs.map((item, index) => (
<li key={item.href} className="flex items-center">
{index > 0 && (
<ChevronRight className="w-4 h-4 mr-2 text-terminal-gray/30 shrink-0" aria-hidden="true" />
)}
{item.isCurrent ? (
<span
className="text-terminal-green font-bold tracking-tight"
aria-current="page"
>
{item.label}
</span>
) : (
<Link
href={item.href}
className="hover:text-terminal-green hover:underline underline-offset-4 transition-all duration-200 decoration-terminal-green/30"
>
{item.label}
</Link>
)}
</li>
))}
</ol>
</nav>
)
}
Loading