feat(projects): capa nos cards de projeto (captura real ou painel gerado em codigo)

- ProjectCover usa as capturas ja publicadas (next/image, webp/avif) e, sem captura, um painel com monograma e stack
- .ds-cover mantem proporcao 16:10 e nao encolhe no card flex

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Evandro Fonseca JuniorandClaude Sonnet 5.5 committed 2026-10-04 23:40:25 -03:00
1 parent e008a7c241
commit d5f04162a8
4 files changed
+103 -1

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
import Image from "next/image";
import { SCREENSHOT_BY_PROJECT_ID } from "./story/notebookProjects";
type CoverProject = { id: string; title: string };
const SCREENSHOTS: Record<string, string | undefined> =
SCREENSHOT_BY_PROJECT_ID;
function initials(title: string) {
return title
.split(/\s+/)
.filter((word) => word.length > 2 || /^[A-Z]/.test(word))
.slice(0, 2)
.map((word) => word[0])
.join("")
.toUpperCase();
}
/**
* Capa do card de projeto: a captura de tela real quando existe; sem captura, um painel gerado em código
* (só o monograma, sem repetir a stack que já aparece nas tags do card) em vez de uma imagem inventada.
*/
export function ProjectCover({
project,
priority = false,
}: {
project: CoverProject;
priority?: boolean;
}) {
const src = SCREENSHOTS[project.id];
if (src) {
return (
<div className="ds-cover">
<Image
src={src}
alt={`Captura de tela de ${project.title}`}
width={1600}
height={1000}
sizes="(min-width: 1360px) 33vw, (min-width: 1025px) 50vw, 100vw"
priority={priority}
/>
</div>
);
}
return (
<div className="ds-cover ds-cover-fallback" aria-hidden="true">
<span className="ds-cover-mark">{initials(project.title)}</span>
</div>
);
}
+2
View File
@@ -6,6 +6,7 @@ import type { PortfolioProjectView } from "@/lib/portfolio-data";
import { caseStudies } from "@/lib/site-content";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
import { ProjectCover } from "./ProjectCover";
import { Section } from "./Section";
export default function Projects({
@@ -59,6 +60,7 @@ export default function Projects({
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
transition={{ type: "spring", stiffness: 170, damping: 18 }}
>
<ProjectCover project={project} />
<div className="flex flex-wrap items-start justify-between gap-sp-6">
<div className="max-w-[560px]">
<div className="ds-kicker">{project.category}</div>
+2 -1
View File
@@ -20,7 +20,8 @@ const NOTEBOOK_ORDER = [
"as-servicos",
] as const;
const SCREENSHOT_BY_PROJECT_ID: Record<
/** Capturas de tela publicadas; os cards de projeto reutilizam as mesmas imagens. */
export const SCREENSHOT_BY_PROJECT_ID: Record<
(typeof NOTEBOOK_ORDER)[number],
string
> = {
+47
View File
@@ -942,6 +942,53 @@ textarea {
}
}
/* Capa dos cards de projeto: captura real ou painel gerado em codigo */
.ds-cover {
position: relative;
flex: none; /* o card e uma coluna flex: sem isto a capa encolhe e perde a proporcao */
aspect-ratio: 16 / 10;
overflow: hidden;
margin-bottom: var(--sp-5);
border: 1px solid var(--border);
background: var(--ink);
& img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
transition: transform 0.7s var(--ease-out);
}
.ds-card:hover & img {
transform: scale(1.035);
}
}
.ds-section-dark .ds-cover {
border-color: rgba(255, 255, 255, 0.1);
}
.ds-cover-fallback {
display: grid;
place-items: center;
padding: var(--sp-4);
color: var(--paper);
background:
radial-gradient(120% 90% at 100% 0%, var(--slash-dim), transparent 60%),
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 100% 32px,
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 32px 100%,
var(--ink);
& .ds-cover-mark {
font-family: var(--font-display);
font-size: clamp(48px, 7vw, 96px);
font-weight: 800;
letter-spacing: -0.05em;
color: var(--slash);
}
}
/* Cinematic card hover effect */
.ds-card {
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);