feat: add jackpot animations and enhance currency handling across components

- Introduced new CSS animations for jackpot effects to improve visual engagement.
- Integrated CurrencySwitcher into PlayerPanel and HallScreen for better currency management.
- Updated various components to utilize active player currency for consistent display.
- Enhanced event handling for currency changes to ensure real-time updates across the application.
This commit is contained in:
2026-05-25 14:31:38 +08:00
parent 2bf44e4c29
commit 9bd7cc9b9e
37 changed files with 1030 additions and 180 deletions

View File

@@ -0,0 +1,151 @@
"use client";
import { Banknote, ChevronDown } from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { cn } from "@/lib/utils";
type CurrencySwitcherProps = {
variant?: "default" | "header" | "minimal";
menuAlign?: "start" | "end";
className?: string;
showLabel?: boolean;
};
export function CurrencySwitcher({
variant = "minimal",
menuAlign,
className,
showLabel = true,
}: CurrencySwitcherProps) {
const { t } = useTranslation("common");
const { activeCurrency, bettableCurrencies, canSwitchCurrency, setActiveCurrency } =
useActivePlayerCurrency();
const [isOpen, setIsOpen] = useState(false);
const options = useMemo(
() =>
bettableCurrencies.map((row) => ({
code: row.code,
name: row.name,
label: t("currency.option", { code: row.code, name: row.name }),
})),
[bettableCurrencies, t],
);
if (!canSwitchCurrency) {
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full border border-[#e4eaf4] bg-[#f8fafc] px-2 text-xs font-bold text-[#0b3f96]",
className,
)}
aria-label={t("currency.current", { code: activeCurrency })}
>
<Banknote className="size-3.5 shrink-0" aria-hidden />
{showLabel ? <span>{activeCurrency}</span> : null}
</span>
);
}
const variantStyles = {
default: {
button: "border border-white/20 bg-white/10 text-white hover:bg-white/20",
dropdown: "border border-gray-200 bg-white shadow-lg",
item: "text-gray-800 hover:bg-gray-100",
activeItem: "bg-red-50 text-red-600",
},
header: {
button: "text-white/80 hover:bg-white/10 hover:text-white",
dropdown: "border border-white/20 bg-white/95 shadow-xl backdrop-blur-sm",
item: "text-gray-800 hover:bg-white/10",
activeItem: "bg-red-500/10 text-red-600",
},
minimal: {
button: "text-current hover:bg-black/5",
dropdown: "border border-gray-200 bg-white shadow-lg",
item: "text-gray-800 hover:bg-gray-100",
activeItem: "bg-red-50 text-red-600",
},
} as const;
const styles = variantStyles[variant];
const align = menuAlign ?? (variant === "header" || variant === "default" ? "start" : "end");
function handleSelect(code: string): void {
setActiveCurrency(code);
setIsOpen(false);
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger
className={cn("inline-flex", className)}
render={
<button
type="button"
className={cn(
"flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium transition-colors",
styles.button,
)}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label={t("currency.switchAria", { code: activeCurrency })}
>
<Banknote className="size-4 shrink-0" aria-hidden />
{showLabel ? <span>{activeCurrency}</span> : null}
<ChevronDown
className={cn("size-4 transition-transform duration-200", isOpen && "rotate-180")}
aria-hidden
/>
</button>
}
/>
<PopoverContent
align={align}
side="bottom"
sideOffset={6}
className={cn(
"z-[200] w-auto min-w-[min(100vw-2rem,200px)] max-w-[min(100vw-2rem,280px)] p-1 text-gray-900",
styles.dropdown,
)}
>
<div className="max-h-[min(280px,50dvh)] overflow-y-auto" role="listbox">
{options.map((option) => (
<button
key={option.code}
type="button"
onClick={() => handleSelect(option.code)}
className={cn(
"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors",
activeCurrency === option.code ? styles.activeItem : styles.item,
)}
role="option"
aria-selected={activeCurrency === option.code}
>
<div className="flex min-w-0 flex-col leading-tight">
<span className="font-bold">{option.code}</span>
<span className="truncate text-xs opacity-70">{option.name}</span>
</div>
{activeCurrency === option.code ? (
<svg
className="ml-auto size-4 shrink-0 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
) : null}
</button>
))}
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -6,6 +6,7 @@ import type { ReactNode } from "react";
import { Bell, ChevronLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CurrencySwitcher } from "@/components/currency-switcher";
import { LanguageSwitcher } from "@/components/language-switcher";
import {
playerHeaderControl,
@@ -70,6 +71,15 @@ export function PlayerPanel({
</div>
<div className="flex min-w-0 items-center justify-end gap-1">
<CurrencySwitcher
variant="minimal"
menuAlign="end"
showLabel
className={cn(
playerHeaderControl,
"rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:py-0 [&_button]:text-xs [&_button]:font-bold [&_button]:text-[#0b3f96]",
)}
/>
<LanguageSwitcher
variant="minimal"
menuAlign="end"