Files
zhimingge/components/shared/hexagram-input.tsx
T
dekun fff77dac3f Implement three divination modes, learn pages, and PM2 deploy on port 3130.
Add liuyao/bazi/combined flows with shared calc and AI infrastructure, 64-gua learn routes, and update Ubuntu PM2 deployment docs for port 3130.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:19:49 +08:00

203 lines
5.2 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 { 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 = 600;
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 [count, setCount] = useState(0);
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
Array(6).fill(null),
);
useEffect(() => {
setHexagramList([]);
setCount(0);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
}, [mode]);
useEffect(() => {
if (
mode !== "online" ||
!enabled ||
rotation ||
hexagramList.length >= 6 ||
count >= 6
) {
return;
}
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
return () => clearTimeout(timer);
}, [mode, enabled, rotation, count, hexagramList.length]);
function finishList(list: GuaResult["list"]) {
const result = computeGuaResult(list);
if (result) {
onResult({ list, result });
}
}
function onTransitionEnd() {
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;
});
}
function startOnlineCast() {
if (rotation || !enabled || hexagramList.length >= 6) {
return;
}
setFrontList([bool(), bool(), bool()]);
setRotation(true);
setCount((c) => c + 1);
}
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([]);
setCount(0);
setOfflineCounts(Array(6).fill(null));
setRotation(false);
onClear();
}
const complete = hexagramList.length === 6;
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" && !complete && (
<>
<Coin
onTransitionEnd={onTransitionEnd}
frontList={frontList}
rotation={rotation}
/>
<span className="text-lg font-medium">
🎲 {" "}
<span className="font-mono text-xl font-bold text-orange-500">
{count === 0 ? "-/-" : `${count}/6`}
</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 && (
<Button size="sm" variant="outline" onClick={handleReset}>
</Button>
)}
</div>
)}
</div>
);
}