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
26 changes: 12 additions & 14 deletions src/components/ApiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import WhyApi from "./WhyApi";
import { colorFromId } from "../utils/colorFromId";
import { ClockIcon, BoltIcon } from "./icons";
import StatusBadge from "./StatusBadge";


// ─── Skeleton ────────────────────────────────────────────────────────────────

Expand All @@ -37,12 +37,12 @@
aria-busy="true"
aria-label="Loading API"
style={{
padding: isCompact ? 10 : 12,
padding: isCompact ? "var(--mkt-card-compact-padding, 10px)" : "var(--mkt-card-padding, 12px)",
display: "flex",
flexDirection: "column",
minHeight: isCompact ? 188 : 220,
gap: isCompact ? 6 : 8,
border: "1px solid rgba(255,255,255,0.03)",
minHeight: isCompact ? "var(--mkt-card-compact-min-height, 188px)" : "var(--mkt-card-min-height, 220px)",
gap: isCompact ? "var(--mkt-card-compact-gap, 6px)" : "var(--mkt-card-gap, 8px)",
border: "1px solid var(--line, rgba(255,255,255,0.05))",
pointerEvents: "none",
position: "relative",
}}
Expand All @@ -58,7 +58,7 @@
left: 0,
width: 4,
height: "100%",
borderRadius: "var(--radius-lg) 0 0 var(--radius-lg)",
borderRadius: "var(--radius-lg, 12px) 0 0 var(--radius-lg, 12px)",
background: "color-mix(in srgb, var(--accent) 12%, transparent)",
}}
/>
Expand Down Expand Up @@ -86,8 +86,8 @@
<div className="api-marketplace-card-header" style={{ display: "flex", gap: 12, alignItems: "center" }}>
<Skeleton tone="stellar" width={56} height={56} borderRadius={10} />

<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: "var(--mkt-space-md)" }}>
<div style={{ display: "flex", gap: "var(--mkt-space-md)", alignItems: "baseline" }}>
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: "var(--mkt-space-md, 8px)" }}>
<div style={{ display: "flex", gap: "var(--mkt-space-md, 8px)", alignItems: "baseline" }}>
<Skeleton tone="stellar" width="60%" height={18} />
<Skeleton tone="stellar" width="20%" height={12} />
</div>
Expand All @@ -108,7 +108,7 @@
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: "var(--mkt-space-sm)"
gap: "var(--mkt-space-sm, 4px)"
}}
>
<Skeleton tone="stellar" width={50} height={12} />
Expand All @@ -122,17 +122,15 @@
<Skeleton tone="stellar" width={40} height={24} borderRadius={8} />
</div>

{/* WhyApi placeholder — matches the real card's rationale section
that appears in comfortable mode. */}
{/* WhyApi placeholder — matches the real card's rationale section in comfortable mode */}
{!isCompact && (
<div style={{ marginTop: 2, display: "flex", flexDirection: "column", gap: 4 }}>
<Skeleton tone="stellar" width="35%" height={14} />
<Skeleton tone="stellar" width="80%" height={14} />
</div>
)}

{/* Sparkline section — matches the real card's 24h sparkline
that appears between tags and stats. */}
{/* Sparkline section placeholder */}
<div
style={{
display: "flex",
Expand All @@ -152,7 +150,7 @@
marginTop: "auto",
display: "flex",
flexDirection: "column",
gap: "var(--mkt-space-lg)",
gap: "var(--mkt-space-lg, 12px)",
}}
>
<div className="api-card__stats" aria-hidden="true">
Expand Down Expand Up @@ -347,7 +345,7 @@
const toggleSave = () => setPopoverOpen((s) => !s);

const handleToggleCollection = (colId: string) => {
const colName = collections.find(c => c.id === colId)?.name || "collection";

Check failure on line 348 in src/components/ApiCard.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

'colName' is declared but its value is never read.
if (savedIn.has(colId)) {
removeEndpointFromCollection(colId, endpointId);
onStatusChange?.("Removed from collection");
Expand Down Expand Up @@ -601,7 +599,7 @@
onViewDetails,
onTagClick,
activeTag,
onBrowse,

Check failure on line 602 in src/components/ApiCard.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

'onBrowse' is declared but its value is never read.
}: {
api?: APIItem;
loading?: boolean;
Expand All @@ -628,7 +626,7 @@
>
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
<EmptyState
variant="api-card"

Check failure on line 629 in src/components/ApiCard.tsx

View workflow job for this annotation

GitHub Actions / TypeScript check & production build

Type '"api-card"' is not assignable to type 'EmptyStateVariant | undefined'.
size={density === "compact" ? "compact" : "default"}
action={{
label: "Explore Marketplace",
Expand Down
2 changes: 1 addition & 1 deletion src/components/FiltersSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Dropdown from "./Dropdown";
import EmptyState from "./EmptyState";
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { usePersistedState } from "../hooks/usePersistedState";
import { FiltersSidebarSkeleton } from "./Skeleton";

import LiveRegion from "./LiveRegion";

const POPULARITY_OPTIONS = [
Expand Down
142 changes: 57 additions & 85 deletions src/components/Skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,62 @@ export function SkeletonRow({ rows = 5 }: { rows?: number }) {

export function FiltersSidebarSkeleton() {
return (
<div className="filters-sidebar filters-sidebar-skeleton" aria-hidden="true" style={{ display: "grid", gap: 16 }}>
{/* Section heading */}
<Skeleton width="55%" height={22} />
<Skeleton width="55%" height={22} />
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} style={{ display: "grid", gap: 8 }}>
<Skeleton width="40%" height={14} />
<Skeleton width="100%" height={40} borderRadius={10} />
<div
className="filters-sidebar filters-sidebar-skeleton"
aria-hidden="true"
style={{ display: "grid", gap: 16 }}
>
{/* Categories Group Skeleton */}
<div style={{ display: "grid", gap: 8 }}>
<Skeleton width="40%" height={16} />
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Skeleton width={16} height={16} borderRadius={4} />
<Skeleton width="65%" height={14} />
</div>
))}
</div>

{/* Price Range Group Skeleton */}
<div style={{ display: "grid", gap: 8 }}>
<Skeleton width="45%" height={16} />
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Skeleton width={28} height={14} />
<Skeleton width="100%" height={36} borderRadius={8} />
</div>
))}
<Skeleton width="48%" height={14} />
<Skeleton width="100%" height={44} borderRadius={12} />
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Skeleton width={28} height={14} />
<Skeleton width="100%" height={36} borderRadius={8} />
</div>
</div>

{/* Status Group Skeleton */}
<div style={{ display: "grid", gap: 8 }}>
<Skeleton width="35%" height={16} />
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Skeleton width={16} height={16} borderRadius={4} />
<Skeleton width="55%" height={14} />
</div>
))}
</div>

{/* Popularity Group Skeleton */}
<div style={{ display: "grid", gap: 8 }}>
<Skeleton width="40%" height={16} />
<Skeleton width="100%" height={38} borderRadius={8} />
</div>

{/* Favorites Group Skeleton */}
<div style={{ display: "grid", gap: 8 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Skeleton width={16} height={16} borderRadius={4} />
<Skeleton width="60%" height={14} />
</div>
</div>

{/* Clear Filters Button Skeleton */}
<Skeleton width="100%" height={40} borderRadius={10} style={{ marginTop: 8 }} />
</div>
);
}
Expand Down Expand Up @@ -247,80 +291,8 @@ export function EmptyStateSkeleton({ size = "default", hasAction = false }: { si
);
}

/**
* MarketplacePageSkeleton — loading placeholder for the marketplace page.
* Renders a simplified layout shell while API data is being fetched.
*/
export function MarketplacePageSkeleton() {
return (
<div
aria-busy="true"
aria-label="Marketplace loading"
style={{
display: "grid",
gap: 16,
padding: "24px 0",
}}
>
{/* Toolbar skeleton */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Skeleton tone="stellar" width="30%" height={24} />
<Skeleton tone="stellar" width={100} height={36} borderRadius={8} />
</div>

{/* Category pills skeleton */}
<div style={{ display: "flex", gap: 8 }}>
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} tone="stellar" width={80 + i * 10} height={32} borderRadius={8} />
))}
</div>

{/* Tag filter skeleton */}
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} tone="stellar" width={60 + i * 8} height={32} borderRadius={8} />
))}
</div>

{/* Grid skeleton */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
gap: 16,
}}
>
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
style={{
padding: 16,
border: "1px solid var(--line)",
borderRadius: 12,
display: "grid",
gap: 12,
}}
>
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
<Skeleton tone="stellar" width={48} height={48} borderRadius={10} />
<div style={{ flex: 1, display: "grid", gap: 6 }}>
<Skeleton tone="stellar" width="70%" height={16} />
<Skeleton tone="stellar" width="50%" height={12} />
</div>
<Skeleton tone="stellar" width={60} height={14} />
</div>
<div style={{ display: "flex", gap: 6 }}>
{Array.from({ length: 3 }).map((_, j) => (
<Skeleton key={j} tone="stellar" width={50 + j * 10} height={24} borderRadius={6} />
))}
</div>
<Skeleton tone="stellar" width="100%" height={40} borderRadius={6} />
</div>
))}
</div>
</div>
);
}
import MarketplacePageSkeleton from "../pages/MarketplacePage.skeleton";
export { MarketplacePageSkeleton };

export function ApiDetailPageSkeleton({ onBack }: { onBack?: () => void }) {
return (
Expand Down
34 changes: 25 additions & 9 deletions src/pages/MarketplacePage.skeleton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,48 @@ describe("MarketplacePageSkeleton", () => {
expect(
getByLabelText("Marketplace loading shell").getAttribute("aria-busy"),
).toBe("true");
expect(container.querySelector(".marketplace-page")).toBeTruthy();
expect(container.querySelector(".marketplace-header")).toBeTruthy();
expect(container.querySelector(".recently-active-rail-skeleton")).toBeTruthy();
expect(container.querySelector(".marketplace-layout")).toBeTruthy();
expect(container.querySelector(".marketplace-sidebar")).toBeTruthy();
expect(container.querySelector(".marketplace-toolbar")).toBeTruthy();
expect(container.querySelector(".filters-sidebar")).toBeTruthy();
expect(container.querySelector(".pill-bar")).toBeTruthy();
expect(container.querySelector(".api-tag-filter")).toBeTruthy();
expect(container.querySelector(".marketplace-grid")).toBeTruthy();
expect(container.querySelectorAll(".api-marketplace-card").length).toBe(6);
expect(container.querySelectorAll(".api-marketplace-card").length).toBe(12);
});

it("renders card skeletons that match the ApiCard structure for shape parity", () => {
const { container } = render(<MarketplacePageSkeleton />);

// Each card skeleton should contain the stats section matching the real card
const cards = container.querySelectorAll(".api-marketplace-card");
expect(cards.length).toBe(6);
expect(cards.length).toBe(12);

// Each card should have a stats section (matching real ApiCard structure)
cards.forEach((card) => {
expect(card.querySelector(".api-card__stats")).toBeTruthy();
expect(card.querySelectorAll(".api-card__stat").length).toBe(3);
});
});

it("renders header, filter sidebar, and toolbar placeholders", () => {
it("supports compact density prop", () => {
const { container } = render(<MarketplacePageSkeleton density="compact" />);

const cards = container.querySelectorAll(".api-marketplace-card");
expect(cards.length).toBe(12);
cards.forEach((card) => {
expect(card.classList.contains("api-card--compact")).toBe(true);
});
});

it("decorates inner skeleton elements with aria-hidden for accessibility", () => {
const { container } = render(<MarketplacePageSkeleton />);

expect(container.querySelector(".marketplace-header")).toBeTruthy();
expect(container.querySelector(".marketplace-sidebar")).toBeTruthy();
expect(container.querySelector(".marketplace-toolbar")).toBeTruthy();
expect(container.querySelector(".filters-sidebar")).toBeTruthy();
const rail = container.querySelector(".recently-active-rail-skeleton");
expect(rail?.getAttribute("aria-hidden")).toBe("true");

const sidebar = container.querySelector(".filters-sidebar-skeleton");
expect(sidebar?.getAttribute("aria-hidden")).toBe("true");
});
});
Loading
Loading