Add zustand
This commit is contained in:
+3
-1
@@ -16,8 +16,10 @@
|
|||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-div-100vh": "^0.7.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"three": "0.152.2"
|
"three": "0.152.2",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.6.0",
|
"@types/node": "20.6.0",
|
||||||
|
|||||||
@@ -41,33 +41,15 @@ export const BanknoteInstance = ({ obverse, reverse }: CoinProps) => {
|
|||||||
<>
|
<>
|
||||||
<boxGeometry attach="geometry" args={[width, 2, 0.005]} />
|
<boxGeometry attach="geometry" args={[width, 2, 0.005]} />
|
||||||
|
|
||||||
<meshStandardMaterial
|
{Array.from({ length: 4 }, (_, index) => (
|
||||||
color={"#888"}
|
<meshStandardMaterial
|
||||||
metalness={0}
|
key={index}
|
||||||
roughness={0.5}
|
color={"#888"}
|
||||||
attach="material-0"
|
metalness={0}
|
||||||
/>
|
roughness={0.5}
|
||||||
|
attach={"material-" + index}
|
||||||
<meshStandardMaterial
|
/>
|
||||||
color={"#888"}
|
))}
|
||||||
metalness={0}
|
|
||||||
roughness={0.5}
|
|
||||||
attach="material-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meshStandardMaterial
|
|
||||||
color={"#888"}
|
|
||||||
metalness={0}
|
|
||||||
roughness={0.5}
|
|
||||||
attach="material-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meshStandardMaterial
|
|
||||||
color={"#888"}
|
|
||||||
metalness={0}
|
|
||||||
roughness={0.5}
|
|
||||||
attach="material-3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
color={"#888"}
|
color={"#888"}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.canvas {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { CoinInstances } from "./coin-instances";
|
|||||||
import { CoinProps } from "@/types/coin";
|
import { CoinProps } from "@/types/coin";
|
||||||
import { PlaneProps, Physics, usePlane } from "@react-three/cannon";
|
import { PlaneProps, Physics, usePlane } from "@react-three/cannon";
|
||||||
import { Mesh } from "three";
|
import { Mesh } from "three";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
coins: CoinProps[];
|
coins: CoinProps[];
|
||||||
@@ -31,8 +32,7 @@ const Lightbulb = (props: PointLightProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Pile = ({ coins }: Props) => (
|
export const Pile = ({ coins }: Props) => (
|
||||||
<Canvas camera={{ position: [0, 16, 16] }}>
|
<Canvas camera={{ position: [0, 16, 16] }} className={styles.canvas}>
|
||||||
<color attach="background" args={["#111"]} />
|
|
||||||
<ambientLight />
|
<ambientLight />
|
||||||
|
|
||||||
<Lightbulb position={[0, 16, 0]} />
|
<Lightbulb position={[0, 16, 0]} />
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
inset: -25% 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -1,173 +1,61 @@
|
|||||||
|
import styles from "./index.module.css";
|
||||||
import { CoinProps } from "@/types/coin";
|
import { CoinProps } from "@/types/coin";
|
||||||
import { Canvas, MeshProps, useFrame } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { CoinInstance } from "@/components/coin";
|
|
||||||
import { Mesh } from "three";
|
|
||||||
import { BanknoteInstance } from "@/components/banknote";
|
|
||||||
import { InfoBox } from "./info-box";
|
import { InfoBox } from "./info-box";
|
||||||
import { OrbitControls } from "@react-three/drei";
|
import { OrbitControls } from "@react-three/drei";
|
||||||
import { OrbitControls as OrbitControlsType } from "three-stdlib";
|
import { Row } from "./row";
|
||||||
|
import { useShowcaseStore } from "./store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
category: "coins" | "banknotes";
|
category: "coins" | "banknotes";
|
||||||
types: CoinProps[];
|
types: CoinProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpinninType = ({
|
|
||||||
isSelected,
|
|
||||||
isSpinning,
|
|
||||||
category,
|
|
||||||
typeProps,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
typeProps: CoinProps;
|
|
||||||
isSelected?: boolean;
|
|
||||||
isSpinning?: boolean;
|
|
||||||
} & MeshProps &
|
|
||||||
Pick<Props, "category">) => {
|
|
||||||
const ref = useRef<Mesh>(null!);
|
|
||||||
const Instance = category === "banknotes" ? BanknoteInstance : CoinInstance;
|
|
||||||
|
|
||||||
useFrame((_state, delta) => {
|
|
||||||
if (!isSelected || !isSpinning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category === "banknotes") {
|
|
||||||
ref.current.rotation.x += delta;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (typeProps.orientation) {
|
|
||||||
case "medal":
|
|
||||||
ref.current.rotation.x += delta;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
ref.current.rotation.z += delta;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh {...props}>
|
|
||||||
<mesh
|
|
||||||
ref={ref}
|
|
||||||
{...(category !== "banknotes"
|
|
||||||
? {
|
|
||||||
scale: [
|
|
||||||
(typeProps.size ?? 16) / 16,
|
|
||||||
(typeProps.thickness ?? 2) / 16,
|
|
||||||
(typeProps.size ?? 16) / 16,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: { rotation: [Math.PI / 2, Math.PI, Math.PI / 2] })}
|
|
||||||
>
|
|
||||||
<Instance {...typeProps} />
|
|
||||||
</mesh>
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Row = ({
|
|
||||||
category,
|
|
||||||
types,
|
|
||||||
selectedIndex,
|
|
||||||
isSpinning,
|
|
||||||
}: Props & { selectedIndex: number; isSpinning?: boolean }) => {
|
|
||||||
const ref = useRef<Mesh>(null!);
|
|
||||||
const spacing = category === "banknotes" ? 8 : 6;
|
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
const xPosition = Math.round(10 * ref.current.position.x) / 10;
|
|
||||||
const target = -selectedIndex * spacing;
|
|
||||||
|
|
||||||
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}>
|
|
||||||
{types.map(
|
|
||||||
(type, index) =>
|
|
||||||
Math.abs(index - selectedIndex) <= 1 && (
|
|
||||||
<Suspense key={type.id} fallback={null}>
|
|
||||||
<SpinninType
|
|
||||||
category={category}
|
|
||||||
isSpinning={isSpinning}
|
|
||||||
isSelected={selectedIndex === index}
|
|
||||||
typeProps={type}
|
|
||||||
position={[index * spacing, 1, 0]}
|
|
||||||
rotation={[Math.PI / 2, Math.PI / 2, 0]}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Showcase = (props: Props) => {
|
export const Showcase = (props: Props) => {
|
||||||
const [selectedIndex, selectIndex] = useState(0);
|
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||||
const [isSpinning, setIsSpinning] = useState(true);
|
const nextIndex = useShowcaseStore((state) => state.nextIndex);
|
||||||
const orbitControlsRef = useRef<OrbitControlsType>(null!);
|
const previousIndex = useShowcaseStore((state) => state.previousIndex);
|
||||||
const toggleSpin = useCallback(() => {
|
const init = useShowcaseStore((state) => state.init);
|
||||||
setIsSpinning((value) => {
|
const toggleSpin = useShowcaseStore((state) => state.toggleSpin);
|
||||||
if (!value) {
|
const setOrbitControlsRef = useShowcaseStore(
|
||||||
orbitControlsRef.current?.setAzimuthalAngle(0);
|
(state) => state.setOrbitControlsRef,
|
||||||
orbitControlsRef.current?.setPolarAngle(Math.PI / 2);
|
);
|
||||||
return true;
|
|
||||||
}
|
useEffect(() => init(props), [init, props]);
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = ({ key }: KeyboardEvent) => {
|
const onKeyDown = ({ key }: KeyboardEvent) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
return selectIndex((value) => Math.max(value - 1, 0));
|
return previousIndex();
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
return selectIndex((value) =>
|
return nextIndex();
|
||||||
Math.min(value + 1, props.types.length - 1),
|
|
||||||
);
|
|
||||||
case " ":
|
case " ":
|
||||||
return toggleSpin();
|
return toggleSpin();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", onKeyDown);
|
document.addEventListener("keydown", onKeyDown);
|
||||||
return () => document.removeEventListener("keydown", onKeyDown);
|
return () => document.removeEventListener("keydown", onKeyDown);
|
||||||
}, [props.types.length, toggleSpin]);
|
}, [nextIndex, previousIndex, toggleSpin]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.wrapper}>
|
||||||
<Canvas>
|
<Canvas className={styles.canvas}>
|
||||||
<color attach="background" args={["#111"]} />
|
|
||||||
<ambientLight />
|
<ambientLight />
|
||||||
<directionalLight position={[0, 0.125, 1]} />
|
<directionalLight position={[0, 0.125, 1]} />
|
||||||
<directionalLight position={[0, 0.125, -1]} />
|
<directionalLight position={[0, 0.125, -1]} />
|
||||||
<Row {...props} selectedIndex={selectedIndex} isSpinning={isSpinning} />
|
<Row />
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
ref={orbitControlsRef}
|
ref={setOrbitControlsRef}
|
||||||
enableRotate={!isSpinning}
|
enableRotate={!isSpinning}
|
||||||
enablePan={false}
|
enablePan={false}
|
||||||
|
minDistance={2}
|
||||||
|
maxDistance={10}
|
||||||
/>
|
/>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
<InfoBox
|
<InfoBox />
|
||||||
category={props.category}
|
</div>
|
||||||
type={props.types[selectedIndex]}
|
|
||||||
selectIndex={selectIndex}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
isSpinning={isSpinning}
|
|
||||||
toggleSpin={toggleSpin}
|
|
||||||
maxIndex={props.types.length - 1}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
import { CoinProps } from "@/types/coin";
|
import { useShowcaseStore } from "./store";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
|
export const InfoBox = () => {
|
||||||
|
const category = useShowcaseStore((state) => state.category);
|
||||||
|
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||||
|
const getType = useShowcaseStore((state) => state.getType);
|
||||||
|
const toggleSpin = useShowcaseStore((state) => state.toggleSpin);
|
||||||
|
const currentIndex = useShowcaseStore((state) => state.currentIndex);
|
||||||
|
const nextIndex = useShowcaseStore((state) => state.nextIndex);
|
||||||
|
const previousIndex = useShowcaseStore((state) => state.previousIndex);
|
||||||
|
const total = useShowcaseStore((state) => state.types.length);
|
||||||
|
|
||||||
|
const currentType = getType();
|
||||||
|
if (!currentType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
issuer,
|
||||||
|
title,
|
||||||
|
min_year,
|
||||||
|
max_year,
|
||||||
|
count = 1,
|
||||||
|
weight,
|
||||||
|
size,
|
||||||
|
thickness,
|
||||||
|
} = currentType;
|
||||||
|
|
||||||
export const InfoBox = ({
|
|
||||||
category,
|
|
||||||
type,
|
|
||||||
maxIndex,
|
|
||||||
selectIndex,
|
|
||||||
selectedIndex,
|
|
||||||
isSpinning,
|
|
||||||
toggleSpin,
|
|
||||||
}: {
|
|
||||||
category: "banknotes" | "coins";
|
|
||||||
type: CoinProps;
|
|
||||||
maxIndex: number;
|
|
||||||
selectIndex: Dispatch<SetStateAction<number>>;
|
|
||||||
selectedIndex: number;
|
|
||||||
isSpinning: boolean;
|
|
||||||
toggleSpin: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -25,7 +33,7 @@ export const InfoBox = ({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 96,
|
bottom: 32,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
}}
|
}}
|
||||||
@@ -42,19 +50,15 @@ export const InfoBox = ({
|
|||||||
color: "white",
|
color: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<small>{type.issuer.name}</small>
|
<small>{issuer.name}</small>
|
||||||
<h1 style={{ textAlign: "center" }}>{type.title}</h1>
|
<h1 style={{ textAlign: "center" }}>{title}</h1>
|
||||||
<p>
|
<p>{min_year !== max_year ? min_year + " - " + max_year : min_year}</p>
|
||||||
{type.min_year !== type.max_year
|
<small>{(count ?? 1) + " in collection"}</small>
|
||||||
? type.min_year + " - " + type.max_year
|
|
||||||
: type.min_year}
|
|
||||||
</p>
|
|
||||||
<small>{(type.count ?? 1) + " in collection"}</small>
|
|
||||||
{category === "coins" && (
|
{category === "coins" && (
|
||||||
<>
|
<>
|
||||||
{type.weight && <p>{type.weight + "g"}</p>}
|
{weight && <p>{weight + "g"}</p>}
|
||||||
{type.size && <p>{type.size + "mm"}</p>}
|
{size && <p>{size + "mm"}</p>}
|
||||||
{type.thickness && <p>{type.thickness + "mm"}</p>}
|
{thickness && <p>{thickness + "mm"}</p>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -74,19 +78,19 @@ export const InfoBox = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
disabled={selectedIndex <= 0}
|
disabled={currentIndex <= 0}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onClick={() => selectIndex((value) => Math.max(value - 1, 0))}
|
onClick={previousIndex}
|
||||||
>
|
>
|
||||||
{"<-"}
|
{"<-"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span>{selectedIndex + 1 + "/" + (maxIndex + 1)}</span>
|
<span>{currentIndex + 1 + "/" + total}</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={selectedIndex >= maxIndex}
|
disabled={currentIndex >= total - 1}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onClick={() => selectIndex((value) => Math.min(value + 1, maxIndex))}
|
onClick={nextIndex}
|
||||||
>
|
>
|
||||||
{"->"}
|
{"->"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { MeshProps, useFrame } from "@react-three/fiber";
|
||||||
|
import { Suspense, useRef } from "react";
|
||||||
|
import { CoinInstance } from "@/components/coin";
|
||||||
|
import { Mesh } from "three";
|
||||||
|
import { BanknoteInstance } from "@/components/banknote";
|
||||||
|
import { useShowcaseStore } from "./store";
|
||||||
|
import { CoinProps } from "@/types/coin";
|
||||||
|
|
||||||
|
const SpinninType = ({
|
||||||
|
typeProps,
|
||||||
|
isSelected,
|
||||||
|
...props
|
||||||
|
}: MeshProps & {
|
||||||
|
typeProps?: CoinProps;
|
||||||
|
isSelected?: boolean;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<Mesh>(null!);
|
||||||
|
const category = useShowcaseStore((state) => state.category);
|
||||||
|
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||||
|
const isTransitioning = useShowcaseStore((state) => state.isTransitioning);
|
||||||
|
const Instance = category === "banknotes" ? BanknoteInstance : CoinInstance;
|
||||||
|
|
||||||
|
useFrame((_state, delta) => {
|
||||||
|
if (!isSelected || !isSpinning || !typeProps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category === "banknotes") {
|
||||||
|
ref.current.rotation.x += delta;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeProps.orientation) {
|
||||||
|
case "medal":
|
||||||
|
ref.current.rotation.x += delta;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ref.current.rotation.z += delta;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!typeProps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { size = 16, thickness = 2 } = typeProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh {...props}>
|
||||||
|
<mesh
|
||||||
|
ref={ref}
|
||||||
|
{...(category !== "banknotes"
|
||||||
|
? {
|
||||||
|
scale: [size / 16, thickness / 16, size / 16],
|
||||||
|
}
|
||||||
|
: { rotation: [Math.PI / 2, Math.PI, Math.PI / 2] })}
|
||||||
|
{...(!isSelected &&
|
||||||
|
!isTransitioning && {
|
||||||
|
scale: [0, 0, 0],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Instance {...typeProps} />
|
||||||
|
</mesh>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Row = () => {
|
||||||
|
const ref = useRef<Mesh>(null!);
|
||||||
|
const category = useShowcaseStore((state) => state.category);
|
||||||
|
const currentIndex = useShowcaseStore((state) => state.currentIndex);
|
||||||
|
const getType = useShowcaseStore((state) => state.getType);
|
||||||
|
const isTransitioning = useShowcaseStore((state) => state.isTransitioning);
|
||||||
|
const endTransition = useShowcaseStore((state) => state.endTransition);
|
||||||
|
const types = useShowcaseStore((state) => state.types);
|
||||||
|
const spacing = category === "banknotes" ? 8 : 6;
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!isTransitioning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = -currentIndex * spacing;
|
||||||
|
const diff = ref.current.position.x - target;
|
||||||
|
|
||||||
|
if (Math.abs(diff) > 1 / 32) {
|
||||||
|
ref.current.position.x -= diff / 16;
|
||||||
|
} else {
|
||||||
|
console.log("ending transition");
|
||||||
|
endTransition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh ref={ref}>
|
||||||
|
{types.map(
|
||||||
|
(type, index) =>
|
||||||
|
Math.abs(index - currentIndex) <= 1 && (
|
||||||
|
<Suspense key={type.id} fallback={null}>
|
||||||
|
<SpinninType
|
||||||
|
typeProps={getType(index)}
|
||||||
|
isSelected={currentIndex === index}
|
||||||
|
position={[index * spacing, 0, 0]}
|
||||||
|
rotation={[Math.PI / 2, Math.PI / 2, 0]}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { OrbitControls } from "three-stdlib";
|
||||||
|
import { CoinProps } from "@/types/coin";
|
||||||
|
import { MutableRefObject, createRef } from "react";
|
||||||
|
|
||||||
|
type Props = { category: "coins" | "banknotes"; types: CoinProps[] };
|
||||||
|
|
||||||
|
interface ShowcaseStore {
|
||||||
|
currentIndex: number;
|
||||||
|
endTransition: () => void;
|
||||||
|
isSpinning: boolean;
|
||||||
|
isTransitioning: boolean;
|
||||||
|
nextIndex: () => void;
|
||||||
|
previousIndex: () => void;
|
||||||
|
orbitControlsRef: MutableRefObject<OrbitControls | null>;
|
||||||
|
setOrbitControlsRef: (ref: OrbitControls | null) => void;
|
||||||
|
toggleSpin: () => void;
|
||||||
|
types: CoinProps[];
|
||||||
|
category: Props["category"] | null;
|
||||||
|
init: (props: Props) => void;
|
||||||
|
getType: (index?: number) => CoinProps | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useShowcaseStore = create<ShowcaseStore>((set, get) => ({
|
||||||
|
isSpinning: true,
|
||||||
|
isTransitioning: false,
|
||||||
|
currentIndex: 0,
|
||||||
|
types: [],
|
||||||
|
category: null,
|
||||||
|
orbitControlsRef: createRef(),
|
||||||
|
nextIndex: () =>
|
||||||
|
set((state) => {
|
||||||
|
const currentIndex = Math.min(
|
||||||
|
state.currentIndex + 1,
|
||||||
|
state.types.length - 1,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
currentIndex,
|
||||||
|
isTransitioning: true,
|
||||||
|
currentType: state.types[currentIndex],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
previousIndex: () =>
|
||||||
|
set((state) => {
|
||||||
|
const currentIndex = Math.max(state.currentIndex - 1, 0);
|
||||||
|
return {
|
||||||
|
currentIndex,
|
||||||
|
isTransitioning: true,
|
||||||
|
currentType: state.types[currentIndex],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
toggleSpin: () =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.isSpinning) {
|
||||||
|
state.orbitControlsRef.current?.setAzimuthalAngle(0);
|
||||||
|
state.orbitControlsRef.current?.setPolarAngle(Math.PI / 2);
|
||||||
|
return { isSpinning: true };
|
||||||
|
}
|
||||||
|
return { isSpinning: false };
|
||||||
|
}),
|
||||||
|
endTransition: () => set(() => ({ isTransitioning: false })),
|
||||||
|
init: (props) =>
|
||||||
|
set((state) => {
|
||||||
|
if (state.types.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}),
|
||||||
|
getType: (index) => {
|
||||||
|
const { types, currentIndex } = get();
|
||||||
|
return types[index ?? currentIndex];
|
||||||
|
},
|
||||||
|
setOrbitControlsRef: (instance) =>
|
||||||
|
set(() => ({
|
||||||
|
orbitControlsRef: { current: instance },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
+21
-3
@@ -1,6 +1,24 @@
|
|||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppContext, AppProps } from "next/app";
|
||||||
|
import App from "next/app";
|
||||||
|
import Div100vh from "react-div-100vh";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return <Component {...pageProps} />;
|
return (
|
||||||
|
<Div100vh>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Div100vh>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MyApp.getInitialProps = async (appContext: AppContext) => {
|
||||||
|
const appProps = await App.getInitialProps(appContext);
|
||||||
|
|
||||||
|
if (appContext.ctx.res?.statusCode === 404) {
|
||||||
|
appContext.ctx.res.writeHead(302, { Location: "/" });
|
||||||
|
appContext.ctx.res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...appProps };
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,16 +28,17 @@ const BanknotesPage: NextPage<Props> = ({ banknotes, ...props }) => (
|
|||||||
|
|
||||||
<Header route={"banknotes"} {...props} />
|
<Header route={"banknotes"} {...props} />
|
||||||
|
|
||||||
<main id={"scene-root"} style={{ position: "relative" }}>
|
<Showcase category="banknotes" types={banknotes} />
|
||||||
<Showcase category="banknotes" types={banknotes} />
|
|
||||||
</main>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
||||||
if (typeof params?.country !== "string") {
|
if (typeof params?.country !== "string") {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
redirect: {
|
||||||
|
destination: "/",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,16 +28,17 @@ const CoinsPage: NextPage<Props> = ({ coins, ...props }) => (
|
|||||||
|
|
||||||
<Header route={"coins"} {...props} />
|
<Header route={"coins"} {...props} />
|
||||||
|
|
||||||
<main id={"scene-root"} style={{ position: "relative" }}>
|
<Showcase category="coins" types={coins} />
|
||||||
<Showcase category="coins" types={coins} />
|
|
||||||
</main>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
||||||
if (typeof params?.country !== "string") {
|
if (typeof params?.country !== "string") {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
redirect: {
|
||||||
|
destination: "/",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const CountriesPage: NextPage<Props> = ({ countsByCountry }) => {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
minHeight: "100vh",
|
|
||||||
gap: 32,
|
gap: 32,
|
||||||
padding: "32px 16px",
|
padding: "32px 16px",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -28,16 +28,17 @@ const PilePage: NextPage<Props> = ({ coins, ...props }) => (
|
|||||||
|
|
||||||
<Header route={"pile"} {...props} />
|
<Header route={"pile"} {...props} />
|
||||||
|
|
||||||
<main id={"scene-root"}>
|
<Pile coins={coins} />
|
||||||
<Pile coins={coins} />
|
|
||||||
</main>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
||||||
if (typeof params?.country !== "string") {
|
if (typeof params?.country !== "string") {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
redirect: {
|
||||||
|
destination: "/",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,3 @@ body {
|
|||||||
background-color: #111;
|
background-color: #111;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scene-root > div:first-child {
|
|
||||||
height: 100vh !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cd /mnt/bigdisk/home/miguel/Documents/projects/react/coins
|
||||||
|
rm -rf packages/site/.next
|
||||||
|
yarn
|
||||||
|
yarn build
|
||||||
|
yarn start
|
||||||
Reference in New Issue
Block a user