Skip to content

Using Components

Guide for integrating Projex components in Next.js pages and building layouts.

Component Patterns

Projex uses compound components for maximum flexibility. Compose your own layout using modular pieces.

Data Fetching Pattern

Fetch data at build time using normalise:

tsx
import type { ProjexProject } from '@manningworks/projex'

async function getProjects(): Promise<ProjexProject[]> {
  const { projects: projectInputs } = await import('../projex.config')
  const { normalise } = await import('@manningworks/projex')
  return Promise.all(projectInputs.map((input) => normalise(input)))
}

Page Component Structure

Portfolio Page (App Router)

tsx
import { ProjectCard } from '@manningworks/projex'
import type { ProjexProject } from '@manningworks/projex'

async function getProjects(): Promise<ProjexProject[]> {
  const { projects: projectInputs } = await import('../projex.config')
  const { normalise } = await import('@manningworks/projex')
  return Promise.all(projectInputs.map((input) => normalise(input)))
}

export default async function PortfolioPage() {
  const projects = await getProjects()

  return (
    <main>
      <h1>My Projects</h1>
      <div className="grid grid-cols-3 gap-4">
        {projects.map((project) => (
          <ProjectCard key={project.id}>
            <ProjectCard.Header project={project} />
            <ProjectCard.Description project={project} />
            <ProjectCard.Tags project={project} />
            <ProjectCard.Stats project={project} />
            <ProjectCard.Status project={project} />
            <ProjectCard.Links project={project} />
          </ProjectCard>
        ))}
      </div>
    </main>
  )
}

Portfolio Page (Pages Router)

tsx
import { GetServerSideProps } from 'next'
import { ProjectCard } from '@manningworks/projex'
import type { ProjexProject } from '@manningworks/projex'
import { projects as projectInputs } from '../projex.config'

export default function PortfolioPage({ projects }: { projects: ProjexProject[] }) {
  return (
    <main>
      <h1>My Projects</h1>
      <div className="grid grid-cols-3 gap-4">
        {projects.map((project) => (
          <ProjectCard key={project.id}>
            <ProjectCard.Header project={project} />
            <ProjectCard.Description project={project} />
            <ProjectCard.Tags project={project} />
            <ProjectCard.Stats project={project} />
            <ProjectCard.Status project={project} />
            <ProjectCard.Links project={project} />
          </ProjectCard>
        ))}
      </div>
    </main>
  )
}

export const getServerSideProps: GetServerSideProps = async () => {
  const { normalise } = await import('@manningworks/projex')
  const projects = await Promise.all(projectInputs.map((input) => normalise(input)))

  return {
    props: { projects }
  }
}

Layout Patterns

Grid Layout

Use ProjectGrid for responsive card grids:

tsx
import { ProjectGrid } from '@manningworks/projex'

<div className="portfolio">
  <ProjectGrid>
    {projects.map((project) => (
      <ProjectCard key={project.id}>
        <ProjectCard.Header project={project} />
        <ProjectCard.Description project={project} />
        <ProjectCard.Stats project={project} />
        <ProjectCard.Links project={project} />
      </ProjectCard>
    ))}
  </ProjectGrid>
</div>

List Layout

Use ProjectList for compact project lists:

tsx
import { ProjectList } from '@manningworks/projex'

<div className="portfolio">
  <ProjectList>
    {projects.map((project) => (
      <ProjectCard key={project.id}>
        <ProjectCard.Header project={project} />
        <ProjectCard.Description project={project} />
        <ProjectCard.Links project={project} />
      </ProjectCard>
    ))}
  </ProjectList>
</div>

Use FeaturedProject for hero sections:

tsx
import { FeaturedProject } from '@manningworks/projex'

const featuredProject = projects.find(p => p.featured)

<div className="portfolio">
  {featuredProject && (
    <FeaturedProject project={featuredProject} />
  )}

  <ProjectGrid>
    {projects.map((project) => (
      <ProjectCard key={project.id}>
        <ProjectCard.Header project={project} />
        <ProjectCard.Description project={project} />
        <ProjectCard.Stats project={project} />
        <ProjectCard.Links project={project} />
      </ProjectCard>
    ))}
  </ProjectGrid>
</div>

Interactive Features

Search and Filter

Combine search and filters for interactive portfolios:

tsx
'use client'

import { useState } from 'react'
import {
  ProjectSearch,
  ProjectFilterBar,
  ProjectFilterTag,
  useProjectSearch,
  useProjectFilters,
} from '@manningworks/projex'

function InteractivePortfolio({ projects }) {
  const [query, setQuery] = useState('')
  const [selectedTags, setSelectedTags] = useState<string[]>([])

  // Extract all unique tags
  const allTags = [...new Set(projects.flatMap(p => p.stack))]

  // Apply filters
  const filteredByTags = useProjectFilters(projects, selectedTags)
  const filteredProjects = useProjectSearch(filteredByTags, query)

  const toggleTag = (tag: string) => {
    setSelectedTags(prev =>
      prev.includes(tag)
        ? prev.filter(t => t !== tag)
        : [...prev, tag]
    )
  }

  return (
    <div>
      <ProjectSearch onSearch={setQuery} placeholder="Search projects..." />

      <ProjectFilterBar>
        {allTags.map(tag => (
          <ProjectFilterTag
            key={tag}
            label={tag}
            isActive={selectedTags.includes(tag)}
            onClick={toggleTag}
          />
        ))}
      </ProjectFilterBar>

      <div className="grid">
        {filteredProjects.map(project => (
          <ProjectCard key={project.id}>
            <ProjectCard.Header project={project} />
            <ProjectCard.Description project={project} />
            <ProjectCard.Stats project={project} />
            <ProjectCard.Links project={project} />
          </ProjectCard>
        ))}
      </div>
    </div>
  )
}

Sorting

Add sorting capabilities:

tsx
'use client'

import { useState, useMemo } from 'react'
import { ProjectSort } from '@manningworks/projex'
import { sortByName, sortByDate, sortByStars } from '@manningworks/projex'

function SortedPortfolio({ projects }) {
  const [sortBy, setSortBy] = useState('name')

  const sortedProjects = useMemo(() => {
    switch (sortBy) {
      case 'name': return sortByName(projects)
      case 'date': return sortByDate(projects)
      case 'stars': return sortByStars(projects)
      default: return projects
    }
  }, [projects, sortBy])

  return (
    <div>
      <ProjectSort
        options={['Name', 'Date', 'Stars']}
        value={sortBy}
        onChange={setSortBy}
      />

      <div className="grid">
        {sortedProjects.map(project => (
          <ProjectCard key={project.id}>
            <ProjectCard.Header project={project} />
            <ProjectCard.Description project={project} />
            <ProjectCard.Stats project={project} />
            <ProjectCard.Links project={project} />
          </ProjectCard>
        ))}
      </div>
    </div>
  )
}

Project Detail Pages

Create dynamic routes for individual projects:

tsx
import { notFound } from 'next/navigation'
import { ProjectView } from '@manningworks/projex'
import type { ProjexProject } from '@manningworks/projex'
import { projects as projectInputs } from '../../projex.config'

async function getProject(id: string): Promise<ProjexProject | null> {
  const input = projectInputs.find((p) => p.id === id)
  if (!input) return null

  const { normalise } = await import('@manningworks/projex')
  return normalise(input)
}

export async function generateStaticParams() {
  return projectInputs.map((project) => ({
    id: project.id,
  }))
}

export default async function ProjectPage({ params }: { params: { id: string } }) {
  const project = await getProject(params.id)

  if (!project) {
    notFound()
  }

  return (
    <main>
      <ProjectView project={project} />
    </main>
  )
}

Conditional Rendering

Components render nothing for empty data:

tsx
<ProjectCard>
  <ProjectCard.Header project={project} />
  <ProjectCard.Description project={project} />
  <ProjectCard.Tags project={project} />
  <ProjectCard.Stats project={project} />
  <ProjectCard.Status project={project} />
  <ProjectCard.Links project={project} />
</ProjectCard>
  • Description returns null if project.description is empty
  • Tags returns null if project.stack is empty
  • Stats returns null if project.stats is empty
  • Links returns null if no links are available

Styling Components

All components render semantic HTML with data attributes. Style with CSS:

css
[data-projex-card] {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1.5rem;
}

[data-projex-grid] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
}

See Styling Guide for comprehensive CSS examples.

Client vs Server Components

Server Components (Default)

Use for static content and build-time data fetching:

tsx
// Default - runs on server
export default async function PortfolioPage() {
  const projects = await getProjects()
  return <ProjectGrid>{/* ... */}</ProjectGrid>
}

Client Components

Use for interactive features (search, filter, sort):

tsx
'use client'

import { useState } from 'react'

function InteractivePortfolio({ projects }) {
  const [query, setQuery] = useState('')
  return <ProjectSearch onSearch={setQuery} />
}

Mix server and client components:

tsx
// Server component
export default async function PortfolioPage() {
  const projects = await getProjects()
  return <InteractivePortfolio projects={projects} />
}

// Client component
'use client'
function InteractivePortfolio({ projects }) {
  return <ProjectSearch onSearch={/*...*/} />
}

Best Practices

  1. Fetch data once at build time, pass to client components
  2. Use compound components for flexible layouts
  3. Leverage empty rendering - components handle missing data
  4. Separate concerns - server for data, client for interactivity
  5. Use data attributes for styling, not CSS classes

Next Steps