casino-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/dist/assets/assets/card-back.svg +43 -0
- package/dist/assets/assets/card-shoe-overlay.svg +14 -0
- package/dist/assets/card-back.svg +43 -0
- package/dist/assets/card-shoe-overlay.svg +14 -0
- package/dist/casino-ui.css +2 -0
- package/dist/components/bet-panel.d.ts +41 -0
- package/dist/components/bet-panel.d.ts.map +1 -0
- package/dist/components/bet-panel.js +37 -0
- package/dist/components/bet-panel.js.map +1 -0
- package/dist/components/button.d.ts +15 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.js +27 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/card-score-badge.d.ts +9 -0
- package/dist/components/card-score-badge.d.ts.map +1 -0
- package/dist/components/card-score-badge.js +20 -0
- package/dist/components/card-score-badge.js.map +1 -0
- package/dist/components/card-shoe.d.ts +6 -0
- package/dist/components/card-shoe.d.ts.map +1 -0
- package/dist/components/card-shoe.js +14 -0
- package/dist/components/card-shoe.js.map +1 -0
- package/dist/components/card-table.d.ts +7 -0
- package/dist/components/card-table.d.ts.map +1 -0
- package/dist/components/card-table.js +6 -0
- package/dist/components/card-table.js.map +1 -0
- package/dist/components/chip-rack.d.ts +6 -0
- package/dist/components/chip-rack.d.ts.map +1 -0
- package/dist/components/chip-rack.js +13 -0
- package/dist/components/chip-rack.js.map +1 -0
- package/dist/components/chip.d.ts +49 -0
- package/dist/components/chip.d.ts.map +1 -0
- package/dist/components/chip.js +81 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/game-footer.d.ts +29 -0
- package/dist/components/game-footer.d.ts.map +1 -0
- package/dist/components/game-footer.js +72 -0
- package/dist/components/game-footer.js.map +1 -0
- package/dist/components/game-info-dialog.d.ts +23 -0
- package/dist/components/game-info-dialog.d.ts.map +1 -0
- package/dist/components/game-info-dialog.js +26 -0
- package/dist/components/game-info-dialog.js.map +1 -0
- package/dist/components/icon.d.ts +10 -0
- package/dist/components/icon.d.ts.map +1 -0
- package/dist/components/icon.js +59 -0
- package/dist/components/icon.js.map +1 -0
- package/dist/components/poker-card.d.ts +15 -0
- package/dist/components/poker-card.d.ts.map +1 -0
- package/dist/components/poker-card.js +77 -0
- package/dist/components/poker-card.js.map +1 -0
- package/dist/components/result-badge.d.ts +14 -0
- package/dist/components/result-badge.d.ts.map +1 -0
- package/dist/components/result-badge.js +71 -0
- package/dist/components/result-badge.js.map +1 -0
- package/dist/components/switch-table-dialog.d.ts +10 -0
- package/dist/components/switch-table-dialog.d.ts.map +1 -0
- package/dist/components/switch-table-dialog.js +51 -0
- package/dist/components/switch-table-dialog.js.map +1 -0
- package/dist/components/toast-container.d.ts +8 -0
- package/dist/components/toast-container.d.ts.map +1 -0
- package/dist/components/toast-container.js +8 -0
- package/dist/components/toast-container.js.map +1 -0
- package/dist/components/toast-helpers.d.ts +22 -0
- package/dist/components/toast-helpers.d.ts.map +1 -0
- package/dist/components/toast-helpers.js +47 -0
- package/dist/components/toast-helpers.js.map +1 -0
- package/dist/components/toast.d.ts +26 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/toast.js +49 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +14 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +35 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +14 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +35 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +39 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/stories/BetPanel.stories.d.ts +10 -0
- package/dist/stories/BetPanel.stories.d.ts.map +1 -0
- package/dist/stories/BetPanel.stories.js +34 -0
- package/dist/stories/BetPanel.stories.js.map +1 -0
- package/dist/stories/Button.stories.d.ts +13 -0
- package/dist/stories/Button.stories.d.ts.map +1 -0
- package/dist/stories/Button.stories.js +39 -0
- package/dist/stories/Button.stories.js.map +1 -0
- package/dist/stories/CardScoreBadge.stories.d.ts +8 -0
- package/dist/stories/CardScoreBadge.stories.d.ts.map +1 -0
- package/dist/stories/CardScoreBadge.stories.js +21 -0
- package/dist/stories/CardScoreBadge.stories.js.map +1 -0
- package/dist/stories/CardShoe.stories.d.ts +7 -0
- package/dist/stories/CardShoe.stories.d.ts.map +1 -0
- package/dist/stories/CardShoe.stories.js +8 -0
- package/dist/stories/CardShoe.stories.js.map +1 -0
- package/dist/stories/CardTable.stories.d.ts +7 -0
- package/dist/stories/CardTable.stories.d.ts.map +1 -0
- package/dist/stories/CardTable.stories.js +15 -0
- package/dist/stories/CardTable.stories.js.map +1 -0
- package/dist/stories/Chip.stories.d.ts +11 -0
- package/dist/stories/Chip.stories.d.ts.map +1 -0
- package/dist/stories/Chip.stories.js +32 -0
- package/dist/stories/Chip.stories.js.map +1 -0
- package/dist/stories/ChipRack.stories.d.ts +7 -0
- package/dist/stories/ChipRack.stories.d.ts.map +1 -0
- package/dist/stories/ChipRack.stories.js +8 -0
- package/dist/stories/ChipRack.stories.js.map +1 -0
- package/dist/stories/Dialog.stories.d.ts +6 -0
- package/dist/stories/Dialog.stories.d.ts.map +1 -0
- package/dist/stories/Dialog.stories.js +15 -0
- package/dist/stories/Dialog.stories.js.map +1 -0
- package/dist/stories/GameFooter.stories.d.ts +7 -0
- package/dist/stories/GameFooter.stories.d.ts.map +1 -0
- package/dist/stories/GameFooter.stories.js +37 -0
- package/dist/stories/GameFooter.stories.js.map +1 -0
- package/dist/stories/Icon.stories.d.ts +9 -0
- package/dist/stories/Icon.stories.d.ts.map +1 -0
- package/dist/stories/Icon.stories.js +27 -0
- package/dist/stories/Icon.stories.js.map +1 -0
- package/dist/stories/PokerCard.stories.d.ts +14 -0
- package/dist/stories/PokerCard.stories.d.ts.map +1 -0
- package/dist/stories/PokerCard.stories.js +40 -0
- package/dist/stories/PokerCard.stories.js.map +1 -0
- package/dist/stories/ResultBadge.stories.d.ts +11 -0
- package/dist/stories/ResultBadge.stories.d.ts.map +1 -0
- package/dist/stories/ResultBadge.stories.js +32 -0
- package/dist/stories/ResultBadge.stories.js.map +1 -0
- package/dist/stories/Toast.stories.d.ts +10 -0
- package/dist/stories/Toast.stories.d.ts.map +1 -0
- package/dist/stories/Toast.stories.js +34 -0
- package/dist/stories/Toast.stories.js.map +1 -0
- package/dist/styles/casino-ui-base.css +310 -0
- package/dist/styles/styles/casino-ui-base.css +310 -0
- package/dist/styles/styles/toast.css +52 -0
- package/dist/styles/toast.css +52 -0
- package/dist/toast.css +52 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/format-amount.d.ts +2 -0
- package/dist/utils/format-amount.d.ts.map +1 -0
- package/dist/utils/format-amount.js +31 -0
- package/dist/utils/format-amount.js.map +1 -0
- package/package.json +58 -0
- package/src/assets/card-back.svg +43 -0
- package/src/assets/card-shoe-overlay.svg +14 -0
- package/src/components/bet-panel.tsx +275 -0
- package/src/components/button.tsx +80 -0
- package/src/components/card-score-badge.tsx +61 -0
- package/src/components/card-shoe.tsx +52 -0
- package/src/components/card-table.tsx +182 -0
- package/src/components/chip-rack.tsx +55 -0
- package/src/components/chip.tsx +203 -0
- package/src/components/game-footer.tsx +356 -0
- package/src/components/game-info-dialog.tsx +245 -0
- package/src/components/icon.tsx +94 -0
- package/src/components/poker-card.tsx +192 -0
- package/src/components/result-badge.tsx +157 -0
- package/src/components/switch-table-dialog.tsx +211 -0
- package/src/components/toast-container.tsx +25 -0
- package/src/components/toast-helpers.ts +79 -0
- package/src/components/toast.tsx +282 -0
- package/src/components/ui/dialog.tsx +134 -0
- package/src/components/ui/drawer.tsx +132 -0
- package/src/components/ui/dropdown-menu.tsx +210 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +88 -0
- package/src/lib/utils.ts +6 -0
- package/src/stories/BetPanel.stories.tsx +113 -0
- package/src/stories/Button.stories.tsx +55 -0
- package/src/stories/CardScoreBadge.stories.tsx +34 -0
- package/src/stories/CardShoe.stories.tsx +12 -0
- package/src/stories/Chip.stories.tsx +51 -0
- package/src/stories/ChipRack.stories.tsx +12 -0
- package/src/stories/Dialog.stories.tsx +45 -0
- package/src/stories/GameFooter.stories.tsx +45 -0
- package/src/stories/Icon.stories.tsx +49 -0
- package/src/stories/PokerCard.stories.tsx +72 -0
- package/src/stories/ResultBadge.stories.tsx +51 -0
- package/src/stories/Toast.stories.tsx +71 -0
- package/src/styles/casino-ui-base.css +310 -0
- package/src/styles/toast.css +52 -0
- package/src/types.ts +11 -0
- package/src/utils/format-amount.ts +35 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import Chip, { CHIP_DENOMINATIONS } from "./chip";
|
|
4
|
+
|
|
5
|
+
const ROW_MARGINS = ["mt-[15px]", "mt-[10px]", "mt-[5px]", "mt-0"];
|
|
6
|
+
const NUM_ROWS = 4;
|
|
7
|
+
|
|
8
|
+
interface ChipRackProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ChipRack = forwardRef<HTMLDivElement, ChipRackProps>(
|
|
13
|
+
function ChipRack({ className }, ref) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
"relative bg-card-shoe-bg border-3 w-76 h-20 border-card-shoe-border rounded-lg shadow-[0px_2.392px_0px_0px_#333] pt-4 pb-2.5 px-1.5",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
{/* Chip grid — all 4 rows overlap in same grid cell, margin increases for depth */}
|
|
23
|
+
<div className="inline-grid">
|
|
24
|
+
{Array.from({ length: NUM_ROWS }).map((_, rowIdx) => (
|
|
25
|
+
<div
|
|
26
|
+
key={rowIdx}
|
|
27
|
+
data-row={rowIdx}
|
|
28
|
+
className={cn("flex gap-2 [grid-area:1/1]", ROW_MARGINS[rowIdx])}
|
|
29
|
+
>
|
|
30
|
+
{CHIP_DENOMINATIONS.map((denom) => (
|
|
31
|
+
<div key={denom} data-denomination={denom}>
|
|
32
|
+
<Chip
|
|
33
|
+
denomination={denom}
|
|
34
|
+
wrapperClass={`w-8.5 h-8.5 p-0`}
|
|
35
|
+
chipClass={`w-full h-full`}
|
|
36
|
+
showText={false}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Top bar overlay — semi-transparent lip of the tray */}
|
|
45
|
+
<div
|
|
46
|
+
className="absolute -top-[3px] left-[-3px] w-76 h-11 bg-card-shoe-bg/60 border-3 border-card-shoe-border rounded-lg z-10 backdrop-blur-[3px]"
|
|
47
|
+
style={{
|
|
48
|
+
backdropFilter: "blur(3px)",
|
|
49
|
+
WebkitBackdropFilter: "blur(3px)",
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import { formatAmountDirect } from "../utils/format-amount";
|
|
3
|
+
|
|
4
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
5
|
+
type DivProps = React.HTMLAttributes<HTMLDivElement>;
|
|
6
|
+
|
|
7
|
+
interface IChipProps {
|
|
8
|
+
denomination?: number;
|
|
9
|
+
amount?: number;
|
|
10
|
+
wrapperClass?: ButtonProps["className"] | DivProps["className"];
|
|
11
|
+
chipClass?: DivProps["className"];
|
|
12
|
+
textClass?: React.HTMLAttributes<HTMLSpanElement>["className"];
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
showText?: boolean;
|
|
15
|
+
isButton?: boolean;
|
|
16
|
+
selected?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hoverEffect = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 48 49" fill="none">
|
|
20
|
+
<g filter="url(#filter0_d_6722_40653)">
|
|
21
|
+
<ellipse cx="24" cy="23" rx="22" ry="23" fill="#C7FE51" fill-opacity="0.4" shape-rendering="crispEdges"/>
|
|
22
|
+
</g>
|
|
23
|
+
<defs>
|
|
24
|
+
<filter id="filter0_d_6722_40653" x="0" y="0" width="48" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
25
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
26
|
+
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
27
|
+
<feOffset dy="2"/>
|
|
28
|
+
<feGaussianBlur stdDeviation="1"/>
|
|
29
|
+
<feComposite in2="hardAlpha" operator="out"/>
|
|
30
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0.780392 0 0 0 0 0.996078 0 0 0 0 0.317647 0 0 0 0.6 0"/>
|
|
31
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6722_40653"/>
|
|
32
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6722_40653" result="shape"/>
|
|
33
|
+
</filter>
|
|
34
|
+
</defs>
|
|
35
|
+
</svg>`;
|
|
36
|
+
|
|
37
|
+
const selectedEffect = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 46 48" fill="none">
|
|
38
|
+
<ellipse cx="23" cy="24" rx="23" ry="24" fill="#C7FE51"/>
|
|
39
|
+
</svg>`;
|
|
40
|
+
|
|
41
|
+
export const CHIP_PRESETS = {
|
|
42
|
+
1: { color: "#999999", shadowColor: "#474747" },
|
|
43
|
+
5: { color: "#DB375E", shadowColor: "#63171D" },
|
|
44
|
+
10: { color: "#037A4C", shadowColor: "#06503F" },
|
|
45
|
+
25: { color: "#F57FE0", shadowColor: "#951F9D" },
|
|
46
|
+
50: { color: "#623CEA", shadowColor: "#2D268B" },
|
|
47
|
+
100: { color: "#E3B800", shadowColor: "#816B0C" },
|
|
48
|
+
500: { color: "#9727B0", shadowColor: "#5B187D" },
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type ChipDenomination = keyof typeof CHIP_PRESETS;
|
|
52
|
+
|
|
53
|
+
export const CHIP_DENOMINATIONS = Object.keys(CHIP_PRESETS).map(
|
|
54
|
+
Number,
|
|
55
|
+
) as ChipDenomination[];
|
|
56
|
+
|
|
57
|
+
export function getChipDenominationForAmount(amount: number): ChipDenomination {
|
|
58
|
+
const absAmount = Math.abs(amount);
|
|
59
|
+
if (absAmount >= 500) return 500;
|
|
60
|
+
if (absAmount >= 100) return 100;
|
|
61
|
+
if (absAmount >= 50) return 50;
|
|
62
|
+
if (absAmount >= 25) return 25;
|
|
63
|
+
if (absAmount >= 10) return 10;
|
|
64
|
+
if (absAmount >= 5) return 5;
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeDenomination(denomination?: number): ChipDenomination {
|
|
69
|
+
if (
|
|
70
|
+
typeof denomination === "number" &&
|
|
71
|
+
CHIP_DENOMINATIONS.includes(denomination as ChipDenomination)
|
|
72
|
+
) {
|
|
73
|
+
return denomination as ChipDenomination;
|
|
74
|
+
}
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ChipBody = ({
|
|
79
|
+
denomination,
|
|
80
|
+
amount,
|
|
81
|
+
chipClass = "w-11 h-11",
|
|
82
|
+
textClass,
|
|
83
|
+
showText = true,
|
|
84
|
+
isButton = false,
|
|
85
|
+
selected = false,
|
|
86
|
+
}: Omit<IChipProps, "wrapperClass" | "onClick">) => {
|
|
87
|
+
const resolvedDenomination =
|
|
88
|
+
typeof amount === "number"
|
|
89
|
+
? getChipDenominationForAmount(amount)
|
|
90
|
+
: normalizeDenomination(denomination);
|
|
91
|
+
const chipText =
|
|
92
|
+
typeof amount === "number"
|
|
93
|
+
? formatAmountDirect(Math.round(amount * 100) / 100)
|
|
94
|
+
: resolvedDenomination;
|
|
95
|
+
const resolvedTextClass =
|
|
96
|
+
typeof amount === "number"
|
|
97
|
+
? "leading-none tracking-tighter text-[10px]"
|
|
98
|
+
: "leading-5.5 text-[10px] md:leading-7.5 md:text-[12.865px]";
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<div
|
|
103
|
+
className={cn(
|
|
104
|
+
"relative rounded-[50%] aspect-square overflow-clip chip-body p-[4.566px] z-2",
|
|
105
|
+
chipClass,
|
|
106
|
+
)}
|
|
107
|
+
style={{
|
|
108
|
+
backgroundColor: CHIP_PRESETS[resolvedDenomination].color,
|
|
109
|
+
boxShadow: `0px 3px 0px 0px ${CHIP_PRESETS[resolvedDenomination].shadowColor}`,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
className="relative z-1 w-full h-full rounded-[50%] flex items-center justify-center select-none"
|
|
114
|
+
style={{
|
|
115
|
+
backgroundColor: CHIP_PRESETS[resolvedDenomination].color,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{showText && (
|
|
119
|
+
<span
|
|
120
|
+
data-slot="chip-text"
|
|
121
|
+
className={cn(
|
|
122
|
+
"font-semibold md:font-bold text-center text-white",
|
|
123
|
+
resolvedTextClass,
|
|
124
|
+
textClass,
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{chipText}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{isButton && (
|
|
134
|
+
<>
|
|
135
|
+
{/* Hover effect */}
|
|
136
|
+
<div
|
|
137
|
+
className={cn(
|
|
138
|
+
"absolute inset-0 opacity-0 ease-in-out duration-300 w-9 h-9.5 md:w-11.5 md:h-12",
|
|
139
|
+
!selected && "group-hover:opacity-100",
|
|
140
|
+
)}
|
|
141
|
+
dangerouslySetInnerHTML={{ __html: hoverEffect }}
|
|
142
|
+
/>
|
|
143
|
+
{/* Active/Selected effect */}
|
|
144
|
+
<div
|
|
145
|
+
className={cn(
|
|
146
|
+
"absolute top-0 left-1/2 -translate-x-1/2 ease-in-out duration-300 w-9 h-9.5 md:w-11.5 md:h-12",
|
|
147
|
+
selected ? "opacity-100" : "group-active:opacity-100 opacity-0",
|
|
148
|
+
)}
|
|
149
|
+
dangerouslySetInnerHTML={{ __html: selectedEffect }}
|
|
150
|
+
/>
|
|
151
|
+
</>
|
|
152
|
+
)}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const Chip = ({
|
|
158
|
+
denomination,
|
|
159
|
+
amount,
|
|
160
|
+
wrapperClass,
|
|
161
|
+
chipClass = "w-11 h-11",
|
|
162
|
+
textClass,
|
|
163
|
+
onClick,
|
|
164
|
+
showText = true,
|
|
165
|
+
isButton = false,
|
|
166
|
+
selected = false,
|
|
167
|
+
}: IChipProps) => {
|
|
168
|
+
if (!isButton) {
|
|
169
|
+
return (
|
|
170
|
+
<div className={cn("relative px-0.5 pb-1 group", wrapperClass)}>
|
|
171
|
+
<ChipBody
|
|
172
|
+
denomination={denomination}
|
|
173
|
+
amount={amount}
|
|
174
|
+
chipClass={chipClass}
|
|
175
|
+
textClass={textClass}
|
|
176
|
+
showText={showText}
|
|
177
|
+
isButton={isButton}
|
|
178
|
+
selected={selected}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={onClick}
|
|
188
|
+
className={cn("relative px-0.5 pb-1 group cursor-pointer", wrapperClass)}
|
|
189
|
+
>
|
|
190
|
+
<ChipBody
|
|
191
|
+
denomination={denomination}
|
|
192
|
+
amount={amount}
|
|
193
|
+
chipClass={chipClass}
|
|
194
|
+
textClass={textClass}
|
|
195
|
+
showText={showText}
|
|
196
|
+
isButton={isButton}
|
|
197
|
+
selected={selected}
|
|
198
|
+
/>
|
|
199
|
+
</button>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default Chip;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenu,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuTrigger,
|
|
6
|
+
} from "./ui/dropdown-menu";
|
|
7
|
+
import { Icon } from "./icon";
|
|
8
|
+
import GameInfoDialog from "./game-info-dialog";
|
|
9
|
+
import SwitchTableDialog from "./switch-table-dialog";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
import { cn } from "../lib/utils";
|
|
12
|
+
import type { CoinInfo } from "../types";
|
|
13
|
+
|
|
14
|
+
interface GameFooterProps {
|
|
15
|
+
tableId: string;
|
|
16
|
+
shareBaseUrl: string | null;
|
|
17
|
+
badges: {
|
|
18
|
+
houseEdge: string;
|
|
19
|
+
minBet: string;
|
|
20
|
+
coinInfo?: CoinInfo;
|
|
21
|
+
};
|
|
22
|
+
tabsContents: {
|
|
23
|
+
table: React.ReactNode;
|
|
24
|
+
howToPlay: React.ReactNode;
|
|
25
|
+
balance: React.ReactNode;
|
|
26
|
+
};
|
|
27
|
+
volumeSlider: {
|
|
28
|
+
globalVolume: number;
|
|
29
|
+
setGlobalVolume: (volume: number) => void;
|
|
30
|
+
setPrevVolume: (volume: number) => void;
|
|
31
|
+
};
|
|
32
|
+
onToggleMute: () => void;
|
|
33
|
+
onJoin: (tableIdOrLink: string) => void;
|
|
34
|
+
isMobile?: boolean;
|
|
35
|
+
gameTitle?: string;
|
|
36
|
+
gameDescription?: string;
|
|
37
|
+
centerLogoUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface VolumeSliderProps {
|
|
41
|
+
globalVolume: number;
|
|
42
|
+
setGlobalVolume: (volume: number) => void;
|
|
43
|
+
setPrevVolume: (volume: number) => void;
|
|
44
|
+
onToggleMute: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function VolumeSlider({
|
|
48
|
+
globalVolume,
|
|
49
|
+
setGlobalVolume,
|
|
50
|
+
setPrevVolume,
|
|
51
|
+
onToggleMute,
|
|
52
|
+
}: VolumeSliderProps) {
|
|
53
|
+
const sliderRef = useRef<HTMLInputElement>(null);
|
|
54
|
+
|
|
55
|
+
const handleChange = useCallback(
|
|
56
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
const val = parseFloat(e.target.value);
|
|
58
|
+
if (val > 0) setPrevVolume(val);
|
|
59
|
+
setGlobalVolume(val);
|
|
60
|
+
},
|
|
61
|
+
[setGlobalVolume, setPrevVolume],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const el = sliderRef.current;
|
|
66
|
+
if (!el) return;
|
|
67
|
+
const pct = globalVolume * 100;
|
|
68
|
+
el.style.background = `linear-gradient(to right, var(--color-accent-success) ${pct}%, var(--color-surface-250) ${pct}%)`;
|
|
69
|
+
}, [globalVolume]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex items-center gap-2 px-2 py-3">
|
|
73
|
+
<Icon
|
|
74
|
+
icon="sound"
|
|
75
|
+
className="size-4 text-text-secondary"
|
|
76
|
+
onClick={() => onToggleMute()}
|
|
77
|
+
/>
|
|
78
|
+
<input
|
|
79
|
+
ref={sliderRef}
|
|
80
|
+
type="range"
|
|
81
|
+
min={0}
|
|
82
|
+
max={1}
|
|
83
|
+
step={0.01}
|
|
84
|
+
value={globalVolume}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
className="volume-slider flex-1"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function SettingsDropdown({
|
|
93
|
+
onJoin,
|
|
94
|
+
volumeSlider,
|
|
95
|
+
onToggleMute,
|
|
96
|
+
isMobile = false,
|
|
97
|
+
tableId,
|
|
98
|
+
className,
|
|
99
|
+
}: {
|
|
100
|
+
onJoin: (tableIdOrLink: string) => void;
|
|
101
|
+
volumeSlider: {
|
|
102
|
+
globalVolume: number;
|
|
103
|
+
setGlobalVolume: (volume: number) => void;
|
|
104
|
+
setPrevVolume: (volume: number) => void;
|
|
105
|
+
};
|
|
106
|
+
onToggleMute: () => void;
|
|
107
|
+
isMobile?: boolean;
|
|
108
|
+
tableId: string;
|
|
109
|
+
className?: string;
|
|
110
|
+
}) {
|
|
111
|
+
const [switchTableOpen, setSwitchTableOpen] = useState(false);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<DropdownMenu>
|
|
116
|
+
<DropdownMenuTrigger
|
|
117
|
+
type="button"
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex cursor-pointer items-center gap-1 h-5 text-base font-semibold text-text-secondary hover:text-white active:text-white tracking-tight ease-in-out duration-300",
|
|
120
|
+
className,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Icon icon="settings" className="size-4" />
|
|
124
|
+
<span className="hidden lg:inline-block">Settings</span>
|
|
125
|
+
</DropdownMenuTrigger>
|
|
126
|
+
|
|
127
|
+
<DropdownMenuContent
|
|
128
|
+
side="top"
|
|
129
|
+
align="start"
|
|
130
|
+
sideOffset={12}
|
|
131
|
+
className="w-[220px] rounded-lg border border-surface-300 bg-surface-450 p-2 shadow-[0px_6px_22px_0px_rgba(0,0,0,0.3)]"
|
|
132
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
133
|
+
>
|
|
134
|
+
<VolumeSlider
|
|
135
|
+
globalVolume={volumeSlider.globalVolume}
|
|
136
|
+
setGlobalVolume={volumeSlider.setGlobalVolume}
|
|
137
|
+
setPrevVolume={volumeSlider.setPrevVolume}
|
|
138
|
+
onToggleMute={onToggleMute}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => setSwitchTableOpen(true)}
|
|
144
|
+
className="flex w-full items-center gap-2 rounded-xl px-2 py-3 transition-colors hover:bg-surface-300"
|
|
145
|
+
>
|
|
146
|
+
<Icon icon="logout" className="size-4 text-text-secondary" />
|
|
147
|
+
<span className="text-sm font-semibold text-white">
|
|
148
|
+
Switch Table
|
|
149
|
+
</span>
|
|
150
|
+
</button>
|
|
151
|
+
</DropdownMenuContent>
|
|
152
|
+
</DropdownMenu>
|
|
153
|
+
|
|
154
|
+
<SwitchTableDialog
|
|
155
|
+
open={switchTableOpen}
|
|
156
|
+
onOpenChange={setSwitchTableOpen}
|
|
157
|
+
currentTableId={tableId}
|
|
158
|
+
onJoin={onJoin}
|
|
159
|
+
isMobile={isMobile}
|
|
160
|
+
/>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function InviteFriendsButton({
|
|
166
|
+
tableId,
|
|
167
|
+
shareBaseUrl,
|
|
168
|
+
className,
|
|
169
|
+
}: {
|
|
170
|
+
tableId: string;
|
|
171
|
+
shareBaseUrl: string | null;
|
|
172
|
+
className?: string;
|
|
173
|
+
}) {
|
|
174
|
+
const [copied, setCopied] = useState(false);
|
|
175
|
+
|
|
176
|
+
const shareUrl = useMemo(() => {
|
|
177
|
+
if (!shareBaseUrl || !tableId) return null;
|
|
178
|
+
return `${shareBaseUrl}?tableId=${tableId}`;
|
|
179
|
+
}, [shareBaseUrl, tableId]);
|
|
180
|
+
|
|
181
|
+
const roomDisplay = useMemo(() => {
|
|
182
|
+
if (!shareUrl) return null;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(shareUrl);
|
|
185
|
+
return `${url.host}${url.pathname}${url.search}`;
|
|
186
|
+
} catch {
|
|
187
|
+
return shareUrl;
|
|
188
|
+
}
|
|
189
|
+
}, [shareUrl]);
|
|
190
|
+
|
|
191
|
+
const handleCopy = useCallback(async () => {
|
|
192
|
+
if (!shareUrl) return;
|
|
193
|
+
try {
|
|
194
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
195
|
+
setCopied(true);
|
|
196
|
+
setTimeout(() => setCopied(false), 2000);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error("Failed to copy:", err);
|
|
199
|
+
}
|
|
200
|
+
}, [shareUrl]);
|
|
201
|
+
|
|
202
|
+
if (!shareBaseUrl) return null;
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className={cn("flex flex-col lg:items-end", className)}>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={handleCopy}
|
|
209
|
+
className="flex cursor-pointer h-5 items-center gap-1 text-base font-semibold tracking-tight group"
|
|
210
|
+
>
|
|
211
|
+
{copied ? (
|
|
212
|
+
<>
|
|
213
|
+
<Icon icon="check" className="size-4 text-accent-success" />
|
|
214
|
+
<span className="text-accent-success">Table Link Copied</span>
|
|
215
|
+
</>
|
|
216
|
+
) : (
|
|
217
|
+
<>
|
|
218
|
+
<Icon icon="share" className="size-4" />
|
|
219
|
+
<span className="text-text-secondary! group-active:text-white">
|
|
220
|
+
Invite Friends
|
|
221
|
+
</span>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</button>
|
|
225
|
+
{roomDisplay && (
|
|
226
|
+
<p className="text-[10px] leading-3.5 text-text-tertiary h-3.5 truncate lg:w-fit w-50">
|
|
227
|
+
Table Id: {tableId}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface GameInfoButtonProps {
|
|
235
|
+
badges: {
|
|
236
|
+
houseEdge: string;
|
|
237
|
+
minBet: string;
|
|
238
|
+
coinInfo?: CoinInfo;
|
|
239
|
+
};
|
|
240
|
+
tabsContents: {
|
|
241
|
+
table: React.ReactNode;
|
|
242
|
+
howToPlay: React.ReactNode;
|
|
243
|
+
balance: React.ReactNode;
|
|
244
|
+
};
|
|
245
|
+
isMobile?: boolean;
|
|
246
|
+
className?: string;
|
|
247
|
+
gameTitle?: string;
|
|
248
|
+
gameDescription?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function GameInfoButton({
|
|
252
|
+
badges,
|
|
253
|
+
tabsContents,
|
|
254
|
+
isMobile,
|
|
255
|
+
className,
|
|
256
|
+
gameTitle = "Blackjack",
|
|
257
|
+
gameDescription = "Classic Blackjack. Play solo or join a multiplayer table in real time.",
|
|
258
|
+
}: GameInfoButtonProps) {
|
|
259
|
+
const [open, setOpen] = useState(false);
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
onClick={() => setOpen(true)}
|
|
266
|
+
className={cn(
|
|
267
|
+
"flex cursor-pointer items-center gap-1 text-base font-semibold text-text-secondary hover:text-white active:text-white tracking-tight ease-in-out duration-300",
|
|
268
|
+
className,
|
|
269
|
+
)}
|
|
270
|
+
>
|
|
271
|
+
<Icon icon="rules" className="size-4" />
|
|
272
|
+
<span className="hidden lg:inline-block">Game Info</span>
|
|
273
|
+
</button>
|
|
274
|
+
<GameInfoDialog
|
|
275
|
+
open={open}
|
|
276
|
+
onOpenChange={setOpen}
|
|
277
|
+
title={gameTitle}
|
|
278
|
+
description={gameDescription}
|
|
279
|
+
badges={badges}
|
|
280
|
+
tabsContents={tabsContents}
|
|
281
|
+
footer={
|
|
282
|
+
<Button
|
|
283
|
+
variant="primary"
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => setOpen(false)}
|
|
286
|
+
className="w-full h-10 rounded-xl"
|
|
287
|
+
>
|
|
288
|
+
Start Playing
|
|
289
|
+
</Button>
|
|
290
|
+
}
|
|
291
|
+
isMobile={isMobile}
|
|
292
|
+
/>
|
|
293
|
+
</>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function GameFooter({
|
|
298
|
+
tableId,
|
|
299
|
+
shareBaseUrl,
|
|
300
|
+
badges,
|
|
301
|
+
tabsContents,
|
|
302
|
+
onJoin,
|
|
303
|
+
onToggleMute,
|
|
304
|
+
isMobile = false,
|
|
305
|
+
volumeSlider,
|
|
306
|
+
gameTitle,
|
|
307
|
+
gameDescription,
|
|
308
|
+
centerLogoUrl,
|
|
309
|
+
}: GameFooterProps) {
|
|
310
|
+
return (
|
|
311
|
+
<div className="relative w-full h-16.5 overflow-clip rounded-b-xl border-t-[5px] border-surface-450 bg-surface-500 p-4 lg:px-7 lg:py-4 flex items-center justify-between">
|
|
312
|
+
{/* Left: Settings + Game Info */}
|
|
313
|
+
<div className="flex items-center gap-7 order-2 lg:order-1">
|
|
314
|
+
<SettingsDropdown
|
|
315
|
+
onJoin={onJoin}
|
|
316
|
+
volumeSlider={volumeSlider}
|
|
317
|
+
onToggleMute={onToggleMute}
|
|
318
|
+
isMobile={isMobile}
|
|
319
|
+
tableId={tableId}
|
|
320
|
+
className="order-2 lg:order-1"
|
|
321
|
+
/>
|
|
322
|
+
|
|
323
|
+
<GameInfoButton
|
|
324
|
+
badges={badges}
|
|
325
|
+
tabsContents={tabsContents}
|
|
326
|
+
isMobile={isMobile}
|
|
327
|
+
className="order-1 lg:order-2"
|
|
328
|
+
gameTitle={gameTitle}
|
|
329
|
+
gameDescription={gameDescription}
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Center: DU logo */}
|
|
334
|
+
{centerLogoUrl && (
|
|
335
|
+
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 hidden lg:block">
|
|
336
|
+
<img
|
|
337
|
+
src={centerLogoUrl}
|
|
338
|
+
alt="Center Logo"
|
|
339
|
+
className="h-16 w-40 opacity-50"
|
|
340
|
+
draggable={false}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
{/* Right: Invite Friends */}
|
|
347
|
+
<InviteFriendsButton
|
|
348
|
+
tableId={tableId}
|
|
349
|
+
shareBaseUrl={shareBaseUrl}
|
|
350
|
+
className="order-1 lg:order-2"
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export { GameFooter };
|