Skip to content

Commit 9289a37

Browse files
sktbrdclaude
andauthored
feat(stake): use the 3D chest as the rewards button, not a generic icon (#245)
The floating trigger was a lucide Gift glyph in Morpheus green, which had nothing to do with the chest the dialog opens with. Both now show the same asset: the chest is the button, and the dialog keeps the full-size interactive one. It is loaded through next/dynamic with ssr disabled, so a react-three-fiber scene never lands in the initial bundle of every route for a 56px button; the Gift glyph is the loading and no-WebGL fallback. The wrapper sets pointer-events-none — the chest ships its own OrbitControls and click target, and inside a button both would swallow the press — and hides the scene's corner label, which is sized for the dialog. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 68b7de0 commit 9289a37

1 file changed

Lines changed: 36 additions & 2 deletions

File tree

src/components/stake/MorLootbox.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// you keep 50%, Gnars 25%, athlete 25%.
1212
import { useCallback, useEffect, useState } from "react";
1313
import { useTranslations } from "next-intl";
14+
import dynamic from "next/dynamic";
1415
import { motion } from "framer-motion";
1516
import { Gift } from "lucide-react";
1617
import { toast } from "sonner";
@@ -31,6 +32,37 @@ const ZERO = "0x0000000000000000000000000000000000000000";
3132
const LOOT_MIN_MOR = 0.0001; // ignore dust
3233
const MOR_GREEN = "#2be58b";
3334

35+
/**
36+
* The same chest the claim dialog opens with, shrunk into the trigger.
37+
*
38+
* Loaded lazily and never on the server: it is a react-three-fiber scene, so
39+
* pulling it into the initial bundle of every route would be the wrong trade for
40+
* a 56px button. Until it arrives the Gift glyph stands in, which is also what
41+
* renders if WebGL is unavailable.
42+
*/
43+
const ChestArt = dynamic(() => import("@/components/lootbox/AnimatedChest3D"), {
44+
ssr: false,
45+
loading: () => <Gift className="h-6 w-6" style={{ color: MOR_GREEN }} />,
46+
});
47+
48+
/**
49+
* `pointer-events-none` matters: the chest ships its own OrbitControls and click
50+
* target, and inside a button both would swallow the press. The wrapper also
51+
* hides the scene's absolutely-positioned "[ GNARS REWARDS ]" corner label,
52+
* which is sized for the dialog and overflows at this scale.
53+
*/
54+
function ChestIcon({ size }: { size: number }) {
55+
return (
56+
<span
57+
aria-hidden
58+
className="pointer-events-none block [&_.absolute]:hidden"
59+
style={{ width: size, height: size }}
60+
>
61+
<ChestArt onOpen={() => {}} tier="gold" disabled />
62+
</span>
63+
);
64+
}
65+
3466
/**
3567
* `open`/`onOpenChange` are optional: unmanaged, the box keeps its own state and
3668
* behaves exactly as before; passed, the parent owns it. This exists so a second
@@ -263,7 +295,7 @@ export function MorLootbox({
263295
border: `1px solid ${MOR_GREEN}33`,
264296
}}
265297
>
266-
<Gift className="h-6 w-6" style={{ color: `${MOR_GREEN}aa` }} />
298+
<ChestIcon size={44} />
267299
</Link>
268300
</TooltipTrigger>
269301
<TooltipContent side="left">{t("lootbox.enableRewards")}</TooltipContent>
@@ -326,7 +358,9 @@ export function MorLootbox({
326358
animate={{ opacity: [0.35, 0.8, 0.35] }}
327359
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
328360
/>
329-
<Gift className="relative h-7 w-7" style={{ color: MOR_GREEN }} />
361+
<span className="relative">
362+
<ChestIcon size={52} />
363+
</span>
330364
<span
331365
className="absolute -right-1.5 -top-1.5 flex min-w-5 items-center justify-center rounded-full px-1.5 text-[10px] font-black"
332366
style={{ backgroundColor: MOR_GREEN, color: "#04140d" }}

0 commit comments

Comments
 (0)