Skip to content

Commit

Permalink
✨ feat(app): add artwork canvas boilerplate component
Browse files Browse the repository at this point in the history
  • Loading branch information
gerdesque committed Jan 9, 2024
1 parent 566f835 commit 250cd21
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 96 deletions.
4 changes: 2 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Route, Routes } from "react-router-dom";

import Artwork from "./Artwork";
import Collection from "./Collection";
import Creation from "./Creation";
import Layout from "./Layout";
import Select from "./Select";
import Start from "./Start";
Expand All @@ -13,7 +13,7 @@ function App() {
<Route index element={<Start />} />
<Route path="selection" element={<Select />} />
<Route path="collection" element={<Collection />} />
<Route path="creation" element={<Creation />} />
<Route path="creation" element={<Artwork />} />
</Route>
</Routes>
);
Expand Down
117 changes: 117 additions & 0 deletions src/components/Artwork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @ts-expect-error konsta typing
import { Block, Button, Icon } from "konsta/react";
import Konva from "konva";
import { MutableRefObject, ReactElement, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { MdDownloadForOffline, MdShare } from "react-icons/md";
import { Image, Layer, Stage } from "react-konva";
import { Link } from "react-router-dom";
import useImage from "use-image";

import useStore, { CanvasImageProps } from "../store";

const FrameImage = () => {
const [image] = useImage("/canvas.webp");
return <Image image={image} />;
};

const CanvasImage = ({ canvasImage }: { canvasImage: CanvasImageProps }) => {
const [image] = useImage(canvasImage.image);
const transformCanvasImage = useStore((state) => state.transformCanvasImage);

const handleDragStart = () => {
transformCanvasImage({ ...canvasImage, isDragging: true });
};
const handleDragEnd = (e: any) => {
transformCanvasImage({
...canvasImage,
isDragging: false,
x: e.target.x(),
y: e.target.y(),
});
};

return (
<Image
image={image}
key={canvasImage.id}
id={canvasImage.id}
x={canvasImage.x}
y={canvasImage.y}
draggable
scaleX={canvasImage.isDragging ? 1.2 : 1}
scaleY={canvasImage.isDragging ? 1.2 : 1}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
/>
);
};

const Canvas = () => {
const { canvasList } = useStore();
const stageRef = useRef() as MutableRefObject<Konva.Stage>;
const sceneWidth = 1080;
const sceneHeight = 1296;

useEffect(() => {
const fitStageIntoParentContainer = () => {
const containerWidth = window.innerWidth || 0;
const scale = containerWidth / sceneWidth;

if (stageRef.current) {
stageRef.current.width(sceneWidth * scale);
stageRef.current.height(sceneHeight * scale);
stageRef.current.scale({ x: scale, y: scale });
}
};

fitStageIntoParentContainer();

window.addEventListener("resize", fitStageIntoParentContainer);

return () => {
window.removeEventListener("resize", fitStageIntoParentContainer);
};
}, []);

return (
<div>
<Stage width={sceneWidth} height={sceneHeight} ref={stageRef}>
<Layer>
<FrameImage />
{canvasList.map((canvasImage) => (
<CanvasImage key={canvasImage.id} canvasImage={canvasImage} />
))}
</Layer>
</Stage>
</div>
);
};

function Artwork() {
const { t } = useTranslation();
const { canvasList } = useStore();

return (
<Block className="flex flex-col flex-wrap gap-4 container mx-auto justify-center content-center text-center">
<div className="p-2 m-4">
<h1 className="text-2xl">{t("artworkTitle")}</h1>
</div>
{canvasList.length === 0 ? (
<Button className="p-2 rounded-full text-xl" rounded inline outline>
<Link to={"/selection"}>{t("artworkEmpty")}</Link>
</Button>
) : (
<Canvas />
)}
<Button className="p-2 rounded-full" rounded inline outline>
<Icon material={<MdShare className="w-6 h-6" />} />
</Button>
<Button className="p-2 rounded-full" rounded inline outline>
<Icon material={<MdDownloadForOffline className="w-6 h-6" />} />
</Button>
</Block>
);
}

export default Artwork;
35 changes: 22 additions & 13 deletions src/components/Collection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// @ts-expect-error konsta typing
import { Block, Button, Card, Link, Navbar, Page, Popup } from "konsta/react";
import { Suspense, lazy, useState } from "react";
import { useTranslation } from "react-i18next";
import { Masonry } from "react-plock";

import useStore, { Image } from "../store";
import useStore, { ImageProps } from "../store";
import utils from "../utils";

const Viewer = lazy(() => import("@samvera/clover-iiif/viewer"));
Expand All @@ -29,31 +30,37 @@ const options = {
};

function Collection() {
const { t } = useTranslation();

const { imageLikeList } = useStore();
const addToCanvas = useStore((state) => state.addToCanvas);

const [selectedImage, setSelectedImage] = useState<Image | null>(null);
const [selectedImage, setSelectedImage] = useState<ImageProps | null>(null);
const [popupOpened, setPopupOpened] = useState(false);

const handleImageClick = (image: Image) => {
const handleImageClick = (image: ImageProps) => {
setSelectedImage(image);
setPopupOpened(true);
};

const addFrameToCanvas = async (imageURL: string) => {
const addFrameToCanvas = async (id: string, imageURL: string) => {
try {
const croppedImagePath = await utils.getCroppedImagePath(imageURL);
console.log("Cropped image path: ", croppedImagePath);
if (croppedImagePath) {
addToCanvas(croppedImagePath);
addToCanvas(id, croppedImagePath);
}
} catch (error) {
console.error("Error:", error);
}
};

return (
<>
<div className="flex flex-col justify-evenly text-center">
<div className="p-2 m-4">
<h1 className="text-2xl">{t("collectionTitle")}</h1>
</div>

<Card className="h-auto rounded-none">
<Masonry
items={imageLikeList}
Expand Down Expand Up @@ -82,31 +89,33 @@ function Collection() {
>
<Page>
<Navbar
title="Popup"
title={selectedImage.name}
right={
<Link navbar onClick={() => setPopupOpened(false)}>
Close
{t("collectionClose")}
</Link>
}
/>
<Block className="space-y-4">
<Button
className="px-4 py-2 rounded-full mr-2"
onClick={() => addFrameToCanvas(selectedImage.identifier)}
className="px-4 py-2 rounded-full mr-2 text-xl"
onClick={() =>
addFrameToCanvas(selectedImage.id, selectedImage.identifier)
}
rounded
inline
outline
>
Add frame to canvas
{t("collectionAddText")}
</Button>
<Suspense fallback={<h2>🌀 Loading...</h2>}>
<Suspense fallback={<h2>🌀 {t("collectionLoading")}</h2>}>
<Viewer iiifContent={selectedImage.url} options={options} />
</Suspense>
</Block>
</Page>
</Popup>
)}
</>
</div>
);
}

Expand Down
44 changes: 0 additions & 44 deletions src/components/Creation.tsx

This file was deleted.

43 changes: 30 additions & 13 deletions src/components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import axios from "axios";
// @ts-expect-error konsta typing
import { Button, Card, Icon } from "konsta/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOutlineThumbDown, MdOutlineThumbUp } from "react-icons/md";
import TinderCard from "react-tinder-card";

import api from "../api";
import useStore, { Image } from "../store";
import useStore, { ImageProps } from "../store";
import utils from "../utils";

export interface Element {
Expand All @@ -33,8 +34,10 @@ export interface API {
}

function Selection() {
const { t } = useTranslation();

const [currentIndex, setCurrentIndex] = useState(0);
const [data, setData] = useState<Image[]>([]);
const [data, setData] = useState<ImageProps[]>([]);

const likeImage = useStore((state) => state.likeImage);

Expand All @@ -48,7 +51,7 @@ function Selection() {
const getValidIIIFIdentifier = async (iiifManifest: string) => {
try {
const imageURL = await utils.fetchIIIFIdentifier(iiifManifest);
console.log("Image URL: ", imageURL);
// console.log("Image URL: ", imageURL);

return imageURL;
} catch (error) {
Expand All @@ -71,18 +74,20 @@ function Selection() {
creator: element.creatorLabel?.value,
url: element.iiifManifest?.value,
identifier: identifier,
image: `${identifier}/full/full/0/default.jpg`,
image: `${identifier}/full/400,/0/default.jpg`,
thumbnail: `${identifier}/full/100,100/0/default.jpg`,
};
}
return null;
});

const imageResults = await Promise.all(imagePromises);
const images = imageResults.filter((image) => image !== null) as Image[];
const images = imageResults.filter(
(image) => image !== null,
) as ImageProps[];

console.log("Data elements: ", elements);
console.log("Data images: ", images);
// console.log("Data elements: ", elements);
// console.log("Data images: ", images);
setData(images);
}, []);

Expand All @@ -108,14 +113,14 @@ function Selection() {

const swiped = (direction: Direction, index: number) => {
if (direction === "right") {
console.log(currentImage);
// console.log(currentImage);
likeImage(currentImage);
}
updateCurrentIndex(index + 1);
};

const outOfFrame = (name: string, idx: number) => {
console.log(`${name} (${idx}) left the screen!`, currentIndexRef.current);
// console.log(`${name} (${idx}) left the screen!`, currentIndexRef.current);
currentIndexRef.current >= idx && childRef.current?.restoreCard();
};

Expand All @@ -128,7 +133,11 @@ function Selection() {
const currentImage = data[currentIndex];

return (
<div className="flex flex-col justify-evenly">
<div className="flex flex-col justify-evenly text-center">
<div className="p-2 m-4">
<h1 className="text-2xl">{t("selectionTitle")}</h1>
</div>

<div className="max-w-2xl mx-auto h-2/3">
<Card className="flex">
{currentImage && (
Expand Down Expand Up @@ -174,9 +183,17 @@ function Selection() {
<h2 className="text-2xl font-bold mb-2 text-black">
{currentImage?.name}
</h2>
<p className="text-gray-600">Year: {currentImage?.year}</p>
<p className="text-gray-600">Creator: {currentImage?.creator}</p>
<p className="text-gray-600">Location: {currentImage?.location}</p>
<p className="text-gray-600">
{t("selectionYear")} {currentImage?.year || t("selectionUnknown")}
</p>
<p className="text-gray-600">
{t("selectionCreator")}{" "}
{currentImage?.creator || t("selectionUnknown")}
</p>
<p className="text-gray-600">
{t("selectionLocation")}{" "}
{currentImage?.location || t("selectionUnknown")}
</p>
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"translation": {
"artwork": "Meine Leinwand",
"artworkEmpty": "Füge deine persönlichen Favoriten hinzu.",
"artworkTitle": "Gestalte mit einzelnen Bildelementen dein ganz eigenes europäisches Kunstwerk und teile es mit anderen.",
"chooseLanguage": "Sprache wählen:",
"collection": "Meine Sammlung",
Expand Down
Loading

0 comments on commit 250cd21

Please sign in to comment.