← Files WixARCHIVED FILE

skills/wix-headless-templates/portfolio/project/src/components/portfolio/CollectionProjectsView.tsx

3.85 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// REFERENCE collection page: collection header + its projects grid on the @theme tokens.
// Correct and complete; per the skill's model you design and build your own on
// useCollectionProjects (ProjectCard is reusable for an all-work grid or a home strip).
import type { ComponentType, ReactNode } from "react";
import { useCollectionProjects } from "../../hooks/portfolio/useCollectionProjects";
import { imgAttrs } from "../../wix/media";
import type { CollectionSummary, ProjectSummary } from "../../wix/portfolio/types";

export interface LinkLikeProps {
  href: string;
  className?: string;
  children?: ReactNode;
}

const PlainLink = ({ href, className, children }: LinkLikeProps) => (
  <a href={href} className={className}>
    {children}
  </a>
);

export interface ProjectCardProps {
  project: ProjectSummary;
  projectHref?: (slug: string) => string;
  LinkComponent?: ComponentType<LinkLikeProps>;
}

export function ProjectCard({
  project,
  projectHref = (slug) => `/projects/${slug}`,
  LinkComponent = PlainLink,
}: ProjectCardProps) {
  return (
    <LinkComponent href={projectHref(project.slug)} className="group block no-underline">
      <div className="relative aspect-[4/3] overflow-hidden rounded-lg bg-secondary">
        {project.imageUrl && (
          <img
            {...imgAttrs(project.imageUrl, "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw", 0.75)}
            alt={project.title}
            className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
          />
        )}
      </div>
      <p className="mt-3 text-sm font-medium text-foreground">{project.title}</p>
      {project.description && (
        <p className="mt-0.5 text-xs text-muted-foreground">{project.description}</p>
      )}
    </LinkComponent>
  );
}

export interface CollectionProjectsViewProps {
  slug: string;
  initialCollection?: CollectionSummary;
  initialProjects?: ProjectSummary[];
  emptyMessage?: string;
  projectHref?: ProjectCardProps["projectHref"];
  LinkComponent?: ComponentType<LinkLikeProps>;
  CardComponent?: ComponentType<ProjectCardProps>;
}

export default function CollectionProjectsView({
  slug,
  initialCollection,
  initialProjects,
  emptyMessage = "No projects in this collection yet.",
  projectHref,
  LinkComponent,
  CardComponent = ProjectCard,
}: CollectionProjectsViewProps) {
  const { collection, notFound, projects, error } = useCollectionProjects(slug, {
    initialCollection,
    initialProjects,
  });

  if (notFound) {
    return <p className="py-16 text-center text-muted-foreground">Collection not found.</p>;
  }

  return (
    <div>
      <header className="mb-8">
        <h1 className="text-2xl font-semibold tracking-tight">
          {collection ? collection.title : <span className="inline-block h-7 w-48 animate-pulse rounded bg-secondary" />}
        </h1>
        {collection?.description && (
          <p className="mt-2 max-w-2xl text-muted-foreground">{collection.description}</p>
        )}
      </header>
      {error && <p className="mb-4 text-sm text-destructive">{error}</p>}
      {projects === null ? (
        <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
          {Array.from({ length: 6 }, (_, i) => (
            <div key={i}>
              <div className="aspect-[4/3] animate-pulse rounded-lg bg-secondary" />
              <div className="mt-3 h-3.5 w-2/3 animate-pulse rounded bg-secondary" />
            </div>
          ))}
        </div>
      ) : projects.length === 0 ? (
        <p className="py-16 text-center text-muted-foreground">{emptyMessage}</p>
      ) : (
        <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
          {projects.map((p) => (
            <CardComponent key={p.id} project={p} projectHref={projectHref} LinkComponent={LinkComponent} />
          ))}
        </div>
      )}
    </div>
  );
}

SHA-256: 7810f56fc3f79d1cfbafb9965566371c64131e6d28562951829b3e300024f69d