Skip to content

Commit

Permalink
overhaul image ui
Browse files Browse the repository at this point in the history
  • Loading branch information
fidoriel committed Oct 27, 2024
1 parent f92a854 commit 367d1fc
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MeshVault

> Disclaimer: Do not use in any production like environment until the first version has been released. There __WILL__ be breaking changes and no migration guides. You have been warned. Have fun.
> Disclaimer: Do not use in any production like environment until the first version has been released. There **WILL** be breaking changes and no migration guides. You have been warned. Have fun.
A blazingly fast and simple self-hosted 3D files platform written in rust and typescript centered around a 3D model packaging format.

Expand Down
139 changes: 112 additions & 27 deletions frontend/Model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Download, Heart, MoreVertical, RefreshCcw, Bookmark } from "lucide-react";

import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import { DetailedModelResponse } from "./bindings";
import { BACKEND_BASE_URL } from "./lib/api";
import { saveAs } from "file-saver";
import { ChevronLeft, ChevronRight } from "lucide-react";

import {
DropdownMenu,
Expand All @@ -16,6 +17,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AspectRatio } from "./components/ui/aspect-ratio";

function OptionsDropdownMenu() {
return (
Expand All @@ -36,39 +38,122 @@ function OptionsDropdownMenu() {
}

function ImageGallery({ model }: { model: DetailedModelResponse }) {
const [selectedImage, setSelectedImage] = useState(model.images[0] || undefined);
const [selectedImage, setSelectedImage] = useState<number>(0);
const thumbnailsRef = useRef<HTMLDivElement>(null);

const nextImage = () => {
setSelectedImage((prev) => (prev + 1) % model.images.length);
};

const previousImage = () => {
setSelectedImage((prev) => (prev - 1 + model.images.length) % model.images.length);
};

// Scroll selected thumbnail into view
useEffect(() => {
const thumbnailsContainer = thumbnailsRef.current;
if (!thumbnailsContainer) return;

const selectedThumbnail = thumbnailsContainer.children[selectedImage] as HTMLElement;
if (!selectedThumbnail) return;

const scrollLeft =
selectedThumbnail.offsetLeft - thumbnailsContainer.offsetWidth / 2 + selectedThumbnail.offsetWidth / 2;
thumbnailsContainer.scrollTo({
left: scrollLeft,
behavior: "smooth",
});
}, [selectedImage]);

// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
previousImage();
} else if (e.key === "ArrowRight") {
nextImage();
}
};

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);

Check warning on line 80 in frontend/Model.tsx

View workflow job for this annotation

GitHub Actions / lint_ts

React Hook useEffect has missing dependencies: 'nextImage' and 'previousImage'. Either include them or remove the dependency array

const scrollThumbnails = (direction: "left" | "right") => {
const thumbnailsContainer = thumbnailsRef.current;
if (!thumbnailsContainer) return;

const scrollAmount = 200; // Adjust this value to control scroll distance
const newScrollLeft = thumbnailsContainer.scrollLeft + (direction === "left" ? -scrollAmount : scrollAmount);
thumbnailsContainer.scrollTo({
left: newScrollLeft,
behavior: "smooth",
});
};

return (
<div className="w-full max-w-4xl">
<Card className="mb-4">
<Card className="mb-4 relative group">
<CardContent className="p-0">
<img
src={`${BACKEND_BASE_URL}${selectedImage}`}
alt="Model Preview"
className="w-full h-full object-cover rounded-lg"
/>
<AspectRatio ratio={4 / 3}>
<img
src={`${BACKEND_BASE_URL}${model.images[selectedImage]}`}
alt="Model Preview"
className="w-full h-full object-cover rounded-lg"
/>
</AspectRatio>
<button
onClick={previousImage}
className="absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Previous image"
>
<ChevronLeft className="h-6 w-6" />
</button>
<button
onClick={nextImage}
className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Next image"
>
<ChevronRight className="h-6 w-6" />
</button>
</CardContent>
</Card>

<div className="flex gap-1 mb-1 overflow-x-auto">
{model.images.map((img, index) => (
<div key={index} className="p-1">
<button
className={
img == selectedImage
? "flex-shrink-0 outline-none ring-2 ring-blue-500 rounded-lg"
: "flex-shrink-0 rounded-lg"
}
>
<img
onClick={() => setSelectedImage(img)}
src={`${BACKEND_BASE_URL}${img}`}
alt={`Preview ${index + 1}`}
className="w-16 h-16 object-cover rounded-lg hover:opacity-80 transition-opacity"
/>
</button>
</div>
))}
<div className="relative">
<button
onClick={() => scrollThumbnails("left")}
className="absolute left-0 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-1 z-10"
aria-label="Scroll thumbnails left"
>
<ChevronLeft className="h-4 w-4" />
</button>

<div ref={thumbnailsRef} className="flex gap-2 overflow-x-auto pb-2 px-8 scroll-smooth scrollbar-hide">
{model.images.map((img, index) => (
<div key={index} className="w-20 h-20 flex-shrink-0 p-1 pb-2">
<button
onClick={() => setSelectedImage(index)}
className={`w-full h-full relative rounded-lg overflow-hidden ${
index === selectedImage ? "ring-2 ring-offset-2" : "hover:opacity-80"
}`}
>
<img
src={`${BACKEND_BASE_URL}${img}`}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>
</button>
</div>
))}
</div>

<button
onClick={() => scrollThumbnails("right")}
className="absolute right-0 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-1 z-10"
aria-label="Scroll thumbnails right"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions frontend/components/ui/aspect-ratio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";

const AspectRatio = AspectRatioPrimitive.Root;

export { AspectRatio };
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"precommit": "npm run format && npm run lint:fix && npm run build"
},
"dependencies": {
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
Expand Down

0 comments on commit 367d1fc

Please sign in to comment.