Add banknotes showcase
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -23,6 +23,7 @@ const CountriesPage: NextPage<Props> = ({ countries }) => {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
gap: 16,
|
||||
padding: "32px 16px",
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user