Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 45 additions & 27 deletions app/components/home/portfolio.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import React from "react";
import Album from '@/app/Media/Photos/Fox Album Cover Alt2.jpg';
import Ript from '@/app/Media/Photos/Ript Portfolio Icon.png';
import Dansbands from '@/app/Media/Photos/dansbands icon.png';
import Image from "next/image";
"use client";

import React, { useRef } from "react";
import Image from "next/image";
import { homepageRecentWorkItems } from "@/app/util/const";

const Portfolio = () => {
const rowRef = useRef<HTMLDivElement>(null);

const scrollRow = (direction: "next" | "prev") => {
if (!rowRef.current) {
return;
}

const cardWidth = rowRef.current.firstElementChild
? (rowRef.current.firstElementChild as HTMLElement).clientWidth
: 320;
const gap = 20;
const delta = direction === "next" ? cardWidth + gap : -(cardWidth + gap);

rowRef.current.scrollBy({ left: delta, behavior: "smooth" });
};

return (
<>
<div id="recent-work" className="smooth"></div>
Expand All @@ -17,28 +32,31 @@ const Portfolio = () => {
<h2 className="subtitle">
SELECTED PROJECTS. <a href="/portfolio">{"SEE MORE >"}</a>
</h2>
<div className="row">
<Image
alt="Fox album artwork"
src={Album}
width={300}
height={300}
sizes="(max-width: 767px) 200px, (max-width: 1199px) 200px, 300px"
/>
<Image
alt="Ript project cover"
src={Ript}
width={300}
height={300}
sizes="(max-width: 767px) 200px, (max-width: 1199px) 200px, 300px"
/>
<Image
alt="dansbands project cover"
src={Dansbands}
width={300}
height={300}
sizes="(max-width: 767px) 200px, (max-width: 1199px) 200px, 300px"
/>
<div className="portfolio-controls" aria-hidden="true">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove aria-hidden from interactive carousel controls

Applying aria-hidden="true" to the container that holds the prev/next <button> elements hides those controls from assistive technologies while they remain focusable and clickable, which creates an accessibility regression for screen-reader users on desktop. In practice, users relying on AT cannot discover or reliably operate carousel navigation even though the buttons are present visually.

Useful? React with 👍 / 👎.

<button className="carousel-btn" onClick={() => scrollRow("prev")} type="button">
</button>
<button className="carousel-btn" onClick={() => scrollRow("next")} type="button">
</button>
</div>
<div className="row" ref={rowRef}>
{homepageRecentWorkItems.map((item) => (
<a
key={item.title}
href={item.caseStudyUrl || "/portfolio"}
className="recent-work-card"
>
<Image
alt={`${item.title} preview`}
src={item.image}
width={300}
height={300}
sizes="(max-width: 767px) 76vw, (max-width: 1199px) 260px, 300px"
/>
<span>{item.title}</span>
</a>
))}
</div>
</div>
</div>
Expand Down
147 changes: 76 additions & 71 deletions app/components/home/professional.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
"use client";

import React, { useRef, useEffect } from "react";
import React, { useRef, useEffect, useState } from "react";

const panels = [
{ eyebrow: "System Architecture", title: "Composable component model", stat: "32 reusable UI patterns" },
{ eyebrow: "Interaction Design", title: "Accessible motion + meaningful states", stat: "Lighthouse a11y 100" },
{ eyebrow: "Delivery Velocity", title: "Fast iteration with stable code paths", stat: "Release-ready slices in days" },
{ eyebrow: "Product Thinking", title: "UX precision tied to business outcomes", stat: "Clear goals, measurable impact" },
];

const Professional = () => {
const professionalRef = useRef<HTMLDivElement>(null);
const animationContainerRef = useRef<HTMLDivElement>(null);
const [reduceMotion, setReduceMotion] = useState(false);

useEffect(() => {
const sectionElement = professionalRef.current;
Expand All @@ -19,7 +27,7 @@ const Professional = () => {
}
});
},
{ threshold: 0.35 }
{ threshold: 0.4 }
);

if (sectionElement) {
Expand All @@ -33,82 +41,79 @@ const Professional = () => {
};
}, []);

useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const updateMotionPreference = () => setReduceMotion(mediaQuery.matches);
updateMotionPreference();

mediaQuery.addEventListener("change", updateMotionPreference);
return () => mediaQuery.removeEventListener("change", updateMotionPreference);
}, []);

const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (reduceMotion || !animationContainerRef.current) {
return;
}

const bounds = animationContainerRef.current.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width - 0.5;
const y = (event.clientY - bounds.top) / bounds.height - 0.5;

animationContainerRef.current.style.setProperty("--pointer-x", `${x.toFixed(4)}`);
animationContainerRef.current.style.setProperty("--pointer-y", `${y.toFixed(4)}`);
};

const handlePointerLeave = () => {
if (!animationContainerRef.current) {
return;
}
animationContainerRef.current.style.setProperty("--pointer-x", "0");
animationContainerRef.current.style.setProperty("--pointer-y", "0");
};

return (
<div className="professional" ref={professionalRef}>
<div id="professional" className="smooth"></div>
<div className="container">
<h1 className="title">
<span className="gray">01</span> PROFESSIONAL
</h1>
<h2 className="subtitle">CORE TECHNOLOGIES USED IN SHIPPED WORK</h2>
<div className="visible-on-scroll" ref={animationContainerRef}>
<div id="skills">
<div id="apps">
<p>JavaScript</p>
<p>HTML5</p>
<p>CSS3</p>
<p>Express</p>
<p>React</p>
<p>NextJS</p>
<p>Figma</p>
</div>

<div className="grid">
<div className="bar pct-95">
<div className="inner"></div>
<div className="right">
<p>95%</p>
</div>
</div>
<div className="bar pct-95">
<div className="inner2"></div>
<div className="right">
<p>95%</p>
</div>
</div>
<div className="bar pct-95">
<div className="inner3"></div>
<div className="right">
<p>95%</p>
</div>
</div>
<div className="bar pct-85">
<div className="inner"></div>
<div className="right">
<p>85%</p>
</div>
</div>
<div className="bar pct-95">
<div className="inner3"></div>
<div className="right">
<p>95%</p>
</div>
</div>
<div className="bar pct-95">
<div className="inner2"></div>
<div className="right">
<p>95%</p>
</div>
</div>
<div className="bar pct-85">
<div className="inner"></div>
<div className="right">
<p>85%</p>
</div>
</div>

<div className="v-divider one"></div>
<div className="v-divider two"></div>
<div className="v-divider three"></div>

<div id="scale">
<p className="zero">0%</p>
<p className="one">25%</p>
<p className="two">50%</p>
<p className="three">75%</p>
<p className="four">100%</p>
</div>
</div>
<h2 className="subtitle">PRODUCT-QUALITY FRONTEND EXECUTION</h2>
<p className="professional-copy">
I design and ship resilient interfaces where architecture, interaction details,
and implementation quality all support measurable product outcomes.
</p>
<div
className={`visible-on-scroll professional-scene${reduceMotion ? " reduced-motion" : ""}`}
ref={animationContainerRef}
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
>
<div className="scene-grid" aria-hidden="true"></div>
<div className="scene-glow scene-glow-one" aria-hidden="true"></div>
<div className="scene-glow scene-glow-two" aria-hidden="true"></div>
<div className="professional-panels">
{panels.map((panel, index) => (
<article
key={panel.eyebrow}
className="professional-panel"
style={
{
"--depth": `${8 + index * 5}`,
"--delay": `${index * 90}ms`,
} as React.CSSProperties
}
>
<p>{panel.eyebrow}</p>
<h3>{panel.title}</h3>
<span>{panel.stat}</span>
</article>
))}
</div>
<div className="scene-footer">
<span>React + TypeScript + Next.js</span>
<span>Accessibility-first motion</span>
<span>Optimized for desktop and mobile</span>
</div>
</div>
</div>
Expand Down
Loading