Add banknotes showcase

This commit is contained in:
2023-09-13 01:56:47 +01:00
parent 03d39b118b
commit 33004ae197
4 changed files with 372 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
import * as THREE from "three";
import { useLoader } from "@react-three/fiber";
import { CoinProps } from "@/types/coin";
import { useMemo } from "react";
export const BanknoteInstance = ({
color,
obverse,
reverse,
}: CoinProps) => {
const obverseTexture: THREE.Texture = useLoader(
THREE.TextureLoader,
obverse.picture,
);
const reverseTexture: THREE.Texture = useLoader(
THREE.TextureLoader,
reverse.picture,
);
const width = useMemo(() => {
try {
return 2*obverseTexture.source.data.width/obverseTexture.source.data.height
} catch {
return 4
}
}, [obverseTexture])
return (
<>
<boxGeometry
attach="geometry"
args={[width,2,0.01]}
/>
<meshStandardMaterial
color={color}
metalness={0}
roughness={0.5}
attach="material-0"
/>
<meshStandardMaterial
color={color}
metalness={0}
roughness={0.5}
attach="material-1"
/>
<meshStandardMaterial
color={color}
metalness={0}
roughness={0.5}
attach="material-2"
/>
<meshStandardMaterial
color={color}
metalness={0}
roughness={0.5}
attach="material-3"
/>
<meshStandardMaterial
color={'#888'}
map={obverseTexture}
metalness={0}
roughness={0.5}
attach="material-4"
/>
<meshStandardMaterial
color={'#888'}
map={reverseTexture}
metalness={0}
roughness={0.5}
attach="material-5"
/>
</>
);
};
+234
View File
@@ -0,0 +1,234 @@
import Head from "next/head";
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { CoinProps } from "@/types/coin";
import { Canvas, MeshProps, useFrame } from "@react-three/fiber";
import { Dispatch, SetStateAction, Suspense, useRef, useState } from "react";
import { Mesh } from "three";
import { OrbitControls } from "@react-three/drei";
import path from "path";
import { readFile, readdir } from "fs/promises";
import { BanknoteInstance } from "@/components/scene/banknote";
type Props = {
banknotes: CoinProps[];
};
const SpinninBanknote = ({
isSelected,
coinProps,
...props
}: { coinProps: CoinProps; isSelected?: boolean } & MeshProps) => {
const ref = useRef<Mesh>(null!);
useFrame((_state, delta) => {
if (isSelected) {
ref.current.rotation.y += delta;
}
});
return (
<mesh {...props}>
<mesh ref={ref}>
<BanknoteInstance {...coinProps} />
</mesh>
</mesh>
);
};
const Row = ({
banknotes,
selectedIndex,
}: Props & { selectedIndex: number }) => {
const ref = useRef<Mesh>(null!);
useFrame(() => {
const xPosition = Math.round(10 * ref.current.position.x) / 10;
const target = -selectedIndex * 8;
if (xPosition > target) {
ref.current.position.x -= Math.abs(xPosition - target) / 16;
}
if (xPosition < target) {
ref.current.position.x += Math.abs(xPosition - target) / 16;
}
});
return (
<mesh ref={ref}>
{banknotes.map((coin, index) => (
<Suspense key={coin.id} fallback={null}>
<SpinninBanknote
isSelected={selectedIndex === index}
coinProps={coin}
position={[index * 8, 1, 0]}
rotation={[0, 0, 0]}
/>
</Suspense>
))}
</mesh>
);
};
const InfoBox = ({
coin,
maxIndex,
selectIndex,
}: {
coin: CoinProps;
maxIndex: number;
selectIndex: Dispatch<SetStateAction<number>>;
}) => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
position: "absolute",
bottom: 96,
left: 0,
right: 0,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
maxWidth: "100vw",
width: 512,
marginBottom: 16,
color: "white",
}}
>
<small>{coin.issuer.name}</small>
<h1 style={{ textAlign: "center" }}>{coin.title}</h1>
<p>{coin.min_year + " - " + coin.max_year}</p>
<small>{(coin.count ?? 1) + " in collection"}</small>
</div>
<div
style={{
width: 128,
display: "flex",
gap: 8,
}}
>
<button
style={{ flex: 1 }}
onClick={() => selectIndex((value) => Math.max(value - 1, 0))}
>
{"<-"}
</button>
<button
style={{ flex: 1 }}
onClick={() => selectIndex((value) => Math.min(value + 1, maxIndex))}
>
{"->"}
</button>
</div>
</div>
);
};
const CountryPage: NextPage<Props> = ({ banknotes }) => {
const [selectedIndex, selectIndex] = useState(0);
return (
<>
<Head>
<title>Coins</title>
<meta name="description" content="My father's coin collection" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main id={"scene-root"} style={{ position: "relative" }}>
<Canvas shadows>
<color attach="background" args={["#111"]} />
<ambientLight />
<directionalLight position={[0, 0.125, 1]} />
<Row banknotes={banknotes} selectedIndex={selectedIndex} />
<OrbitControls />
</Canvas>
<div
style={{
position: "absolute",
display: "flex",
justifyContent: "space-between",
top: 0,
padding: 16,
width: "100%",
}}
>
<button
style={{
width: 128,
padding: "4px 8px",
}}
>
<a href={"/showcase-banknotes"}>{"<- All countries"}</a>
</button>
<button
style={{
width: 128,
padding: "4px 8px",
}}
>
<a href={"/showcase"}>{"Coins"}</a>
</button>
</div>
<InfoBox
coin={banknotes[selectedIndex]}
selectIndex={selectIndex}
maxIndex={banknotes.length - 1}
/>
</main>
</>
);
};
export const getStaticProps: GetStaticProps<Props> = async (ctx) => {
console.log(ctx.params?.country);
if (typeof ctx.params?.country !== "string") {
return {
notFound: true,
};
}
const coinIds = await readdir(
path.join("./public/banknotes", ctx.params?.country),
);
const banknotes: CoinProps[] = [];
for (const coinId of coinIds) {
const coin = await readFile(
path.join("./public/banknotes", ctx.params.country, coinId),
{ encoding: "utf-8" },
);
banknotes.push(JSON.parse(coin));
}
return {
props: {
banknotes: banknotes.sort((a, b) => a.min_year - b.min_year),
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const countries = await readdir("./public/banknotes");
return {
fallback: false,
paths: countries.map((country) => ({ params: { country } })),
};
};
export default CountryPage;
+57
View File
@@ -0,0 +1,57 @@
import Head from "next/head";
import { GetStaticProps, NextPage } from "next";
import { readdir } from "fs/promises";
type Props = {
countries: string[];
};
const CountriesPage: NextPage<Props> = ({ countries }) => {
return (
<>
<Head>
<title>Coins</title>
<meta name="description" content="My father's coin collection" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main
id={"scene-root"}
style={{
backgroundColor: "#111",
color: "white",
position: "relative",
display: "flex",
minHeight: "100vh",
flexDirection: "column",
gap: 16,
padding: "32px 16px",
}}
>
{countries.map((country) => (
<a key={country} href={"/showcase-banknotes/" + country}>
{country}
</a>
))}
</main>
</>
);
};
export const getStaticProps: GetStaticProps<Props> = async () => {
const countries = await readdir("./public/banknotes");
if (!Array.isArray(countries)) {
return {
notFound: true,
};
}
return {
props: {
countries,
},
};
};
export default CountriesPage;
+1
View File
@@ -23,6 +23,7 @@ const CountriesPage: NextPage<Props> = ({ countries }) => {
position: "relative",
display: "flex",
flexDirection: "column",
minHeight: "100vh",
gap: 16,
padding: "32px 16px",
}}