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
1 change: 1 addition & 0 deletions src/components/ApiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ export default function ApiCard({
width={90}
height={28}
/>
</div>

<div
className="api-marketplace-card-footer"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { CSSProperties } from "react";
import React, { CSSProperties, Fragment } from "react";

interface SkeletonProps {
width?: string | number;
Expand Down
23 changes: 23 additions & 0 deletions src/pages/MarketplacePage.skeleton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,27 @@ describe("MarketplacePageSkeleton", () => {
expect(container.querySelector(".marketplace-grid")).toBeTruthy();
expect(container.querySelectorAll(".api-marketplace-card").length).toBe(6);
});

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);

// 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", () => {
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();
});
});
56 changes: 34 additions & 22 deletions src/pages/MarketplacePage.skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
/**
* MarketplacePage.skeleton.tsx
*
* Loading shell for the Marketplace page.
* Uses ApiCardSkeleton (from ApiCard.tsx) to keep skeleton shapes in
* exact parity with the real cards — dimensions, layout structure, and
* spacing all mirror the final rendered output so the CLS shift on load
* is minimal.
*/
import Skeleton from "../components/Skeleton";
import { ApiCardSkeleton } from "../components/ApiCard";

function MarketplaceCardSkeleton() {
return (
<article
className="preview-card api-marketplace-card"
aria-hidden="true"
style={{ padding: 16, display: "grid", gap: 12, minHeight: 220 }}
>
<Skeleton width={56} height={56} borderRadius={10} />
<Skeleton width="68%" height={18} />
<Skeleton width="88%" height={14} />
<Skeleton width="74%" height={14} />
<div style={{ display: "flex", gap: 8, marginTop: 4 }}>
<Skeleton width={48} height={24} borderRadius={999} />
<Skeleton width={64} height={24} borderRadius={999} />
<Skeleton width={40} height={24} borderRadius={999} />
</div>
<Skeleton width="100%" height={36} borderRadius={14} style={{ marginTop: "auto" }} />
</article>
);
}

/**
* Mirrors the FiltersSidebar structure:
* - heading placeholder
* - 4 filter groups (label + input)
* - price range row
* - apply button
*/
function FilterBlockSkeleton() {
return (
<section
className="filters-sidebar"
aria-hidden="true"
style={{ display: "grid", gap: 16 }}
>
{/* Section heading */}
<Skeleton width="55%" height={22} />
{/* Filter groups */}
{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>
))}
{/* Price range label */}
<Skeleton width="48%" height={14} />
{/* Apply / clear button */}
<Skeleton width="100%" height={44} borderRadius={12} />
</section>
);
Expand All @@ -48,9 +48,12 @@ export default function MarketplacePageSkeleton() {
aria-busy="true"
aria-label="Marketplace loading shell"
>
{/* ── Header row ── */}
<div className="marketplace-header">
{/* Page title */}
<Skeleton width={240} height={42} />

{/* Search + density toggle */}
<div className="marketplace-search-row">
<Skeleton width="100%" height={48} borderRadius={12} />

Expand All @@ -64,26 +67,35 @@ export default function MarketplacePageSkeleton() {
</div>
</div>

{/* Sort dropdown */}
<Skeleton width={170} height={40} borderRadius={12} />
</div>

{/* ── Main layout ── */}
<div className="marketplace-layout">
<aside className="marketplace-sidebar">
<FilterBlockSkeleton />
</aside>

<main className="marketplace-results">
{/* Toolbar: result count + actions */}
<div className="marketplace-toolbar">
<Skeleton width="42%" height={18} />
<div className="marketplace-actions" aria-hidden="true">
{/* Sort dropdown placeholder */}
<Skeleton width={142} height={40} borderRadius={10} />
{/* Filters button placeholder */}
<Skeleton width={110} height={40} borderRadius={10} />
</div>
</div>

{/*
* Card grid — ApiCardSkeleton is kept in sync with the real ApiCard
* layout automatically, so any future card changes are reflected here.
*/}
<div className="marketplace-grid">
{Array.from({ length: 6 }).map((_, index) => (
<MarketplaceCardSkeleton key={index} />
<ApiCardSkeleton key={index} />
))}
</div>
</main>
Expand Down