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:
1 parent
e008a7c241
commit
d5f04162a8
4 files changed
+103
-1
No files matched your search
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type { PortfolioProjectView } from "@/lib/portfolio-data";
|
|||||||
import { caseStudies } from "@/lib/site-content";
|
import { caseStudies } from "@/lib/site-content";
|
||||||
import { DrawLine } from "./animations/DrawLine";
|
import { DrawLine } from "./animations/DrawLine";
|
||||||
import MagneticButton from "./cinematic/MagneticButton";
|
import MagneticButton from "./cinematic/MagneticButton";
|
||||||
|
import { ProjectCover } from "./ProjectCover";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
|
||||||
export default function Projects({
|
export default function Projects({
|
||||||
@@ -59,6 +60,7 @@ export default function Projects({
|
|||||||
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
|
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
|
||||||
transition={{ type: "spring", stiffness: 170, damping: 18 }}
|
transition={{ type: "spring", stiffness: 170, damping: 18 }}
|
||||||
>
|
>
|
||||||
|
<ProjectCover project={project} />
|
||||||
<div className="flex flex-wrap items-start justify-between gap-sp-6">
|
<div className="flex flex-wrap items-start justify-between gap-sp-6">
|
||||||
<div className="max-w-[560px]">
|
<div className="max-w-[560px]">
|
||||||
<div className="ds-kicker">{project.category}</div>
|
<div className="ds-kicker">{project.category}</div>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const NOTEBOOK_ORDER = [
|
|||||||
"as-servicos",
|
"as-servicos",
|
||||||
] as const;
|
] 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],
|
(typeof NOTEBOOK_ORDER)[number],
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
|
|||||||
@@ -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 */
|
/* Cinematic card hover effect */
|
||||||
.ds-card {
|
.ds-card {
|
||||||
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|||||||
Reference in new issue
Block a user