Fix dogfood QA: 404, content hygiene, grid UX, a11y, CTAs
- Add branded NotFound catch-all (no more blank unknown routes) - Portfolio cards: shimmer skeleton, eager first-row thumbs, real alts - Hide empty categories; rename CAD view slug to avoid collision - Resume summary preserves paragraphs/bullets - Home/About/footer hire contact mailto CTA - Sticky header scroll-padding; chip aria-labels - Security headers; Flickr import strip View On Black + junk tags - Content cleanup script for grammar, residue, tags, featured
This commit is contained in:
@@ -72,6 +72,9 @@ export function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="site-footer__links">
|
||||
<a href={`mailto:${site?.social?.email || "jmartin@jmartgraphix.com"}`}>
|
||||
Contact
|
||||
</a>
|
||||
{site?.social?.youtube && (
|
||||
<a href={site.social.youtube} target="_blank" rel="noreferrer">
|
||||
YouTube
|
||||
|
||||
@@ -27,11 +27,34 @@
|
||||
overflow: hidden;
|
||||
background: #0e0e10;
|
||||
|
||||
&.is-loading::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
#141416 0%,
|
||||
#1c1c20 40%,
|
||||
#141416 60%,
|
||||
#0e0e10 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: project-card-shimmer 1.25s ease-in-out infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.55s ease;
|
||||
opacity: 0;
|
||||
transition: transform 0.55s ease, opacity 0.35s ease;
|
||||
|
||||
&.is-loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,3 +116,12 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.35rem;
|
||||
}
|
||||
|
||||
@keyframes project-card-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Project } from "../lib/api";
|
||||
import { thumbOf } from "../lib/api";
|
||||
import { thumbOf, thumbAltOf } from "../lib/api";
|
||||
import { formatProjectYear } from "../lib/dates";
|
||||
import "./ProjectCard.scss";
|
||||
|
||||
export function ProjectCard({ project, index = 0 }: { project: Project; index?: number }) {
|
||||
const thumb = thumbOf(project);
|
||||
const alt = thumbAltOf(project);
|
||||
const year = formatProjectYear(project.date);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
// First row-ish: eager load so portfolio doesn't look empty pre-scroll
|
||||
const eager = index < 8;
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -14,9 +19,17 @@ export function ProjectCard({ project, index = 0 }: { project: Project; index?:
|
||||
style={{ animationDelay: `${Math.min(index, 12) * 40}ms` }}
|
||||
>
|
||||
<Link to={`/project/${project.slug}`} className="project-card__link">
|
||||
<div className="project-card__media">
|
||||
<div className={`project-card__media${thumb && !loaded ? " is-loading" : ""}`}>
|
||||
{thumb ? (
|
||||
<img src={thumb} alt="" loading="lazy" />
|
||||
<img
|
||||
src={thumb}
|
||||
alt={alt}
|
||||
loading={eager ? "eager" : "lazy"}
|
||||
decoding="async"
|
||||
fetchPriority={eager ? "high" : "auto"}
|
||||
onLoad={() => setLoaded(true)}
|
||||
className={loaded ? "is-loaded" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div className="project-card__placeholder" aria-hidden>
|
||||
{project.title.slice(0, 1)}
|
||||
@@ -32,9 +45,7 @@ export function ProjectCard({ project, index = 0 }: { project: Project; index?:
|
||||
{year && <span className="project-card__year">{year}</span>}
|
||||
</div>
|
||||
<h3>{project.title}</h3>
|
||||
{project.shortDescription && (
|
||||
<p>{project.shortDescription}</p>
|
||||
)}
|
||||
{project.shortDescription && <p>{project.shortDescription}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user