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
22 changes: 20 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Routes, Route, NavLink, useNavigate, useLocation } from 'react-router-d
import { ThemeToggle } from './ThemeToggle';
import ApiUsage from './ApiUsage';
import Dashboard from './components/Dashboard';
import MyApis from './pages/MyApis';
import RouteProgressBar from './components/RouteProgressBar';
import ServerError from './components/ServerError';
import useDocumentTitle from "./hooks/useDocumentTitle";
import NotFound from './components/NotFound';
import { startRouteLoading, stopRouteLoading } from './hooks/useRouteLoading';
import useDocumentTitle from './hooks/useDocumentTitle';
import { formatUsdc, formatUsdShortcut } from './utils/format';
import {
EXPLORER_BASE_URL,
Expand Down Expand Up @@ -104,6 +104,7 @@ const APP_ROUTES = {
dashboard: "/dashboard",
marketplace: "/marketplace",
publish: "/publish",
myApis: "/apis/my-apis",
apiUsage: "/api-usage",
billing: "/billing",
documentation: "/documentation",
Expand Down Expand Up @@ -273,10 +274,10 @@ function createMockHash() {
function App() {
const navigate = useNavigate();
const location = useLocation();
const navigate = useNavigate();
const routeTitleMap: Record<string, string> = {
[APP_ROUTES.marketplace]: "Marketplace – Callora",
[APP_ROUTES.dashboard]: "Dashboard – Callora",
[APP_ROUTES.myApis]: "My APIs – Callora",
[APP_ROUTES.billing]: "Billing – Callora",
"/api-usage": "API Usage – Callora",
[APP_ROUTES.landing]: "Callora",
Expand Down Expand Up @@ -305,6 +306,16 @@ function App() {

// Handle global shortcuts
const handleGlobalKeyDown = useCallback((event: KeyboardEvent) => {
// Navigation to My APIs (e.g. g then a)
if (event.key === 'g') {
const handleNextKey = (e: KeyboardEvent) => {
if (e.key === 'a') {
navigate(APP_ROUTES.myApis);
}
};
window.addEventListener('keydown', handleNextKey, { once: true });
}

// Open shortcuts modal with ?
if (event.key === '?' || (event.shiftKey && event.key === '/')) {
event.preventDefault();
Expand Down Expand Up @@ -548,6 +559,7 @@ function App() {
<nav className="nav" aria-label="Primary navigation">
<NavLink to={APP_ROUTES.dashboard} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Dashboard</NavLink>
<NavLink to={APP_ROUTES.marketplace} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Marketplace</NavLink>
<NavLink to={APP_ROUTES.myApis} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>My APIs</NavLink>
<NavLink to={APP_ROUTES.billing} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Billing</NavLink>
<NavLink to={APP_ROUTES.themePlayground} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Theme Playground</NavLink>
</nav>
Expand All @@ -570,6 +582,11 @@ function App() {
}
/>

<Route
path={APP_ROUTES.myApis}
element={<MyApis />}
/>

<Route
path={APP_ROUTES.dashboard}
element={
Expand Down Expand Up @@ -744,6 +761,7 @@ function App() {
<nav className="footer-nav" aria-label="Footer navigation">
<NavLink to={APP_ROUTES.dashboard} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Dashboard</NavLink>
<NavLink to={APP_ROUTES.marketplace} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Marketplace</NavLink>
<NavLink to={APP_ROUTES.myApis} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>My APIs</NavLink>
<NavLink to={APP_ROUTES.billing} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Billing</NavLink>
<NavLink to={APP_ROUTES.themePlayground} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Theme Playground</NavLink>
<NavLink to={APP_ROUTES.status} className={({ isActive }) => isActive ? "link-nav active" : "link-nav"}>Status</NavLink>
Expand Down
7 changes: 7 additions & 0 deletions src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export default function CommandPalette() {
action: () => navigateTo('/marketplace'),
icon: '🛍️'
},
{
id: 'my-apis',
name: 'Go to My APIs',
category: 'Navigation',
action: () => navigateTo('/apis/my-apis'),
icon: '🔌'
},
{
id: 'billing',
name: 'Go to Billing',
Expand Down
19 changes: 19 additions & 0 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export interface EmptyStateProps {
message?: string;
onClearFilters?: () => void;
onRetry?: () => void | Promise<void>;
action?: {
label: string;
onClick: () => void;
};
}

/**
Expand All @@ -23,6 +27,7 @@ export default function EmptyState({
message,
onClearFilters,
onRetry,
action,
}: EmptyStateProps) {
// Default copy based on variant
const defaults = {
Expand Down Expand Up @@ -153,6 +158,20 @@ export default function EmptyState({
</p>

{/* Primary action */}
{action && (
<button
className="primary-button"
onClick={action.onClick}
style={{
minHeight: "44px",
minWidth: "160px",
}}
type="button"
>
{action.label}
</button>
)}

{variant === "filtered" && onClearFilters && (
<button
className="primary-button"
Expand Down
58 changes: 58 additions & 0 deletions src/pages/MyApis.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, it, expect, vi } from "vitest";
import MyApis from "./MyApis";

// Mock useNavigate
const mockNavigate = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual("react-router-dom");
return {
...actual,
useNavigate: () => mockNavigate,
};
});

// Mock useDocumentTitle hook
vi.mock("../hooks/useDocumentTitle", () => ({
default: vi.fn(),
}));

describe("MyApis Page", () => {
it("renders the empty state with correct title and message", () => {
render(
<MemoryRouter>
<MyApis />
</MemoryRouter>
);

expect(screen.getByText("No APIs published yet")).toBeTruthy();
expect(
screen.getByText(/You haven't listed any APIs on the marketplace/i)
).toBeTruthy();
});

it("renders the 'Publish your first API' CTA button", () => {
render(
<MemoryRouter>
<MyApis />
</MemoryRouter>
);

const ctaButton = screen.getByRole("button", { name: /Publish your first API/i });
expect(ctaButton).toBeTruthy();
});

it("navigates to the publish page when the CTA button is clicked", () => {
render(
<MemoryRouter>
<MyApis />
</MemoryRouter>
);

const ctaButton = screen.getByRole("button", { name: /Publish your first API/i });
fireEvent.click(ctaButton);

expect(mockNavigate).toHaveBeenCalledWith("/publish");
});
});
54 changes: 54 additions & 0 deletions src/pages/MyApis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useNavigate } from "react-router-dom";
import EmptyState from "../components/EmptyState";
import useDocumentTitle from "../hooks/useDocumentTitle";

/**
* MyApis page for Callora.
* Shows the list of APIs published by the current user.
* In the current phase, it displays a polished empty state with a CTA to publish an API.
*/
export default function MyApis() {
const navigate = useNavigate();

useDocumentTitle(
"My APIs – Callora",
"Manage and monitor your published APIs on the Callora marketplace."
);

return (
<div className="my-apis-page" style={{ padding: "24px 0" }}>
<header style={{ marginBottom: "32px", padding: "0 4px" }}>
<p className="eyebrow">Developer Dashboard</p>
<h1 style={{
margin: "0 0 12px",
fontSize: "clamp(1.8rem, 3vw, 2.4rem)",
fontWeight: "700",
color: "var(--text)"
}}>
My APIs
</h1>
<p style={{
margin: 0,
fontSize: "1rem",
color: "var(--muted)",
lineHeight: "1.65",
maxWidth: "600px"
}}>
View performance metrics, update documentation, and manage pricing for your listed APIs.
</p>
</header>

<section className="surface" style={{ borderRadius: "16px", overflow: "hidden" }}>
<EmptyState
variant="empty"
title="No APIs published yet"
message="You haven't listed any APIs on the marketplace. Start monetizing your services with usage-based USDC billing today."
action={{
label: "Publish your first API",
onClick: () => navigate("/publish"),
}}
/>
</section>
</div>
);
}