Fikirtepe Urban Renewal
One of Istanbul's largest urban renewal projects — 15,000 housing units modernised across the Fikirtepe district.
- Istanbul, Kadıköy
- 2023
Project and case-study card with editorial status states and grid or feature layouts — overlay links and soft-failure item handling.
Use when listing portfolio / case-study / completed-projects cards on a public page or embedded business-profile widget. The grid variant fits magazine grids (compose with magazine-layout + filter-bar + page-hero for the full page assembly with zero new code); the feature variant fits embedded mosaic widgets (designed for the future bento-grid-01 layout). Status is editorial — set on the data object by an editor, NOT derived from a clock; differs from event-card's time-window kernel by design. The public PROJECT_STATUS_CONFIG export lets consumers build status legends, count summaries, and filter rows that share the same color / label vocabulary as the card. Migration origin: kasder kas-social-front-v0 ProjectCard.tsx + BusinessProjectsSection.tsx.
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/project-cardAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/project-card-fixturesGrid variant — vertical image-on-top, hover-reveal "View details" CTA, lift-on-hover. Compose with magazine-layout for the public projects-page assembly (see usage notes).
One of Istanbul's largest urban renewal projects — 15,000 housing units modernised across the Fikirtepe district.
Earthquake-resistant modern housing complex built in the aftermath of the 2020 İzmir earthquake.
Net-zero carbon-footprint living district powered by renewable energy and integrated mobility.
Restoration of the UNESCO World Heritage historical centre of Bursa, balancing daily life with preservation.
Modern, accessible public-space design along the Black Sea coastline, integrating recreation and ecology.
Conversion of the historic port district into a tourism and culture hub — currently in detailed-planning phase.
Reach for ProjectCard when you need a project / case-study / portfolio preview with a 3-state editorial status (completed / in-progress / planned). Two layouts: variant="grid"(vertical image-on-top with a hover-reveal "View details" CTA) and variant="feature" (full-bleed image background, white-on-dark text, designed for embedded mosaic widgets).
import { ProjectCard } from "@/components/project-card";
<ProjectCard
project={{
id: "fikirtepe-renewal",
title: "Fikirtepe Urban Renewal",
category: "Urban Renewal",
location: "Istanbul, Kadıköy",
year: "2023",
image: "/cover.jpg",
description: "15,000 housing units modernised across the district.",
status: "ongoing",
}}
variant="grid"
href="/projects/fikirtepe-renewal"
/>;grid— vertical image-on-top with status pill (top-left), category pill (top-right), primary-tinted gradient overlay, hover-reveal "View details" CTA, lift-on-hover. Renders well in 1/2/3-column responsive grids — pair with magazine-layout.feature — full-bleed image background, white-on-dark content overlaid at the bottom (title + 2-line description). Status pill top-right, category top-left. NO meta row, NO hover-CTA — denser and quieter than grid. Designed for embedded widgets inside bento-grid-01 (deferred) or any consumer-driven sized parent.Status is set on the data object by an editor. The card does NOT derive status from a date or completion-percentage — projects don't have a time-window kernel. The status drives the pill color + label via PROJECT_STATUS_CONFIG:
completed — primary lime pill. Project is done; reads celebratory.ongoing — chart-3 teal pill. Project is currently active. (Originally proposed bg-accent but pro-ui's --accent is a near-white surface token, not a brand color — teal is the readable middle ground.)planned — muted-grey pill with subtle border. Project is scheduled but not yet started; reads quiet, not attention-grabbing.PROJECT_STATUS_CONFIG + ProjectStatus are exported alongside the card. Consumers can read the same color / label map for status legends, filter rows, count summaries — without rendering a card. Pure data, server-component-importable, tree-shakeable.
import {
PROJECT_STATUS_CONFIG,
type ProjectStatus,
} from "@/components/project-card";
function StatusSummary({ projects }: { projects: { status: ProjectStatus }[] }) {
const counts = projects.reduce<Record<ProjectStatus, number>>(
(acc, p) => ({ ...acc, [p.status]: (acc[p.status] ?? 0) + 1 }),
{ completed: 0, ongoing: 0, planned: 0 },
);
return (
<div className="flex gap-3">
{(Object.keys(counts) as ProjectStatus[]).map((s) => (
<span key={s} className={`px-2 py-0.5 rounded-full text-xs ${PROJECT_STATUS_CONFIG[s].className}`}>
{PROJECT_STATUS_CONFIG[s].label}: {counts[s]}
</span>
))}
</div>
);
}Pass linkComponent to swap the underlying anchor:
import NextLink from "next/link";
import { ProjectCard } from "@/components/project-card";
<ProjectCard
project={project}
variant="grid"
href={`/projects/${project.id}`}
linkComponent={NextLink}
/>;href precedence: getHref(project) wins over href wins over project.href wins over "#" fallback.
Default behavior renders a universal Building2 icon + a white-translucent chip on top of the image. Override per-category via the categoryStyles map (default: empty). Each entry can provide a className, an icon, or both:
import { Trees, Shield } from "lucide-react";
<ProjectCard
project={project}
variant="grid"
href={`/projects/${project.id}`}
// Solid backgrounds (90% opacity) for over-image legibility — light tints
// (bg-X/15 text-X) read poorly over photographic content. Use solid fills with
// text-white or text-{token}-foreground.
categoryStyles={{
"Sustainable Development": {
className: "bg-chart-3/90 text-white",
icon: Trees,
},
"Disaster Management": {
className: "bg-warning/90 text-warning-foreground",
icon: Shield,
},
}}
/>;Pass a partial labels object — only the keys you want to override:
<ProjectCard
project={project}
variant="grid"
href={`/projects/${project.id}`}
labels={{
completed: "Tamamlandı",
ongoing: "Devam Ediyor",
planned: "Planlanan",
viewDetails: "Detayları Gör",
featuredAriaLabel: "Öne çıkan proje",
}}
/>;Drop interactive children into actions — they sit at z-10 over the link overlay. When supplied, the category pill yields its top-right slot to actions and moves to bottom-right (grid) OR the status pill yields and stacks under the category pill at top-left (feature). Each nested button MUST call e.stopPropagation()or the card's link will fire too.
<ProjectCard
project={project}
variant="grid"
href={`/projects/${project.id}`}
actions={
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleBookmark(project.id);
}}
aria-label="Bookmark"
>
<Bookmark className="size-4" />
</button>
}
/>;The card is a leaf. Compose the full page from already-shipped pro-comps — zero new code beyond the data layer:
import { useState } from "react";
import NextLink from "next/link";
import { Building2 } from "lucide-react";
import { PageHero } from "@/components/page-hero";
import { FilterBar } from "@/components/filter-bar";
import {
MagazineLayout,
useMagazineFilter,
} from "@/components/magazine-layout";
import { ProjectCard } from "@/components/project-card";
export default function ProjectsPage({ allProjects }) {
const [category, setCategory] = useState<string | null>(null);
const filtered = useMagazineFilter({
items: allProjects,
pageSize: 6,
filterPredicate: (p) => !category || p.category === category,
simulatedLoadingMs: 500,
});
return (
<MagazineLayout
hero={
<PageHero
badge="Projects"
badgeIcon={Building2}
title="Transformations We've Delivered"
titleHighlight="Across Türkiye"
description="Urban renewal, disaster management, and sustainable-development projects."
/>
}
filterBar={
<FilterBar
categories={["Urban Renewal", "Disaster Management", "Sustainable Development"]}
category={category}
onCategoryChange={setCategory}
hideSearch
hideDateRange
/>
}
displayedItems={filtered.displayedItems}
hasMore={filtered.hasMore}
isLoading={filtered.isLoading}
onLoadMore={filtered.loadMore}
renderItem={(project) => (
<ProjectCard
key={project.id}
project={project}
variant="grid"
href={`/projects/${project.id}`}
linkComponent={NextLink}
/>
)}
/>
);
}feature variant sizing contractThe feature variant uses absolute inset-0 for its image and requires a sized parent — the card does NOT impose a default aspect ratio. Without a sized container, the card collapses to zero height. The future bento-grid-01 (layout) will absorb this responsibility; until then, drive sizing with auto-rows-[180px] on the grid container or pass className="lg:col-span-2 lg:row-span-1" per card.
image empty string ⇒ bg-muted placeholder with centered Building2 icon. No broken-image icon.imageAlt undefined ⇒ falls back to title.location / year undefined ⇒ that meta cell omitted (grid). Both undefined ⇒ entire <ul> not rendered.feature variant never renders the meta row regardless (matches source DNA).href / getHref / project.href ⇒ link falls to "#".aria-labelledby pointing to the useId()-generated <h3>id. The link's accessible name is the title — not a flattened blob.ariaLabel when needed (e.g. translated titles).motion-safe:. Reduced-motion users see static cards.<Star> icon prefix on the title (aria-hidden) plus an sr-only announcement (labels.featuredAriaLabel).ArrowRight on the grid hover-CTA is mirrored via rtl:rotate-180 for right-to-left locales.React.memo at the export. Pass stable project references from your data layer to keep memo effective.<img> uses loading="lazy" by default — override via the loading prop for above-fold cards.PROJECT_STATUS_CONFIG) is pure data with zero React imports — safe to import in Server Components.