JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 51
  • Score
    100M100P100Q73199F
  • License MIT

Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient (with built-in filtering), ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, and GalleryCarousel. Pass a clientSlug and apiBase — done.

Package Exports

  • project-portfolio

Readme

project-portfolio

A suite of self-contained project portfolio components for Next.js App Router. Drop in a clientSlug and apiBase — each component fetches, caches, and renders everything on the server with zero client-side waterfall requests.

Requirements

  • Next.js 13+ (App Router)
  • React 18+

No other dependencies required.

Installation

npm install project-portfolio

Components

ProjectPortfolio

A full project grid page. Fetches all projects for a client and renders them as responsive cards (1 column on mobile, 2 on tablet, 3 on desktop).

// app/projects/page.tsx
import { ProjectPortfolio } from "project-portfolio"

// Must be a Server Component — do NOT add "use client"
export default async function ProjectsPage() {
  return (
    <ProjectPortfolio
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
    />
  )
}
Prop Type Required Default Description
clientSlug string Yes Identifies which client's projects to load
apiBase string Yes Base URL of the projects API
basePath string No "/projects" Base path for project detail links
searchParams Record<string, string | string[] | undefined> No {} Filter params forwarded to the API — pass Next.js searchParams directly
revalidate number No 86400 Cache revalidation period in seconds (24 hours)

URL-driven filter integration

ProjectPortfolio works together with ProjectMenu to form a complete filter flow. When a user clicks a filter link in the megamenu, they are navigated to the project grid with filter query params appended to the URL. Pass Next.js searchParams to ProjectPortfolio so it can forward them to the API:

// app/projects/page.tsx
import { ProjectPortfolio } from "project-portfolio"

export default async function ProjectsPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return (
    <ProjectPortfolio
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      searchParams={searchParams}
    />
  )
}

Filter URLs follow this pattern — the key matches the custom field key in the schema:

/projects?type=commercial
/projects?type=educational-facilities
/projects?system=spacematic

When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to basePath.


ProjectPortfolioClient

A "use client" component that fetches all projects once on mount (module-level cached — no re-fetch on remount) and filters them locally in memory. It ships with no filter UI — build your own dropdowns, buttons, or search inputs and drive the component with the filters prop.

// Works in any component — no RSC required
import { ProjectPortfolioClient } from "project-portfolio"

export default function ProjectsPage() {
  return (
    <ProjectPortfolioClient
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
    />
  )
}

Wire up your own filter UI with the filters prop:

"use client"
import { useState } from "react"
import { ProjectPortfolioClient } from "project-portfolio"

export default function ProjectsPage() {
  const [filters, setFilters] = useState<Record<string, string>>({})

  return (
    <>
      {/* Your own filter UI */}
      <select onChange={(e) => setFilters({ type: e.target.value })}>
        <option value="">All Types</option>
        <option value="commercial">Commercial</option>
        <option value="educational-facilities">Educational</option>
      </select>

      {/* Component receives filters and applies them in memory */}
      <ProjectPortfolioClient
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
        filters={filters}
      />
    </>
  )
}
Prop Type Required Default Description
clientSlug string Yes Identifies which client's projects to load
apiBase string Yes Base URL of the projects API
basePath string No "/projects" Base path for project detail links
filters Record<string, string> No {} Active filters keyed by custom field key — filtering is instant, no API call on change
columns 2 | 3 No 3 Number of columns in the project grid
font string No System font stack Font family string applied to all text

How filtering works: Pass any Record<string, string> of field key/value pairs and the component filters the already-fetched project list in memory. All filter logic uses AND matching with case-insensitive comparison. The field keys must match the custom field keys in the API schema.


ProjectDetail

A full project detail page. Fetches a single project by slug and renders its gallery, custom fields, related projects, and a back link.

// app/projects/[slug]/page.tsx
import { ProjectDetail } from "project-portfolio"

// Must be a Server Component — do NOT add "use client"
export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <ProjectDetail
      slug={params.slug}
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      backPath="/projects"
      backLabel="All Projects"
    />
  )
}
Prop Type Required Default Description
slug string Yes The project slug to load
clientSlug string Yes The client slug that owns this project
apiBase string Yes Base URL of the projects API
backPath string No "/projects" Path for the back navigation link
backLabel string No "All Projects" Label for the back navigation link
revalidate number No 86400 Cache revalidation period in seconds (24 hours)

ProjectMenu

A compact megamenu component. Shows featured projects as list-style cards on the left and "Browse By" filter links on the right. Designed to be dropped directly into a navigation megamenu dropdown.

// components/navigation/MegaMenu.tsx
import { ProjectMenu } from "project-portfolio"

// Must be a Server Component — do NOT add "use client"
export async function ProjectsMegaMenu() {
  return (
    <ProjectMenu
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      subtitle="Our systems are installed in every geographic region of the U.S. and in a variety of applications."
      maxProjects={6}
    />
  )
}
Prop Type Required Default Description
clientSlug string Yes Identifies which client's projects to load
apiBase string Yes Base URL of the projects API
basePath string No "/projects" Base path for project detail links
viewAllPath string No Same as basePath Path for the "View All Projects" link
subtitle string No Description paragraph shown above the project cards
font string No System font stack Font family string applied to all inline styles
maxProjects number No 6 Maximum number of projects to display
revalidate number No 86400 Cache revalidation period in seconds (24 hours)
noCache boolean No false Disable caching entirely — useful for development

The filter options in the right sidebar are driven automatically by the is_filterable fields returned from the API schema — no manual configuration needed.


SimilarProjects

A standalone similar projects section. Fetches all projects for a client, filters to the same type as the current project, and renders up to 3 matching cards. Designed to be placed after ProjectDetail on a project detail page.

// app/projects/[slug]/page.tsx
import { ProjectDetail, SimilarProjects } from "project-portfolio"

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <>
      <ProjectDetail
        slug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
      />
      <SimilarProjects
        filters={{ type: "commercial" }}
        excludeSlug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
      />
    </>
  )
}

filters accepts any combination of custom field key/value pairs — all must match (AND logic). excludeSlug optionally removes the current project from results.

Prop Type Required Default Description
filters Record<string, string> No {} Key/value pairs to filter projects by custom field values. All filters must match (AND logic)
excludeSlug string No Slug of a project to exclude from results (e.g. the current project)
clientSlug string Yes Identifies which client's projects to load
apiBase string Yes Base URL of the projects API
basePath string No "/projects" Base path for project detail links
maxItems number No 3 Maximum number of projects to show
font string No System font stack Font family string applied to all inline styles
revalidate number No 60 Cache revalidation period in seconds

ProjectMenuClient + createMenuHandler

ProjectMenuClient is a "use client" component that works in any React tree — no RSC, no Suspense wrapper, no server file needed. It fetches its own data and caches it in memory for the lifetime of the page session so the API is never called twice on a hover/re-hover.

There are two ways to use it:


Option 1 — dataUrl + createMenuHandler (recommended for production)

Set up one API route once. The data is server-cached for 24 hours — most users never trigger a call to the upstream API at all.

// app/api/chisel-menu/route.ts
import { createMenuHandler } from "project-portfolio"

export const GET = createMenuHandler({
  clientSlug: "your-client-slug",
  apiBase: "https://your-api.com",
})
// In your nav/header — works in any "use client" component
import { ProjectMenuClient } from "project-portfolio"

<ProjectMenuClient
  dataUrl="/api/chisel-menu"
  basePath="/projects"
  viewAllPath="/projects"
/>

Option 2 — Direct fetch (quick setup / non-Next.js environments)

No API route needed. The component fetches directly from the upstream API on first mount and caches the result in memory for the session.

import { ProjectMenuClient } from "project-portfolio"

<ProjectMenuClient
  clientSlug="your-client-slug"
  apiBase="https://your-api.com"
  basePath="/projects"
  viewAllPath="/projects"
/>

Prop Type Required Default Description
dataUrl string No URL of a local API route created with createMenuHandler(). Recommended for production
clientSlug string No* Client slug for direct fetch mode
apiBase string No* API base URL for direct fetch mode
basePath string Yes Base path for project detail links
viewAllPath string Yes Path for the "View All Projects" link
subtitle string No Description shown above the project cards (hidden on mobile)
font string No System font stack Font family string
maxProjects number No 6 Maximum number of projects to display (capped at 3 on mobile)

*One of dataUrl or clientSlug + apiBase must be provided. Pre-fetched projects, schema, filterOptions, filterFieldKey, and fieldOptionsMap can also be passed directly for SSR usage.


Important: Server Component Usage

All top-level components (ProjectPortfolio, ProjectDetail, ProjectMenu, SimilarProjects) are async Server Components. They must be rendered in a server context:

// CORRECT
export default async function Page() {
  return <ProjectPortfolio clientSlug="..." apiBase="..." />
}

// WRONG — causes infinite client-side fetch loop
"use client"
export default function Page() {
  return <ProjectPortfolio clientSlug="..." apiBase="..." />
}

If a parent component uses "use client", these components cannot be rendered inside it directly. Pass them as children from a server component instead.


Caching

Data is cached at multiple levels depending on which component is used.

Server Components (ProjectMenu, ProjectPortfolio, ProjectDetail, SimilarProjects)

  1. Per-renderReact.cache() deduplicates duplicate calls within the same render pass
  2. Cross-requestnext: { revalidate: 86400 } (24 hours) caches responses in Next.js's Data Cache across all users and sessions

ProjectMenuClient with createMenuHandler (recommended) 3. Server-side route cache — the /api/chisel-menu route is cached by Next.js for 24 hours. The upstream API is called at most once per day regardless of traffic 4. Module-level client cache — after the first fetch within a session, the result is held in memory. Re-hovering the menu or remounting the component never triggers another network request

ProjectMenuClient with direct fetch / ProjectPortfolioClient 4. Module-level client cache only — the upstream API is called once per session per user (on first mount), then cached in memory for the rest of that page session. Filter changes on ProjectPortfolioClient never trigger a fetch — filtering is done entirely in memory

Bypassing the cache

Method Scope Use case
?bust=1 on /api/chisel-menu Single request Dev/testing after a CMS change
CHISEL_CACHE_BYPASS=true env var Entire deployment Staging environments
revalidateTag("chisel-menu-{clientSlug}") Server cache only CMS webhook on content publish
noCache: true on ProjectMenu Single render Debug during development
// CMS webhook example — invalidate server cache immediately on publish
import { revalidateTag } from "next/cache"
revalidateTag("chisel-menu-your-client-slug")

No third-party caching libraries are required.


Publishing to npm

# Log in to npm
npm login

# Build and publish
cd package
npm run build
npm publish --access public

To release an update, bump the version field in package/package.json then run npm publish again.