Compare commits

..

2 Commits

Author SHA1 Message Date
arthur
ae453d9c94 fix size and add sparkle effect 2025-12-12 20:00:46 +07:00
arthur
199317d0c6 a 2025-12-12 19:58:27 +07:00
7 changed files with 165 additions and 22 deletions

BIN
golden-ticket.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
result-remade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
result.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@ -3,6 +3,7 @@ import { useLuckyDraw } from './hooks/useLuckyDraw';
import { SlotMachine } from './components/SlotMachine'; import { SlotMachine } from './components/SlotMachine';
import { HistoryPanel } from './components/HistoryPanel'; import { HistoryPanel } from './components/HistoryPanel';
import { WinnerModal } from './components/WinnerModal'; import { WinnerModal } from './components/WinnerModal';
import { SparkleEffect } from './components/SparkleEffect';
import { Sparkles } from 'lucide-react'; import { Sparkles } from 'lucide-react';
function App() { function App() {
@ -75,11 +76,11 @@ function App() {
<div className="flex-1 flex flex-col items-center justify-center p-4 md:p-6 lg:p-8 py-8"> <div className="flex-1 flex flex-col items-center justify-center p-4 md:p-6 lg:p-8 py-8">
{/* Company Logo */} {/* Company Logo */}
<div className="mb-8 md:mb-12"> <div className="mb-4 md:mb-6">
<img <img
src="/asset/img/logo-white-color.svg" src="/asset/img/logo-white-color.svg"
alt="Company Logo" alt="Company Logo"
className="h-8 md:h-10 lg:h-12 w-auto" className="h-6 md:h-8 lg:h-9 w-auto"
style={{ style={{
filter: 'drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3))' filter: 'drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3))'
}} }}
@ -89,7 +90,7 @@ function App() {
{/* Golden Ticket + Slot Machine - Layered */} {/* Golden Ticket + Slot Machine - Layered */}
<div className="relative flex flex-col items-center"> <div className="relative flex flex-col items-center">
{/* Golden Ticket Title - Background Layer */} {/* Golden Ticket Title - Background Layer */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 z-0 w-[200%] max-w-none"> <div className="absolute top-0 left-1/2 -translate-x-1/2 z-0 w-[140%] max-w-none">
<img <img
src="/asset/img/Font.png" src="/asset/img/Font.png"
alt="Golden Ticket" alt="Golden Ticket"
@ -101,7 +102,7 @@ function App() {
</div> </div>
{/* Slot Machine - Foreground Layer */} {/* Slot Machine - Foreground Layer */}
<div className="relative z-10 mt-64 sm:mt-72 md:mt-80 lg:mt-96 xl:mt-[28rem]"> <div className="relative z-10 mt-32 sm:mt-40 md:mt-48 lg:mt-56 xl:mt-64">
<SlotMachine <SlotMachine
winner={currentWinner} winner={currentWinner}
isSpinning={isSpinning} isSpinning={isSpinning}
@ -109,6 +110,16 @@ function App() {
onDraw={handleDraw} onDraw={handleDraw}
poolLength={pool.length} poolLength={pool.length}
/> />
{/* Left Sparkles - Absolute positioned next to slot machine */}
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-[calc(100%+15px)] w-[250px] h-[400px] pointer-events-none overflow-visible">
<SparkleEffect />
</div>
{/* Right Sparkles - Absolute positioned next to slot machine */}
<div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-[calc(100%+15px)] w-[250px] h-[400px] pointer-events-none overflow-visible">
<SparkleEffect />
</div>
</div> </div>
</div> </div>
@ -122,7 +133,7 @@ function App() {
)} )}
{/* Winners Panel - Below Draw Button */} {/* Winners Panel - Below Draw Button */}
<div className="w-full max-w-6xl px-4 mt-12 md:mt-16"> <div className="w-full max-w-6xl px-4 mt-8 md:mt-10">
<HistoryPanel <HistoryPanel
history={history} history={history}
maxNumber={maxNumber} maxNumber={maxNumber}

View File

@ -10,7 +10,7 @@ export function HistoryPanel({ history, onReset, maxNumber, onMaxNumberChange })
return ( return (
<div <div
className="w-full backdrop-blur-xl rounded-3xl p-6 md:p-8 lg:p-10 shadow-2xl relative overflow-hidden" className="w-full backdrop-blur-xl rounded-2xl p-4 md:p-6 lg:p-7 shadow-2xl relative overflow-hidden"
style={{ style={{
background: 'linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(20, 20, 20, 0.5) 50%, rgba(0, 0, 0, 0.7) 100%)', background: 'linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(20, 20, 20, 0.5) 50%, rgba(0, 0, 0, 0.7) 100%)',
border: '3px solid transparent', border: '3px solid transparent',
@ -27,9 +27,9 @@ export function HistoryPanel({ history, onReset, maxNumber, onMaxNumberChange })
maskComposite: 'exclude', maskComposite: 'exclude',
}}></div> }}></div>
{/* Header with Title and Controls */} {/* Header with Title and Controls */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8 relative z-10"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-6 relative z-10">
<h3 <h3
className="text-3xl md:text-4xl font-black tracking-wide flex items-center gap-3" className="text-2xl md:text-3xl font-black tracking-wide flex items-center gap-2"
style={{ style={{
background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)', background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
@ -80,18 +80,18 @@ export function HistoryPanel({ history, onReset, maxNumber, onMaxNumberChange })
{/* Winners Table - Vertical Rows */} {/* Winners Table - Vertical Rows */}
{history.length === 0 ? ( {history.length === 0 ? (
<div className="text-yellow-200/60 text-center italic py-16 text-xl relative z-10"> <div className="text-yellow-200/60 text-center italic py-12 text-lg relative z-10">
No winners yet - click the lottery machine to start! No winners yet - click the lottery machine to start!
</div> </div>
) : ( ) : (
<div className="space-y-4 relative z-10"> <div className="space-y-3 relative z-10">
{reversedHistory.map((num, idx) => { {reversedHistory.map((num, idx) => {
const rank = idx + 1; const rank = idx + 1;
const isTop3 = rank <= 3; const isTop3 = rank <= 3;
return ( return (
<div <div
key={idx} key={idx}
className="flex items-center gap-6 p-5 md:p-6 rounded-2xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl relative overflow-hidden group" className="flex items-center gap-4 p-4 md:p-5 rounded-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl relative overflow-hidden group"
style={{ style={{
background: isTop3 background: isTop3
? 'linear-gradient(90deg, rgba(191, 149, 63, 0.15) 0%, rgba(179, 135, 40, 0.1) 50%, rgba(170, 119, 28, 0.05) 100%)' ? 'linear-gradient(90deg, rgba(191, 149, 63, 0.15) 0%, rgba(179, 135, 40, 0.1) 50%, rgba(170, 119, 28, 0.05) 100%)'
@ -114,7 +114,7 @@ export function HistoryPanel({ history, onReset, maxNumber, onMaxNumberChange })
)} )}
{/* Rank badge - left */} {/* Rank badge - left */}
<div className="text-2xl md:text-3xl font-black w-20 text-center flex-shrink-0" style={{ <div className="text-xl md:text-2xl font-black w-16 text-center flex-shrink-0" style={{
background: isTop3 background: isTop3
? 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)' ? 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)'
: 'linear-gradient(to bottom, #9CA3AF 0%, #D1D5DB 50%, #9CA3AF 100%)', : 'linear-gradient(to bottom, #9CA3AF 0%, #D1D5DB 50%, #9CA3AF 100%)',
@ -129,11 +129,11 @@ export function HistoryPanel({ history, onReset, maxNumber, onMaxNumberChange })
</div> </div>
{/* Decorative separator */} {/* Decorative separator */}
<div className="w-px h-16 bg-gradient-to-b from-transparent via-gold-400/30 to-transparent flex-shrink-0"></div> <div className="w-px h-12 bg-gradient-to-b from-transparent via-gold-400/30 to-transparent flex-shrink-0"></div>
{/* Number - center, LARGE with metallic effect */} {/* Number - center, LARGE with metallic effect */}
<div <div
className="text-5xl md:text-6xl lg:text-7xl font-black flex-1 tabular-nums" className="text-4xl md:text-5xl lg:text-6xl font-black flex-1 tabular-nums"
style={{ style={{
background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)', background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',

View File

@ -15,14 +15,14 @@ const shuffleArray = (array) => {
// Responsive slot heights based on screen size // Responsive slot heights based on screen size
const getSlotHeight = () => { const getSlotHeight = () => {
if (typeof window === 'undefined') return 320; if (typeof window === 'undefined') return 240;
const width = window.innerWidth; const width = window.innerWidth;
if (width < 640) return 96; // mobile (h-24) if (width < 640) return 80; // mobile (h-20)
if (width < 768) return 128; // sm (h-32) if (width < 768) return 112; // sm (h-28)
if (width < 1024) return 176; // md (h-44) if (width < 1024) return 144; // md (h-36)
if (width < 1280) return 224; // lg (h-56) if (width < 1280) return 176; // lg (h-44)
if (width < 1536) return 288; // xl (h-72) if (width < 1536) return 208; // xl (h-52)
return 320; // 2xl (h-80) return 240; // 2xl (h-60)
}; };
export function SlotMachine({ winner, isSpinning, onAnimationComplete, onDraw, poolLength }) { export function SlotMachine({ winner, isSpinning, onAnimationComplete, onDraw, poolLength }) {
@ -178,7 +178,7 @@ export function SlotMachine({ winner, isSpinning, onAnimationComplete, onDraw, p
const SlotBox = ({ children }) => ( const SlotBox = ({ children }) => (
<div <div
className="relative w-32 h-40 sm:w-40 sm:h-48 md:w-48 md:h-60 lg:w-60 lg:h-[18rem] xl:w-72 xl:h-[22rem] 2xl:w-80 2xl:h-96 flex items-start justify-center overflow-hidden" className="relative w-24 h-32 sm:w-28 sm:h-36 md:w-36 md:h-44 lg:w-44 lg:h-56 xl:w-52 xl:h-64 2xl:w-60 2xl:h-72 flex items-start justify-center overflow-hidden"
style={{ style={{
borderRadius: '1.5rem', borderRadius: '1.5rem',
background: '#FFFFFF', background: '#FFFFFF',

View File

@ -0,0 +1,132 @@
import { useEffect, useState } from 'react';
/**
* Helper to generate random number within a range
*/
const random = (min, max) => Math.random() * (max - min) + min;
/**
* Component: Star
* Renders a 4-pointed glowing star (lens flare style)
*/
const Star = ({ style }) => {
return (
<div
className="absolute flex items-center justify-center mix-blend-screen pointer-events-none"
style={{
...style,
animationName: 'twinkle',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
}}
>
{/* The Glow Center - Bright Core */}
<div className="absolute w-[10%] h-[10%] bg-white rounded-full shadow-[0_0_20px_5px_rgba(255,255,255,1),0_0_50px_20px_rgba(253,224,71,0.8)] z-20" />
{/* Vertical Ray - Sharp White Core */}
<div className="absolute w-[2px] h-full bg-gradient-to-b from-transparent via-white to-transparent opacity-100 z-10" />
{/* Vertical Ray - Gold Glow */}
<div className="absolute w-[6px] h-full bg-gradient-to-b from-transparent via-yellow-200 to-transparent opacity-60 blur-[2px]" />
{/* Horizontal Ray - Sharp White Core */}
<div className="absolute h-[2px] w-full bg-gradient-to-r from-transparent via-white to-transparent opacity-100 z-10" />
{/* Horizontal Ray - Gold Glow */}
<div className="absolute h-[6px] w-full bg-gradient-to-r from-transparent via-yellow-200 to-transparent opacity-60 blur-[2px]" />
{/* Diffuse Halo */}
<div className="absolute w-[70%] h-[70%] bg-amber-500/40 rounded-full blur-2xl" />
</div>
);
};
/**
* Component: Particle
* Renders tiny background dust/bokeh circles
*/
const Particle = ({ style }) => (
<div
className="absolute rounded-full bg-yellow-50 mix-blend-screen"
style={{
...style,
animationName: 'pulse',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
}}
/>
);
export function SparkleEffect() {
const [stars, setStars] = useState([]);
const [particles, setParticles] = useState([]);
useEffect(() => {
// Generate Stars (The big cross flares) - spread across entire area
const starCount = 30; // Reduced for smaller screens
const newStars = Array.from({ length: starCount }).map((_, i) => {
return {
id: i,
style: {
left: `${random(0, 100)}%`,
top: `${random(0, 100)}%`,
width: `${random(40, 120)}px`,
height: `${random(40, 120)}px`,
animationDuration: `${random(1.5, 4)}s`,
animationDelay: `${random(0, 4)}s`,
opacity: random(0.6, 1),
},
};
});
// Generate Particles (The small bokeh dots) - spread across entire area
const particleCount = 70; // Reduced for smaller screens
const newParticles = Array.from({ length: particleCount }).map((_, i) => {
return {
id: i,
style: {
left: `${random(0, 100)}%`,
top: `${random(0, 100)}%`,
width: `${random(2, 4)}px`,
height: `${random(2, 4)}px`,
opacity: random(0.3, 0.8),
boxShadow: `0 0 ${random(3, 6)}px rgba(255, 255, 220, 0.8)`,
animationDuration: `${random(1, 3)}s`,
animationDelay: `${random(0, 5)}s`,
},
};
});
setStars(newStars);
setParticles(newParticles);
}, []);
return (
<div className="absolute inset-0 pointer-events-none">
{/* Custom Keyframes */}
<style>{`
@keyframes twinkle {
0% { transform: scale(0.3) rotate(0deg); opacity: 0; }
50% { transform: scale(1.2) rotate(15deg); opacity: 1; filter: brightness(2.5); }
100% { transform: scale(0.3) rotate(0deg); opacity: 0; }
}
@keyframes pulse {
0%, 100% { opacity: 0.2; transform: scale(0.5); }
50% { opacity: 1; transform: scale(1.3); filter: brightness(1.5); }
}
`}</style>
{/* Render Stars (Big cross flares) */}
<div className="absolute inset-0 z-10">
{stars.map((s) => (
<Star key={s.id} style={s.style} />
))}
</div>
{/* Render Particles (Small dots) */}
<div className="absolute inset-0 z-0">
{particles.map((p) => (
<Particle key={p.id} style={p.style} />
))}
</div>
</div>
);
}