Skip to content

Commit 52ac95c

Browse files
Link Jam racing shirt photo to shop (#481)
1 parent 17b5421 commit 52ac95c

2 files changed

Lines changed: 92 additions & 20 deletions

File tree

app/actions/jam/y2026/public/photo-moments.test.browser.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,32 @@ describe("Jam2026PhotoMoments", () => {
4040
expect(firstWindow.style.transform).toBe("translate(32px, 48px)");
4141
});
4242

43+
it("links the racing shirt photo to the shop without dragging", async (t) => {
44+
let result = render(
45+
<Jam2026PhotoMoments popInBaseDelay={0} popInStagger={0} />,
46+
);
47+
t.after(result.cleanup);
48+
49+
let shirtWindow = getPhotoWindow(result.container, "hero-racing-shirt");
50+
let shirtLink = shirtWindow.querySelector("a");
51+
52+
expect(shirtLink?.getAttribute("href")).toBe(
53+
"https://shop.remix.run/products/remix-tee?discount=START_YOUR_ENGINES",
54+
);
55+
expect(shirtLink?.getAttribute("target")).toBe(null);
56+
expect(shirtLink?.getAttribute("aria-label")).toBe("Shop Remix tee");
57+
58+
await result.act(() => {
59+
shirtWindow.dispatchEvent(pointerEvent("pointerdown", 100, 100));
60+
shirtWindow.dispatchEvent(pointerEvent("pointermove", 140, 160));
61+
shirtWindow.dispatchEvent(pointerEvent("pointerup", 140, 160));
62+
});
63+
64+
shirtWindow = getPhotoWindow(result.container, "hero-racing-shirt");
65+
expect(shirtWindow.style.transform).toBe("translate(0px, 0px)");
66+
expect(shirtWindow.getAttribute("data-dragging")).toBe(null);
67+
});
68+
4369
it("closes windows with Escape and moves focus to the next close button", async (t) => {
4470
let result = render(
4571
<Jam2026PhotoMoments popInBaseDelay={0} popInStagger={0} />,

app/actions/jam/y2026/public/photo-moments.tsx

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type PhotoMoment = {
2323
xCompact: number;
2424
y: number;
2525
yCompact: number;
26+
href?: string;
27+
linkLabel?: string;
2628
};
2729

2830
type Jam2026PhotoMomentsProps = {
@@ -70,6 +72,8 @@ const PHOTO_MOMENTS = [
7072
xCompact: -136,
7173
y: 571,
7274
yCompact: 365,
75+
href: "https://shop.remix.run/products/remix-tee?discount=START_YOUR_ENGINES",
76+
linkLabel: "Shop Remix tee",
7377
},
7478
{
7579
id: "speaker-ryan",
@@ -223,17 +227,23 @@ export let Jam2026PhotoMoments = clientEntry(
223227
let state = runtime[moment.id];
224228
if (!state?.isOpen) return null;
225229

230+
let { href, linkLabel = moment.filename }: PhotoMoment = moment;
231+
let draggable = !href;
232+
226233
return (
227234
<article
228235
key={moment.id}
229236
data-dragging={drag?.id === moment.id ? "true" : undefined}
230237
data-photo-window-id={moment.id}
231238
mix={[
232239
photoMomentPositionStyle,
240+
draggable ? photoMomentDraggableStyle : undefined,
233241
moment.anchor === "right"
234242
? photoMomentRightStyle
235243
: photoMomentLeftStyle,
236-
on("pointerdown", (event) => handleStart(event, moment.id)),
244+
draggable
245+
? on("pointerdown", (event) => handleStart(event, moment.id))
246+
: undefined,
237247
on("focusin", () => {
238248
bringToFront(moment.id);
239249
handle.update();
@@ -288,15 +298,34 @@ export let Jam2026PhotoMoments = clientEntry(
288298
</button>
289299
<p mix={jam2026WindowTitleStyle}>{moment.filename}</p>
290300
</div>
291-
<div mix={photoMomentImageFrameStyle}>
292-
<img
293-
alt=""
294-
decoding="async"
295-
draggable={false}
296-
loading="eager"
297-
src={moment.src}
298-
/>
299-
</div>
301+
{href ? (
302+
<a
303+
aria-label={linkLabel}
304+
href={href}
305+
mix={[
306+
photoMomentImageFrameStyle,
307+
photoMomentImageLinkStyle,
308+
]}
309+
>
310+
<img
311+
alt=""
312+
decoding="async"
313+
draggable={false}
314+
loading="eager"
315+
src={moment.src}
316+
/>
317+
</a>
318+
) : (
319+
<div mix={photoMomentImageFrameStyle}>
320+
<img
321+
alt=""
322+
decoding="async"
323+
draggable={false}
324+
loading="eager"
325+
src={moment.src}
326+
/>
327+
</div>
328+
)}
300329
</div>
301330
</div>
302331
</article>
@@ -326,19 +355,22 @@ let photoLayerStyle = css({
326355
});
327356

328357
let photoMomentPositionStyle = css({
329-
cursor: "grab",
330358
pointerEvents: "auto",
331359
position: "absolute",
332360
top: "var(--jam-2026-photo-y)",
361+
[breakpointMedia.xl]: {
362+
top: "var(--jam-2026-photo-y-wide)",
363+
},
364+
});
365+
366+
let photoMomentDraggableStyle = css({
367+
cursor: "grab",
333368
touchAction: "none",
334369
userSelect: "none",
335370
WebkitUserSelect: "none",
336371
'&[data-dragging="true"]': {
337372
cursor: "grabbing",
338373
},
339-
[breakpointMedia.xl]: {
340-
top: "var(--jam-2026-photo-y-wide)",
341-
},
342374
});
343375

344376
let photoMomentLeftStyle = css({
@@ -385,9 +417,10 @@ let photoMomentSurfaceStyle = css({
385417
transformOrigin: "50% 50%",
386418
transition:
387419
"transform 350ms cubic-bezier(0.22, 1.5, 0.36, 1), box-shadow 350ms cubic-bezier(0.22, 1.5, 0.36, 1)",
388-
"[data-photo-window-id]:hover &": {
389-
transform: "rotate(-2deg)",
390-
},
420+
"[data-photo-window-id]:hover &, [data-photo-window-id]:has(:focus-visible) &":
421+
{
422+
transform: "rotate(-2deg)",
423+
},
391424
"[data-dragging='true'] &": {
392425
boxShadow:
393426
"0 2px 4px light-dark(rgb(8 40 69 / 0.08), rgb(0 0 0 / 0.24)), 0 8px 16px light-dark(rgb(8 40 69 / 0.07), rgb(0 0 0 / 0.28)), 0 20px 36px light-dark(rgb(8 40 69 / 0.08), rgb(0 0 0 / 0.32)), 0 40px 72px light-dark(rgb(8 40 69 / 0.1), rgb(0 0 0 / 0.38))",
@@ -398,9 +431,10 @@ let photoMomentSurfaceStyle = css({
398431
},
399432
"@media (prefers-reduced-motion: reduce)": {
400433
transition: "none",
401-
"[data-photo-window-id]:hover &": {
402-
transform: "rotate(0deg)",
403-
},
434+
"[data-photo-window-id]:hover &, [data-photo-window-id]:has(:focus-visible) &":
435+
{
436+
transform: "rotate(0deg)",
437+
},
404438
"[data-dragging='true'] &": {
405439
transform: "none",
406440
},
@@ -466,3 +500,15 @@ let photoMomentImageFrameStyle = css({
466500
WebkitUserDrag: "none",
467501
},
468502
});
503+
504+
let photoMomentImageLinkStyle = css({
505+
color: "inherit",
506+
cursor: "pointer",
507+
display: "block",
508+
outline: "none",
509+
textDecoration: "none",
510+
"&:focus-visible": {
511+
outline: `2px solid ${jamTheme.brandRed}`,
512+
outlineOffset: "2px",
513+
},
514+
});

0 commit comments

Comments
 (0)