a487b17165
Use numeric /learn/{num} routes, register Tailwind coin animations with方孔铜钱 UI, expand to 34 provinces, and surface missing OPENAI_API_KEY clearly.
Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import clsx from "clsx";
|
|
|
|
const rotationDuration = 3800;
|
|
|
|
const COIN_ANIM: Record<string, string> = {
|
|
"front-front": "animate-coin-front-front",
|
|
"front-back": "animate-coin-front-back",
|
|
"back-front": "animate-coin-back-front",
|
|
"back-back": "animate-coin-back-back",
|
|
};
|
|
|
|
function Coin(props: {
|
|
frontList: boolean[];
|
|
rotation: boolean;
|
|
onTransitionEnd: () => void;
|
|
}) {
|
|
const [lastFront, setLastFront] = useState(props.frontList);
|
|
|
|
useEffect(() => {
|
|
if (!props.rotation) {
|
|
return;
|
|
}
|
|
const id = setTimeout(() => {
|
|
setLastFront(props.frontList);
|
|
props.onTransitionEnd();
|
|
}, rotationDuration);
|
|
return () => clearTimeout(id);
|
|
}, [props.rotation, props.frontList, props.onTransitionEnd]);
|
|
|
|
return (
|
|
<div className="flex w-full max-w-md justify-around rounded-md border bg-secondary p-4 shadow dark:border-0 dark:shadow-none sm:p-6">
|
|
{props.frontList.map((value, index) => (
|
|
<CoinItem
|
|
key={index}
|
|
front={value}
|
|
lastFront={lastFront[index]}
|
|
rotation={props.rotation}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 方孔铜钱造型 */
|
|
function CoinFace({ side }: { side: "front" | "back" }) {
|
|
const isFront = side === "front";
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"absolute inset-0 rounded-full border-[3px] shadow-md",
|
|
isFront
|
|
? "border-amber-600 bg-gradient-to-br from-amber-300 via-amber-400 to-amber-600"
|
|
: "border-stone-600 bg-gradient-to-br from-stone-400 via-stone-500 to-stone-700",
|
|
)}
|
|
style={{
|
|
backfaceVisibility: "hidden",
|
|
transform: isFront ? undefined : "rotateY(180deg)",
|
|
}}
|
|
>
|
|
<div className="absolute inset-[18%] rounded-sm border-2 border-amber-800/40 bg-amber-900/10" />
|
|
<span
|
|
className={clsx(
|
|
"absolute bottom-1 left-0 right-0 text-center text-[10px] font-bold sm:text-xs",
|
|
isFront ? "text-amber-950/80" : "text-stone-200/90",
|
|
)}
|
|
>
|
|
{isFront ? "乾" : "坤"}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CoinItem(props: {
|
|
front: boolean;
|
|
lastFront: boolean;
|
|
rotation: boolean;
|
|
}) {
|
|
const animKey = `${getFront(props.lastFront)}-${getFront(props.front)}`;
|
|
const animClass = props.rotation ? COIN_ANIM[animKey] : "";
|
|
|
|
return (
|
|
<div className="h-[4.5rem] w-[4.5rem] sm:h-20 sm:w-20" style={{ perspective: "900px" }}>
|
|
<div
|
|
style={{
|
|
transform: `rotateY(${props.front ? 0 : 180}deg)`,
|
|
transformStyle: "preserve-3d",
|
|
}}
|
|
className={clsx("relative h-full w-full", animClass)}
|
|
>
|
|
<CoinFace side="front" />
|
|
<CoinFace side="back" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getFront(front: boolean): string {
|
|
return front ? "front" : "back";
|
|
}
|
|
|
|
export default Coin;
|