Files
zhimingge/components/shared/hexagram-input.tsx
T
dekun a487b17165 Fix learn 404, coin animation, full regions, and AI key errors.
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>
2026-06-10 21:32:20 +08:00

203 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
import { bool } from "aimless.js";
import Coin from "@/components/coin";
import Hexagram from "@/components/hexagram";
import { Button } from "@/components/ui/button";
import {
buildHexagramList,
COIN_OPTIONS,
computeGuaResult,
type GuaResult,
YAO_LABELS,
} from "@/lib/calc/hexagram";
const AUTO_DELAY = 800;
interface HexagramInputProps {
mode: "online" | "offline";
enabled: boolean;
onResult: (data: GuaResult) => void;
onClear: () => void;
}
export default function HexagramInput({
mode,
enabled,
onResult,
onClear,
}: HexagramInputProps) {
const [frontList, setFrontList] = useState([true, true, true]);
const [rotation, setRotation] = useState(false);
const [hexagramList, setHexagramList] = useState<GuaResult["list"]>([]);
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
Array(6).fill(null),
);
const complete = hexagramList.length === 6;
useEffect(() => {
setHexagramList([]);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
}, [mode]);
const finishList = useCallback(
(list: GuaResult["list"]) => {
const result = computeGuaResult(list);
if (result) {
onResult({ list, result });
}
},
[onResult],
);
const onTransitionEnd = useCallback(() => {
setRotation(false);
const frontCount = frontList.reduce(
(acc, val) => (val ? acc + 1 : acc),
0,
);
setHexagramList((list) => {
const newList = [
...list,
{
change: frontCount === 0 || frontCount === 3,
yang: frontCount >= 2,
separate: list.length === 3,
},
];
if (newList.length === 6) {
finishList(newList);
}
return newList;
});
}, [frontList, finishList]);
const startOnlineCast = useCallback(() => {
if (rotation || !enabled || hexagramList.length >= 6) {
return;
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
}, [rotation, enabled, hexagramList.length]);
useEffect(() => {
if (mode !== "online" || !enabled || rotation || complete) {
return;
}
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
return () => clearTimeout(timer);
}, [mode, enabled, rotation, complete, hexagramList.length, startOnlineCast]);
function handleOfflineSelect(index: number, frontCount: number) {
const next = [...offlineCounts];
next[index] = frontCount;
setOfflineCounts(next);
}
function confirmOffline() {
if (offlineCounts.some((v) => v === null)) {
return;
}
const list = buildHexagramList(offlineCounts as number[]);
finishList(list);
setHexagramList(list);
}
function handleReset() {
setHexagramList([]);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
onClear();
}
const offlineReady = offlineCounts.every((v) => v !== null);
if (!enabled) {
return (
<p className="text-center text-sm text-muted-foreground">
</p>
);
}
return (
<div className="flex w-full flex-col items-center gap-4">
{mode === "online" && (
<>
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
<span className="text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{complete ? "6/6" : `${hexagramList.length + (rotation ? 1 : 0)}/6`}
</span>{" "}
{rotation && (
<span className="ml-2 text-sm text-muted-foreground"></span>
)}
</span>
</>
)}
{mode === "offline" && !complete && (
<div className="w-full max-w-md space-y-2">
<p className="text-center text-xs text-muted-foreground">
</p>
{YAO_LABELS.map((label, index) => (
<div
key={label}
className="flex flex-wrap items-center gap-2 rounded-md border p-2"
>
<span className="w-10 shrink-0 text-sm font-medium">{label}</span>
{COIN_OPTIONS.map((opt) => (
<button
key={opt.count}
type="button"
onClick={() => handleOfflineSelect(index, opt.count)}
className={`rounded border px-2 py-1 text-xs transition ${
offlineCounts[index] === opt.count
? "border-primary bg-primary/10 text-primary"
: "hover:bg-accent"
}`}
>
{opt.label}
</button>
))}
</div>
))}
<Button
className="w-full"
disabled={!offlineReady}
onClick={confirmOffline}
>
</Button>
</div>
)}
{hexagramList.length > 0 && (
<div className="flex flex-col items-center gap-3 sm:flex-row">
<Hexagram list={hexagramList} />
{complete && (
<>
<p className="text-sm text-muted-foreground sm:max-w-[10rem]">
AI
</p>
<Button size="sm" variant="outline" onClick={handleReset}>
</Button>
</>
)}
</div>
)}
</div>
);
}