1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 19:15:30 -04:00

fix: image comparison component weirdness

This commit is contained in:
2026-01-28 17:11:28 -05:00
parent 4dca81b58a
commit 9afe8abfed
6 changed files with 52 additions and 126 deletions
+18 -108
View File
@@ -1,40 +1,11 @@
/* eslint-disable jsx-a11y/alt-text, @next/next/no-img-element */
"use client";
import { useState, useRef, useEffect, Children } from "react";
import { Children } from "react";
import { getImageProps } from "next/image";
import { ReactCompareSlider, ReactCompareSliderImage } from "react-compare-slider";
import { cn } from "@/lib/utils";
import { ChevronsLeftRightIcon } from "lucide-react";
const ImageDiff = ({ children, className }: { children: React.ReactElement[]; className?: string }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [sliderPosition, setSliderPosition] = useState(50);
const [isDragging, setIsDragging] = useState(false);
// Add event listeners to handle dragging outside the component
useEffect(() => {
const handleMouseUpGlobal = () => {
setIsDragging(false);
};
const handleMouseMoveGlobal = (e: MouseEvent) => {
if (!isDragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
setSliderPosition(percentage);
};
document.addEventListener("mouseup", handleMouseUpGlobal);
document.addEventListener("mousemove", handleMouseMoveGlobal);
return () => {
document.removeEventListener("mouseup", handleMouseUpGlobal);
document.removeEventListener("mousemove", handleMouseMoveGlobal);
};
}, [isDragging]);
// Extract the two image children
const childrenArray = Children.toArray(children);
if (childrenArray.length !== 2) {
@@ -42,88 +13,27 @@ const ImageDiff = ({ children, className }: { children: React.ReactElement[]; cl
return null;
}
// Get the original image source to extract dimensions for aspect ratio
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const beforeImageProps = getImageProps(children[0].props as any).props;
const firstChildProps = children[0].props as any;
const imageSrc = firstChildProps.src;
const aspectRatio =
typeof imageSrc === "object" && "width" in imageSrc && "height" in imageSrc
? imageSrc.width / imageSrc.height
: 16 / 9;
// Extract image props, stripping out MDX className (margins, etc.) that would break slider layout
const beforeImageProps = getImageProps(firstChildProps).props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const afterImageProps = getImageProps(children[1].props as any).props;
const handleMouseDown = () => {
setIsDragging(true);
};
const handleMouseUp = () => {
setIsDragging(false);
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isDragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
setSliderPosition(percentage);
};
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const touch = e.touches[0];
const rect = containerRef.current.getBoundingClientRect();
const x = Math.max(0, Math.min(touch.clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
setSliderPosition(percentage);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
const step = 5;
if (e.key === "ArrowLeft") {
setSliderPosition((prev) => Math.max(0, prev - step));
e.preventDefault();
} else if (e.key === "ArrowRight") {
setSliderPosition((prev) => Math.min(100, prev + step));
e.preventDefault();
}
};
return (
<div
ref={containerRef}
onMouseMove={handleMouseMove}
onTouchMove={handleTouchMove}
onKeyDown={handleKeyDown}
className={cn("relative isolate w-full max-w-full overflow-hidden select-none", className)}
style={{ ["--slider-position" as string]: `${sliderPosition}%` }}
role="slider"
aria-label="Image comparison slider"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={sliderPosition}
tabIndex={0}
>
{/* After image (full width, underneath) */}
<div className="h-full w-full">
<img {...afterImageProps} className="h-full w-full object-cover object-top-left" />
</div>
{/* Before image (clipped with width based on slider position) */}
<div className="absolute top-0 left-0 h-full w-[var(--slider-position)] overflow-hidden">
<img {...beforeImageProps} className="h-full w-full object-cover object-top-left" />
</div>
{/* Divider line */}
<div className="bg-muted-foreground absolute top-0 bottom-0 left-[var(--slider-position)] w-1 -translate-x-1/2 drop-shadow-md" />
{/* Slider handle */}
<div
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onTouchEnd={handleMouseUp}
className="bg-muted absolute top-1/2 left-[var(--slider-position)] flex size-10 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize touch-none items-center justify-center rounded-full border-2 drop-shadow-md"
aria-hidden="true"
>
<ChevronsLeftRightIcon className="text-foreground/70 size-6" />
</div>
</div>
<ReactCompareSlider
className={cn("my-8 w-full max-w-full overflow-hidden rounded-sm", className)}
style={{ aspectRatio }}
itemOne={<ReactCompareSliderImage {...beforeImageProps} className="size-full object-cover" />}
itemTwo={<ReactCompareSliderImage {...afterImageProps} className="size-full object-cover" />}
/>
);
};
+8 -7
View File
@@ -5,20 +5,21 @@ import siteConfig from "@/lib/config/site";
const Footer = () => {
return (
<footer className="text-muted-foreground mt-8 w-full py-6 text-center text-[13px] leading-loose">
Content <Link href="/license">licensed under {siteConfig.license}</Link>,{" "}
<Link href="/previously" title="Previously on...">
{siteConfig.copyrightYearStart}
</Link>{" "}
2026.{" "}
All content is licensed under{" "}
<Link href="/license" className="underline underline-offset-4">
{siteConfig.license}
</Link>
. Code is{" "}
<a
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
target="_blank"
rel="noopener noreferrer"
title="View Source on GitHub"
className="font-medium underline underline-offset-4"
className="underline underline-offset-4"
>
View source.
open source
</a>
.
</footer>
);
};
+4 -4
View File
@@ -62,19 +62,19 @@ const PostStats = ({ slug }: { slug: string }) => {
return (
<>
{viewCount > 0 && (
<Badge variant="secondary" className="tabular-nums">
<EyeIcon className="text-foreground/85" aria-hidden="true" />
<Badge variant="secondary" className="text-foreground/80 gap-[5px] tabular-nums">
<EyeIcon className="text-foreground/65" aria-hidden="true" />
{numberFormatter.format(viewCount)}
</Badge>
)}
{commentCount > 0 && (
<Badge variant="secondary" className="tabular-nums" asChild>
<Badge variant="secondary" className="text-foreground/80 gap-[5px] tabular-nums" asChild>
<Link
href={`/${slug}#comments`}
title={`${numberFormatter.format(commentCount)} ${commentCount === 1 ? "comment" : "comments"}`}
>
<MessagesSquareIcon className="text-foreground/85" aria-hidden="true" />
<MessagesSquareIcon className="text-foreground/65" aria-hidden="true" />
{numberFormatter.format(commentCount)}
</Link>
</Badge>