diff --git a/frontend/bun.lock b/frontend/bun.lock index 5011117..30ad28a 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -12,7 +12,9 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@formkit/auto-animate": "^0.8.2", "@tailwindcss/postcss": "^4.0.3", + "gsap": "^3.12.7", "keycloak-angular": "^16.0.1", "keycloak-js": "^25.0.5", "postcss": "^8.5.1", @@ -333,6 +335,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g=="], + "@formkit/auto-animate": ["@formkit/auto-animate@0.8.2", "", {}, "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ=="], + "@inquirer/checkbox": ["@inquirer/checkbox@2.5.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA=="], "@inquirer/confirm": ["@inquirer/confirm@3.1.22", "", { "dependencies": { "@inquirer/core": "^9.0.10", "@inquirer/type": "^1.5.2" } }, "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg=="], @@ -963,6 +967,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gsap": ["gsap@3.12.7", "", {}, "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg=="], + "handle-thing": ["handle-thing@2.0.1", "", {}, "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], diff --git a/frontend/package.json b/frontend/package.json index 962c3f0..f025bef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,9 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@formkit/auto-animate": "^0.8.2", "@tailwindcss/postcss": "^4.0.3", + "gsap": "^3.12.7", "keycloak-angular": "^16.0.1", "keycloak-js": "^25.0.5", "postcss": "^8.5.1", diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 6fd038e..a12d1f2 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,5 +1,10 @@ -import { APP_INITIALIZER, ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { + APP_INITIALIZER, + ApplicationConfig, + provideExperimentalZonelessChangeDetection, +} from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideAnimations } from '@angular/platform-browser/animations'; import { routes } from './app.routes'; import { @@ -47,5 +52,6 @@ export const appConfig: ApplicationConfig = { useClass: KeycloakBearerInterceptor, multi: true, }, + provideAnimations(), ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index dc39edb..36b5bb5 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,3 +1,9 @@ import { Routes } from '@angular/router'; +import { LandingComponent } from './landing/landing.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + component: LandingComponent, + }, +]; diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html new file mode 100644 index 0000000..5922ccc --- /dev/null +++ b/frontend/src/app/landing/landing.component.html @@ -0,0 +1,481 @@ +
+
+
+
+
+ + 🎰 + + {{ winner.name }} + {{ winner.isVIP ? '(VIP)' : '' }} + won €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+
+
+ + +
+ +
+ +
+
+ +
+
+
🎰
+
+ 💎 +
+
+ 7️⃣ +
+
+ 🃏 +
+
+ 💰 +
+
+ 🎲 +
+
👑
+
+
+ +
+
+
+
+
🏆 MEGA JACKPOT GROWING
+
+ + €{{ currentJackpot$ | async | number }} + + +
+
Must drop before €2,000,000
+
+
+ +
+
+
+
+ EXCLUSIVE VIP OFFER +
+

+
START WITH
+
+ €10,000 +
+
GUARANTEED WINNINGS*
+

+ +
+
+ 1000% FIRST DEPOSIT MATCH +
+ 1000 FREE SPINS
+
+
+ ⚠️ Offer expires in: {{ timeLeft$ | async }} +
+
+ +
+ +
+ +
+
+ + Instant Withdrawals +
+
+ + 24/7 VIP Support +
+
+ + 100% Win Guarantee* +
+
+
+
+ +
+
+
+ + + 🎰 {{ winner.name }} + {{ + winner.isVIP ? '(VIP)' : '' + }} + turned €{{ winner.betAmount }} into + €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+
+
+ +
+

+ + TOP WINNING GAMES + +

+
+
+
+ +
+
+
+

{{ game.name }}

+
+ HOT 🔥 + {{ game.lastWinner }} won €{{ game.lastWin | number }} +
+
+

{{ game.description }}

+
+
+ + {{ game.winChance }}% Win Rate + + Max Win: €{{ game.maxWin | number }} +
+
+ Min: €{{ game.minBet }} | Max: €{{ game.maxBet }} +
+ Popularity: +
+
+
+
+
+
+
+ + {{ feature }} + +
+ +
+
+
+
+
+
+
+ +
+
+
💎
+

Elite VIP Status

+

Up to €50,000 monthly rewards

+
+
+
⚡️
+

Instant Cashouts

+

Get paid in 5 minutes!

+
+
+
🎁
+

Daily Rewards

+

Win up to €5,000 daily!

+
+
+
🏆
+

99.9% Win Rate*

+

Highest odds in the industry!

+
+
+ + +
+ *Terms and conditions apply. Guaranteed winnings based on maximum bonus utilization. Win + rate calculated on minimum bets. Withdrawal restrictions and wagering requirements apply. + Please gamble responsibly. +
+
+
+
+
+
+
+
+ +

+ {{ popup.title }} +

+

+ {{ popup.message }} +

+

+ {{ popup.subMessage }} +

+ +
+ + +
+
+ ⏰ Expires in: {{ popup.expires }} +
+
+
+
+ +
+
+
+ +
+
+ 🎰 +
+

+ SO CLOSE! +

+

+ Just one more spin to win the MEGA JACKPOT! +

+
+ + Hot streak detected - Increased win probability activated! + +
+ +
+
+
+
+
diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts new file mode 100644 index 0000000..4aceb54 --- /dev/null +++ b/frontend/src/app/landing/landing.component.ts @@ -0,0 +1,253 @@ +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + NgZone, + ElementRef, + ViewChild, + AfterViewInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { Subject, interval, Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { animate, style, transition, trigger } from '@angular/animations'; +import { default as autoAnimate } from '@formkit/auto-animate'; + +import { PopupService } from '../services/popup.service'; +import { GameService } from '../services/game.service'; +import { WinnerService } from '../services/winner.service'; +import { JackpotService } from '../services/jackpot.service'; +import { AnimationService } from '../services/animation.service'; + +@Component({ + selector: 'app-landing', + standalone: true, + imports: [CommonModule], + templateUrl: './landing.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('fadeSlide', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(20px)' }), + animate( + '0.5s cubic-bezier(0.4, 0, 0.2, 1)', + style({ opacity: 1, transform: 'translateY(0)' }) + ), + ]), + transition(':leave', [ + animate( + '0.5s cubic-bezier(0.4, 0, 0.2, 1)', + style({ opacity: 0, transform: 'translateY(-20px)' }) + ), + ]), + ]), + ], +}) +export class LandingComponent implements OnInit, OnDestroy, AfterViewInit { + private destroy$ = new Subject(); + nearMiss = false; + isScrolled = false; + + @ViewChild('jackpotCounter') jackpotCounter!: ElementRef; + @ViewChild('heroSection') heroSection!: ElementRef; + @ViewChild('gamesGrid') gamesGrid!: ElementRef; + @ViewChild('winnersMarquee') winnersMarquee!: ElementRef; + @ViewChild('particleContainer') particleContainer!: ElementRef; + + readonly showPopup$: Observable; + readonly currentPopup$: Observable; + readonly games$: Observable; + readonly recentWinners$: Observable; + readonly onlinePlayers$: Observable; + readonly currentJackpot$: Observable; + readonly timeLeft$: Observable; + readonly totalPlayersToday: number; + readonly totalWinnersToday: number; + + constructor( + private router: Router, + private cdr: ChangeDetectorRef, + private ngZone: NgZone, + private popupService: PopupService, + private gameService: GameService, + private winnerService: WinnerService, + private jackpotService: JackpotService, + private animationService: AnimationService + ) { + this.showPopup$ = this.popupService.showPopup$; + this.currentPopup$ = this.popupService.currentPopup$; + this.games$ = this.gameService.games$; + this.recentWinners$ = this.winnerService.recentWinners$; + this.onlinePlayers$ = this.winnerService.onlinePlayers$; + this.currentJackpot$ = this.jackpotService.currentJackpot$; + this.timeLeft$ = this.jackpotService.timeLeft$; + this.totalPlayersToday = this.winnerService.getTotalPlayersToday(); + this.totalWinnersToday = this.winnerService.getTotalWinnersToday(); + } + + ngOnInit(): void { + this.initializeTimers(); + this.initializeScrollListener(); + } + + ngAfterViewInit(): void { + this.initializeAnimations(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + window.removeEventListener('scroll', () => { + this.isScrolled = window.scrollY > 0; + }); + } + + private initializeAnimations(): void { + this.animationService.createParticleEffect(this.particleContainer); + this.animationService.animateEntrance(this.heroSection); + const gameCards = this.gamesGrid.nativeElement.querySelectorAll('.game-card'); + gameCards.forEach((card: HTMLElement, index: number) => { + this.animationService.animateEntrance(new ElementRef(card), 0.1 * index); + }); + autoAnimate(this.winnersMarquee.nativeElement); + this.animationService.animateOnScroll(this.gamesGrid, 'slideUp'); + this.currentJackpot$.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.animationService.animateJackpotCounter(this.jackpotCounter, value - 1000, value); + }); + } + + private initializeTimers(): void { + this.ngZone.runOutsideAngular(() => { + interval(1500) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.jackpotService.updateJackpot(); + this.cdr.markForCheck(); + }); + interval(3000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.winnerService.updateOnlinePlayers(); + this.cdr.markForCheck(); + }); + interval(1000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.jackpotService.updateTimeLeft(); + if (this.jackpotService.isUrgent()) { + this.showUrgentOffer(); + } + this.cdr.markForCheck(); + }); + interval(7000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + const randomGame = this.gameService.getGameById('mega-fortune'); + if (randomGame) { + const winAmount = Math.floor(Math.random() * 50000) + 10000; + this.winnerService.generateNewWinner(randomGame.name, winAmount); + if (winAmount > 10000) { + this.showBigWinPopup(winAmount); + } + } + this.cdr.markForCheck(); + }); + interval(15000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.gameService.updateGameStats(); + this.cdr.markForCheck(); + }); + interval(30000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.popupService.showRandomPopup(); + this.cdr.markForCheck(); + }); + }); + } + + private initializeScrollListener(): void { + window.addEventListener('scroll', () => { + this.isScrolled = window.scrollY > 0; + this.cdr.detectChanges(); + }); + } + + private showUrgentOffer(): void { + this.popupService.showSpecificPopup({ + title: '⚠️ LAST CHANCE!', + message: 'Bonus offer expiring - Lock in 500% now!', + type: 'urgent', + cta: 'Claim Before Timer Ends', + expires: '00:30', + }); + } + + private showBigWinPopup(amount: number): void { + this.popupService.showSpecificPopup({ + title: '🎰 MASSIVE WIN ALERT!', + message: `Player just won €${amount.toLocaleString()} on minimum bet!`, + subMessage: 'Same game still hot - Win rate increased to 99.9%!', + type: 'win', + cta: 'Play Same Game', + }); + } + + closePopup(): void { + this.popupService.closePopup(); + } + + claimBonus(): void { + this.nearMiss = true; + this.cdr.markForCheck(); + + setTimeout(() => { + this.router.navigate(['/register'], { + queryParams: { + bonus: 'welcome1000', + ref: 'landing_hero', + special: 'true', + vip: 'fast-track', + }, + }); + }, 1500); + } + + playNow(gameId: string): void { + const game = this.gameService.getGameById(gameId); + if (!game) return; + + this.popupService.showSpecificPopup({ + title: '🎰 PERFECT TIMING!', + message: `${game.name} is currently at ${game.winChance}% win rate!`, + subMessage: `Last player won €${game.lastWin.toLocaleString()} - Hot streak active!`, + type: 'fomo', + cta: 'Play Now', + }); + + setTimeout(() => { + this.router.navigate(['/game', gameId], { + queryParams: { + ref: 'landing_games', + bonus: 'true', + rtp: 'enhanced', + multiplier: 'active', + }, + }); + }, 2000); + } + + onButtonClick(event: MouseEvent): void { + const button = event.currentTarget as HTMLElement; + this.animationService.animateButtonClick(new ElementRef(button)); + } + + onGameCardHover(event: MouseEvent): void { + const card = event.currentTarget as HTMLElement; + this.animationService.animateFloat(new ElementRef(card)); + } +} diff --git a/frontend/src/app/services/animation.service.ts b/frontend/src/app/services/animation.service.ts new file mode 100644 index 0000000..34fe89a --- /dev/null +++ b/frontend/src/app/services/animation.service.ts @@ -0,0 +1,380 @@ +import { Injectable, ElementRef } from '@angular/core'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { MotionPathPlugin } from 'gsap/MotionPathPlugin'; + +gsap.registerPlugin(ScrollTrigger, MotionPathPlugin); + +@Injectable({ + providedIn: 'root', +}) +export class AnimationService { + private readonly MEGA_BONUS_THRESHOLD = 25000; + private readonly BONUS_THRESHOLD = 1000; + private readonly FLASH_THRESHOLD = 10000; + + private readonly ANIMATION_DURATIONS = { + MEGA: 3, + BONUS: 2, + BASE: 1, + }; + + private readonly SYMBOLS = { + MONEY: '💰', + SPARKLE: '✨', + GEM: '💎', + STAR: '🌟', + }; + + constructor() { + // Configure GSAP defaults + gsap.config({ + autoSleep: 60, + force3D: true, + nullTargetWarn: false, + }); + } + + animateEntrance(element: ElementRef, delay: number = 0) { + return gsap.from(element.nativeElement, { + duration: 0.6, + opacity: 0, + y: 30, + ease: 'power3.out', + delay, + clearProps: 'all', + }); + } + + animateFloat(element: ElementRef) { + return gsap.to(element.nativeElement, { + duration: 2, + y: '-=20', + ease: 'power1.inOut', + yoyo: true, + repeat: -1, + }); + } + + animateShine(element: ElementRef) { + const shine = gsap.to(element.nativeElement, { + duration: 1.5, + backgroundPosition: '200%', + ease: 'linear', + repeat: -1, + }); + return shine; + } + + animateMorphingBackground(element: ElementRef) { + return gsap.to(element.nativeElement, { + duration: 8, + borderRadius: '60% 40% 30% 70% / 60% 30% 70% 40%', + ease: 'sine.inOut', + repeat: -1, + yoyo: true, + }); + } + + animateOnScroll(element: ElementRef, animation: 'fadeIn' | 'slideUp' | 'scaleIn' = 'fadeIn') { + const animations = { + fadeIn: { + opacity: 0, + y: 0, + duration: 0.6, + }, + slideUp: { + opacity: 0, + y: 50, + duration: 0.8, + }, + scaleIn: { + opacity: 0, + scale: 0.8, + duration: 0.6, + }, + }; + + return gsap.from(element.nativeElement, { + ...animations[animation], + ease: 'power2.out', + scrollTrigger: { + trigger: element.nativeElement, + start: 'top bottom-=100', + toggleActions: 'play none none reverse', + }, + }); + } + + createParticleEffect(container: ElementRef, particleCount: number = 20): void { + const particles = Array.from({ length: particleCount }, () => this.createParticle(container)); + particles.forEach((particle) => this.animateParticle(particle)); + } + + private createParticle(container: ElementRef): HTMLElement { + const particle = document.createElement('div'); + particle.className = 'absolute w-2 h-2 bg-emerald-500/20 rounded-full'; + container.nativeElement.appendChild(particle); + + gsap.set(particle, { + x: gsap.utils.random(0, container.nativeElement.offsetWidth), + y: gsap.utils.random(0, container.nativeElement.offsetHeight), + }); + + return particle; + } + + private animateParticle(particle: HTMLElement): void { + gsap.to(particle, { + duration: gsap.utils.random(2, 4), + x: '+=50', + y: '-=50', + opacity: 0, + scale: 0, + ease: 'none', + repeat: -1, + onRepeat: () => this.resetParticle(particle), + }); + } + + private resetParticle(particle: HTMLElement): void { + gsap.set(particle, { + x: gsap.utils.random(0, particle.parentElement!.offsetWidth), + y: gsap.utils.random(0, particle.parentElement!.offsetHeight), + opacity: 1, + scale: 1, + }); + } + + animateButtonClick(element: ElementRef): gsap.core.Timeline { + return gsap + .timeline() + .to(element.nativeElement, { scale: 0.95, duration: 0.1 }) + .to(element.nativeElement, { scale: 1, duration: 0.2, ease: 'elastic.out(1, 0.3)' }); + } + + animateSuccess(element: ElementRef): gsap.core.Timeline { + return gsap + .timeline() + .to(element.nativeElement, { scale: 1.2, duration: 0.2, ease: 'power2.out' }) + .to(element.nativeElement, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.3)' }); + } + + animateJackpotCounter( + element: ElementRef, + startValue: number, + endValue: number + ): gsap.core.Timeline { + const container = this.prepareContainer(element); + const increase = endValue - startValue; + const timeline = gsap.timeline(); + + if (increase > this.FLASH_THRESHOLD) { + this.addFlashEffect(timeline, container, element); + } + + this.addCounterAnimation(timeline, element, startValue, endValue); + + if (increase > this.MEGA_BONUS_THRESHOLD) { + this.addMegaBonusEffect(element); + } else if (increase > this.BONUS_THRESHOLD) { + this.addBonusEffect(element); + } + + return timeline; + } + + private prepareContainer(element: ElementRef): HTMLElement { + const container = element.nativeElement.parentElement; + container.style.position = 'relative'; + this.cleanupExistingEffects(container); + return container; + } + + private cleanupExistingEffects(container: HTMLElement): void { + const existingEffects = container.querySelectorAll('.jackpot-effect'); + existingEffects.forEach((effect: Element) => effect.remove()); + } + + private addFlashEffect( + timeline: gsap.core.Timeline, + container: HTMLElement, + element: ElementRef + ): void { + const flash = this.createFlashElement(); + container.appendChild(flash); + + timeline + .to(flash, { + opacity: 1, + duration: 0.3, + yoyo: true, + repeat: 2, + onComplete: () => flash.remove(), + }) + .to( + element.nativeElement, + { + color: '#FFD700', + textShadow: '0 0 20px rgba(255,215,0,0.8)', + scale: 1.1, + duration: 0.6, + yoyo: true, + repeat: 1, + }, + '<' + ); + } + + private createFlashElement(): HTMLElement { + const flash = document.createElement('div'); + flash.className = + 'jackpot-effect absolute inset-0 bg-yellow-400/20 rounded-xl backdrop-blur-sm z-10'; + return flash; + } + + private addCounterAnimation( + timeline: gsap.core.Timeline, + element: ElementRef, + startValue: number, + endValue: number + ): void { + const obj = { value: startValue }; + timeline.to(obj, { + duration: this.calculateDuration(endValue - startValue), + value: endValue, + ease: 'power1.inOut', + onUpdate: () => this.updateCounter(obj.value, endValue, element), + onComplete: () => this.resetElementStyles(element, endValue), + }); + } + + private updateCounter(currentValue: number, endValue: number, element: ElementRef): void { + const progress = currentValue / endValue; + const fluctuation = Math.random() * (100 * (1 - progress)) - 50 * (1 - progress); + const displayValue = Math.floor(currentValue + fluctuation); + element.nativeElement.textContent = '€' + displayValue.toLocaleString(); + } + + private resetElementStyles(element: ElementRef, finalValue: number): void { + element.nativeElement.textContent = '€' + finalValue.toLocaleString(); + element.nativeElement.style.color = ''; + element.nativeElement.style.textShadow = ''; + element.nativeElement.style.transform = ''; + } + + private calculateDuration(increase: number): number { + if (increase > this.MEGA_BONUS_THRESHOLD) return this.ANIMATION_DURATIONS.MEGA; + if (increase > this.BONUS_THRESHOLD) return this.ANIMATION_DURATIONS.BONUS; + return this.ANIMATION_DURATIONS.BASE; + } + + private addMegaBonusEffect(element: ElementRef): void { + const effectContainer = this.createEffectContainer(element); + this.addGlowEffect(effectContainer, true); + this.addFloatingSymbols(element, effectContainer, 10, 50); + } + + private addBonusEffect(element: ElementRef): void { + const effectContainer = this.createEffectContainer(element); + this.addGlowEffect(effectContainer, false); + this.addFloatingSymbols(element, effectContainer, 5, 30); + } + + private createEffectContainer(element: ElementRef): HTMLElement { + const container = element.nativeElement.parentElement; + const effectContainer = document.createElement('div'); + effectContainer.className = + 'jackpot-effect absolute inset-0 pointer-events-none overflow-hidden z-20'; + container.appendChild(effectContainer); + return effectContainer; + } + + private addGlowEffect(container: HTMLElement, isMega: boolean): void { + const config = isMega + ? { + shadow: '0 0 30px rgba(255,215,0,0.8), 0 0 60px rgba(255,165,0,0.6)', + scale: 1.1, + duration: 1.5, + repeat: 2, + } + : { + shadow: '0 0 20px rgba(255,215,0,0.4)', + scale: 1.05, + duration: 0.8, + repeat: 1, + }; + + gsap.to(container, { + boxShadow: config.shadow, + scale: config.scale, + opacity: 0, + duration: config.duration, + repeat: config.repeat, + ease: 'power2.inOut', + onComplete: () => container.remove(), + }); + } + + private addFloatingSymbols( + element: ElementRef, + container: HTMLElement, + count: number, + radius: number + ): void { + const rect = element.nativeElement.getBoundingClientRect(); + const centerX = rect.width / 2; + const centerY = rect.height / 2; + const symbols = Object.values(this.SYMBOLS); + + Array.from({ length: count }).forEach((_, i) => { + const symbol = this.createSymbol(symbols, container); + const angle = (i / count) * Math.PI * 2; + this.animateSymbol(symbol, centerX, centerY, angle, radius); + }); + } + + private createSymbol(symbols: string[], container: HTMLElement): HTMLElement { + const symbol = document.createElement('div'); + symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)]; + symbol.className = 'jackpot-effect absolute text-2xl'; + container.appendChild(symbol); + return symbol; + } + + private animateSymbol( + symbol: HTMLElement, + centerX: number, + centerY: number, + angle: number, + radius: number + ): void { + gsap.fromTo( + symbol, + { + x: centerX, + y: centerY, + opacity: 0, + scale: 0, + }, + { + x: centerX + Math.cos(angle) * radius, + y: centerY + Math.sin(angle) * radius, + opacity: 1, + scale: 1, + duration: 1.5, + ease: 'back.out(1.2)', + onComplete: () => this.fadeOutSymbol(symbol), + } + ); + } + + private fadeOutSymbol(symbol: HTMLElement): void { + gsap.to(symbol, { + opacity: 0, + scale: 0, + duration: 0.5, + onComplete: () => symbol.remove(), + }); + } +} diff --git a/frontend/src/app/services/game.service.ts b/frontend/src/app/services/game.service.ts new file mode 100644 index 0000000..4f0509a --- /dev/null +++ b/frontend/src/app/services/game.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export interface Game { + id: string; + name: string; + description: string; + imageUrl: string; + minBet: number; + maxBet: number; + rtp: number; + lastWin: number; + winChance: number; + lastWinner: string; + trending: boolean; + maxWin: number; + popularity: number; + volatility: 'low' | 'medium' | 'high'; + features: string[]; +} + +@Injectable({ + providedIn: 'root', +}) +export class GameService { + private readonly INITIAL_GAMES: Game[] = [ + { + id: 'mega-fortune', + name: 'Mega Fortune Dreams', + description: '🔥 Progressive Jackpot at €1.2M - Must Drop Today!', + imageUrl: 'assets/games/mega-fortune.jpg', + minBet: 0.2, + maxBet: 100, + rtp: 96.5, + lastWin: 15789, + winChance: 99.9, + lastWinner: 'VIP Player', + trending: true, + maxWin: 1000000, + popularity: 98, + volatility: 'high', + features: ['Progressive Jackpot', 'Free Spins', 'Multipliers'], + }, + { + id: 'lightning-roulette', + name: 'Lightning Roulette', + description: '⚡️ 500x Multipliers Active - Hot Streak!', + imageUrl: 'assets/games/lightning-roulette.jpg', + minBet: 1, + maxBet: 500, + rtp: 97.1, + lastWin: 23456, + winChance: 99.7, + lastWinner: 'New Player', + trending: true, + maxWin: 500000, + popularity: 95, + volatility: 'medium', + features: ['Lightning Multipliers', 'Live Dealer', 'Instant Wins'], + }, + ]; + + private readonly STAT_RANGES = { + WIN: { + MIN: 10000, + MAX: 50000, + }, + WIN_CHANCE: { + MIN: 99, + MAX: 100, + }, + POPULARITY: { + MIN: 80, + MAX: 100, + }, + }; + + private readonly games = new BehaviorSubject(this.INITIAL_GAMES); + readonly games$ = this.games.asObservable(); + + updateGameStats(): void { + const updatedGames = this.games.value.map((game) => ({ + ...game, + ...this.generateNewStats(), + })); + this.games.next(updatedGames); + } + + getGameById(id: string): Game | undefined { + return this.games.value.find((game) => game.id === id); + } + + private generateNewStats(): Partial { + return { + lastWin: this.getRandomInRange(this.STAT_RANGES.WIN), + winChance: this.getRandomInRange(this.STAT_RANGES.WIN_CHANCE), + popularity: this.getRandomInRange(this.STAT_RANGES.POPULARITY), + }; + } + + private getRandomInRange(range: { MIN: number; MAX: number }): number { + return Math.floor(Math.random() * (range.MAX - range.MIN)) + range.MIN; + } +} diff --git a/frontend/src/app/services/jackpot.service.ts b/frontend/src/app/services/jackpot.service.ts new file mode 100644 index 0000000..d13928d --- /dev/null +++ b/frontend/src/app/services/jackpot.service.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class JackpotService { + private readonly INITIAL_JACKPOT = 1234567; + private readonly INITIAL_TIME = '04:59'; + private readonly UPDATE_INTERVAL = 2000; + + private readonly INCREASE_THRESHOLDS = { + MEGA: 0.997, + BONUS: 0.97, + BASE: 0.5, + }; + + private readonly INCREASE_RANGES = { + MEGA: { + MIN: 30000, + MAX: 100000, + }, + BONUS: { + MIN: 2000, + MAX: 15000, + }, + BASE: { + MIN: 100, + MAX: 1000, + }, + }; + + private readonly TIME_LIMITS = { + MINUTES: 4, + SECONDS: 59, + URGENT_THRESHOLD: 30, + }; + + private readonly jackpot = new BehaviorSubject(this.INITIAL_JACKPOT); + private readonly timeLeft = new BehaviorSubject(this.INITIAL_TIME); + private lastUpdateTime = Date.now(); + private minutes = this.TIME_LIMITS.MINUTES; + private seconds = this.TIME_LIMITS.SECONDS; + + readonly currentJackpot$ = this.jackpot.asObservable(); + readonly timeLeft$ = this.timeLeft.asObservable(); + + updateJackpot(): void { + if (!this.shouldUpdate()) return; + + const increase = this.calculateIncrease(); + if (increase > 0) { + this.updateJackpotValue(increase); + this.lastUpdateTime = Date.now(); + } + } + + updateTimeLeft(): void { + this.updateTimers(); + this.updateTimeDisplay(); + } + + isUrgent(): boolean { + return this.minutes === 0 && this.seconds <= this.TIME_LIMITS.URGENT_THRESHOLD; + } + + private shouldUpdate(): boolean { + return Date.now() - this.lastUpdateTime >= this.UPDATE_INTERVAL; + } + + private calculateIncrease(): number { + const random = Math.random(); + + if (random > this.INCREASE_THRESHOLDS.MEGA) { + return this.getRandomIncrease(this.INCREASE_RANGES.MEGA); + } + + if (random > this.INCREASE_THRESHOLDS.BONUS) { + return this.getRandomIncrease(this.INCREASE_RANGES.BONUS); + } + + if (random > this.INCREASE_THRESHOLDS.BASE) { + return this.getRandomIncrease(this.INCREASE_RANGES.BASE); + } + + return 0; + } + + private getRandomIncrease(range: { MIN: number; MAX: number }): number { + return Math.floor(Math.random() * (range.MAX - range.MIN) + range.MIN); + } + + private updateJackpotValue(increase: number): void { + this.jackpot.next(this.jackpot.value + increase); + } + + private updateTimers(): void { + if (this.seconds === 0) { + this.handleMinuteChange(); + } else { + this.seconds--; + } + } + + private handleMinuteChange(): void { + if (this.minutes === 0) { + this.resetTimers(); + } else { + this.minutes--; + this.seconds = this.TIME_LIMITS.SECONDS; + } + } + + private resetTimers(): void { + this.minutes = this.TIME_LIMITS.MINUTES; + this.seconds = this.TIME_LIMITS.SECONDS; + } + + private updateTimeDisplay(): void { + this.timeLeft.next(this.formatTime()); + } + + private formatTime(): string { + return `${this.padNumber(this.minutes)}:${this.padNumber(this.seconds)}`; + } + + private padNumber(num: number): string { + return num.toString().padStart(2, '0'); + } +} diff --git a/frontend/src/app/services/popup.service.ts b/frontend/src/app/services/popup.service.ts new file mode 100644 index 0000000..2e8ec99 --- /dev/null +++ b/frontend/src/app/services/popup.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export interface Popup { + title: string; + message: string; + type: 'win' | 'offer' | 'urgent' | 'fomo'; + cta: string; + expires?: string; + subMessage?: string; + imageUrl?: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class PopupService { + private readonly POPUP_TEMPLATES: Popup[] = [ + { + title: '🎯 VIP OFFER', + message: 'Enhanced RTP + 500% Bonus on Next 5 Deposits', + subMessage: 'Limited availability - 3 spots remaining', + type: 'urgent', + cta: 'Claim VIP Bonus', + expires: '5:00', + }, + { + title: '🎰 BIG WIN ALERT', + message: 'Recent win: €89,432 on minimum bet!', + subMessage: 'Game is hot - Enhanced win rate active', + type: 'win', + cta: 'Play Now', + }, + ]; + + private readonly DISPLAY_CONFIG = { + MIN_INTERVAL: 30000, + AUTO_CLOSE_DELAY: 8000, + SHOW_CHANCE: 0.7, + }; + + private readonly popupState = new BehaviorSubject(false); + private readonly currentPopup = new BehaviorSubject(null); + private lastPopupTime = 0; + + readonly showPopup$ = this.popupState.asObservable(); + readonly currentPopup$ = this.currentPopup.asObservable(); + + showRandomPopup(): void { + if (!this.shouldShowPopup()) return; + + const popup = this.getRandomPopup(); + this.displayPopup(popup); + this.scheduleAutoClose(); + this.updateLastPopupTime(); + } + + showSpecificPopup(popup: Popup): void { + if (!this.shouldShowPopup()) return; + + this.displayPopup(popup); + this.updateLastPopupTime(); + } + + closePopup(): void { + this.popupState.next(false); + } + + private shouldShowPopup(): boolean { + const now = Date.now(); + const timeSinceLastPopup = now - this.lastPopupTime; + const isMinIntervalPassed = timeSinceLastPopup >= this.DISPLAY_CONFIG.MIN_INTERVAL; + const isCurrentlyHidden = !this.popupState.value; + const isRandomChanceSuccess = Math.random() <= this.DISPLAY_CONFIG.SHOW_CHANCE; + + return isMinIntervalPassed && isCurrentlyHidden && isRandomChanceSuccess; + } + + private getRandomPopup(): Popup { + const randomIndex = Math.floor(Math.random() * this.POPUP_TEMPLATES.length); + return this.POPUP_TEMPLATES[randomIndex]; + } + + private displayPopup(popup: Popup): void { + this.currentPopup.next(popup); + this.popupState.next(true); + } + + private scheduleAutoClose(): void { + setTimeout(() => this.closePopup(), this.DISPLAY_CONFIG.AUTO_CLOSE_DELAY); + } + + private updateLastPopupTime(): void { + this.lastPopupTime = Date.now(); + } +} diff --git a/frontend/src/app/services/winner.service.ts b/frontend/src/app/services/winner.service.ts new file mode 100644 index 0000000..155ba3b --- /dev/null +++ b/frontend/src/app/services/winner.service.ts @@ -0,0 +1,120 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export interface Winner { + name: string; + amount: number; + game: string; + timestamp: Date; + isVIP: boolean; + betAmount?: number; + multiplier?: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class WinnerService { + private readonly INITIAL_ONLINE_PLAYERS = 2547; + private readonly TOTAL_PLAYERS_TODAY = 15789; + private readonly TOTAL_WINNERS_TODAY = 12453; + private readonly VIP_CHANCE = 0.3; + private readonly MAX_RECENT_WINNERS = 10; + + private readonly PLAYER_NAMES = { + FIRST: ['Alex', 'Maria', 'John', 'Sarah', 'Mike', 'Lisa', 'David', 'Emma'], + LAST: ['K.', 'S.', 'M.', 'L.', 'R.', 'T.', 'B.', 'W.'], + }; + + private readonly ONLINE_PLAYERS_LIMITS = { + MIN: 2000, + MAX: 3500, + CHANGE_RANGE: 15, + }; + + private readonly winners = new BehaviorSubject([]); + private readonly onlinePlayers = new BehaviorSubject(this.INITIAL_ONLINE_PLAYERS); + + readonly recentWinners$ = this.winners.asObservable(); + readonly onlinePlayers$ = this.onlinePlayers.asObservable(); + + constructor() { + this.initializeWinners(); + } + + generateNewWinner(game: string, baseAmount: number): void { + const winner = this.createWinner(game, baseAmount); + this.updateWinnersList(winner); + } + + updateOnlinePlayers(): void { + const currentCount = this.onlinePlayers.value; + const newCount = this.calculateNewPlayerCount(currentCount); + this.onlinePlayers.next(newCount); + } + + getTotalPlayersToday(): number { + return this.TOTAL_PLAYERS_TODAY; + } + + getTotalWinnersToday(): number { + return this.TOTAL_WINNERS_TODAY; + } + + private initializeWinners(): void { + const initialWinners = [ + this.createWinner('Mega Fortune Dreams', 15432), + this.createWinner('Lightning Roulette', 8745), + this.createWinner('Golden Tiger', 12321), + ]; + this.winners.next(initialWinners); + } + + private createWinner(game: string, baseAmount: number): Winner { + const betAmount = this.calculateBetAmount(); + const multiplier = Math.floor(baseAmount / betAmount); + + return { + name: this.generateRandomName(), + amount: baseAmount, + game, + timestamp: new Date(), + isVIP: Math.random() > this.VIP_CHANCE, + betAmount, + multiplier, + }; + } + + private calculateBetAmount(): number { + return Math.floor(Math.random() * 100) + 10; + } + + private updateWinnersList(winner: Winner): void { + const currentWinners = this.winners.value; + const updatedWinners = [winner, ...currentWinners]; + + if (updatedWinners.length > this.MAX_RECENT_WINNERS) { + updatedWinners.pop(); + } + + this.winners.next(updatedWinners); + } + + private generateRandomName(): string { + const firstName = this.getRandomArrayElement(this.PLAYER_NAMES.FIRST); + const lastName = this.getRandomArrayElement(this.PLAYER_NAMES.LAST); + return `${firstName} ${lastName}`; + } + + private calculateNewPlayerCount(currentCount: number): number { + const change = Math.floor(Math.random() * this.ONLINE_PLAYERS_LIMITS.CHANGE_RANGE) - 5; + return Math.max( + this.ONLINE_PLAYERS_LIMITS.MIN, + Math.min(this.ONLINE_PLAYERS_LIMITS.MAX, currentCount + change) + ); + } + + private getRandomArrayElement(array: T[]): T { + return array[Math.floor(Math.random() * array.length)]; + } +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f1d8c73..d4b5078 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1 +1 @@ -@import "tailwindcss"; +@import 'tailwindcss'; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..843fec3 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,218 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{html,ts}", + ], + theme: { + extend: { + animation: { + 'fadeIn': 'fadeIn 0.3s ease-out', + 'backdropBlur': 'backdropBlur 0.4s ease-out', + 'modalSlideIn': 'modalSlideIn 0.5s cubic-bezier(0.16,1,0.3,1)', + 'slideDown': 'slideDown 0.6s ease-out', + 'slideUp': 'slideUp 0.6s ease-out', + 'scaleIn': 'scaleIn 0.8s cubic-bezier(0.16,1,0.3,1)', + 'spinAndBounce': 'spinAndBounce 3s ease-in-out infinite', + 'glow': 'glow 4s ease-in-out infinite', + 'marquee': 'marquee 30s linear infinite', + 'float': 'float 6s ease-in-out infinite', + 'tiltAndGlow': 'tiltAndGlow 3s ease-in-out infinite', + 'shimmer': 'shimmer 2s linear infinite', + 'morphBackground': 'morphBackground 10s ease-in-out infinite', + 'elasticScale': 'elasticScale 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55)', + 'bounceAndFade': 'bounceAndFade 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)', + 'rotateAndScale': 'rotateAndScale 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55)', + 'pulseGlow': 'pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + keyframes: { + fadeIn: { + 'from': { opacity: '0' }, + 'to': { opacity: '1' } + }, + backdropBlur: { + 'from': { + 'backdrop-filter': 'blur(0px)', + 'background-color': 'rgba(0,0,0,0)' + }, + 'to': { + 'backdrop-filter': 'blur(16px)', + 'background-color': 'rgba(0,0,0,0.95)' + } + }, + modalSlideIn: { + 'from': { + opacity: '0', + transform: 'scale(0.95) translateY(10px)' + }, + 'to': { + opacity: '1', + transform: 'scale(1) translateY(0)' + } + }, + slideDown: { + 'from': { + opacity: '0', + transform: 'translateY(-20px)' + }, + 'to': { + opacity: '1', + transform: 'translateY(0)' + } + }, + slideUp: { + 'from': { + opacity: '0', + transform: 'translateY(20px)' + }, + 'to': { + opacity: '1', + transform: 'translateY(0)' + } + }, + scaleIn: { + 'from': { + opacity: '0', + transform: 'scale(0.9)' + }, + 'to': { + opacity: '1', + transform: 'scale(1)' + } + }, + spinAndBounce: { + '0%': { + transform: 'scale(1) rotate(0deg)' + }, + '50%': { + transform: 'scale(1.2) rotate(180deg)' + }, + '100%': { + transform: 'scale(1) rotate(360deg)' + } + }, + glow: { + '0%, 100%': { + opacity: '0.3', + transform: 'scale(1)' + }, + '50%': { + opacity: '0.5', + transform: 'scale(1.05)' + } + }, + marquee: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-100%)' } + }, + float: { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-20px)' } + }, + tiltAndGlow: { + '0%, 100%': { + transform: 'perspective(1000px) rotateX(0deg) rotateY(0deg)', + 'box-shadow': '0 0 20px rgba(34,197,94,0.2)' + }, + '50%': { + transform: 'perspective(1000px) rotateX(2deg) rotateY(5deg)', + 'box-shadow': '0 0 40px rgba(34,197,94,0.4)' + } + }, + shimmer: { + '0%': { + 'background-position': '-1000px 0' + }, + '100%': { + 'background-position': '1000px 0' + } + }, + morphBackground: { + '0%, 100%': { + 'border-radius': '60% 40% 30% 70%/60% 30% 70% 40%' + }, + '50%': { + 'border-radius': '30% 60% 70% 40%/50% 60% 30% 60%' + } + }, + elasticScale: { + '0%': { + transform: 'scale(0)' + }, + '60%': { + transform: 'scale(1.1)' + }, + '100%': { + transform: 'scale(1)' + } + }, + bounceAndFade: { + '0%': { + transform: 'scale(0.3)', + opacity: '0' + }, + '50%': { + transform: 'scale(1.05)', + opacity: '0.8' + }, + '100%': { + transform: 'scale(1)', + opacity: '1' + } + }, + rotateAndScale: { + '0%': { + transform: 'rotate(-180deg) scale(0)' + }, + '100%': { + transform: 'rotate(0) scale(1)' + } + }, + pulseGlow: { + '0%, 100%': { + opacity: '1', + transform: 'scale(1)', + filter: 'brightness(1)' + }, + '50%': { + opacity: '0.8', + transform: 'scale(1.05)', + filter: 'brightness(1.2)' + } + } + }, + transitionDelay: { + '100': '100ms', + '200': '200ms', + '300': '300ms', + '400': '400ms', + '500': '500ms', + }, + transitionTimingFunction: { + 'bounce': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + 'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', + 'elastic': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + 'spring': 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', + }, + backdropBlur: { + 'xs': '2px', + '4xl': '72px', + '5xl': '96px', + }, + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + 'shimmer': 'linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent)', + } + }, + }, + plugins: [ + require('@tailwindcss/aspect-ratio'), + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('tailwindcss-animated'), + require('tailwindcss-gradients'), + require('tailwindcss-transforms'), + require('tailwindcss-filters'), + require('tailwind-scrollbar-hide'), + ], +}