From e6e7d656028fac710a74b0f4ca3ab5d61c16f125 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:33:31 +0100 Subject: [PATCH 001/368] feat(landing): add landing component with animations and services --- frontend/bun.lock | 6 + frontend/package.json | 2 + frontend/src/app/app.config.ts | 8 +- frontend/src/app/app.routes.ts | 8 +- .../src/app/landing/landing.component.html | 481 ++++++++++++++++++ frontend/src/app/landing/landing.component.ts | 253 +++++++++ .../src/app/services/animation.service.ts | 380 ++++++++++++++ frontend/src/app/services/game.service.ts | 104 ++++ frontend/src/app/services/jackpot.service.ts | 130 +++++ frontend/src/app/services/popup.service.ts | 96 ++++ frontend/src/app/services/winner.service.ts | 120 +++++ frontend/src/styles.css | 2 +- frontend/tailwind.config.js | 218 ++++++++ 13 files changed, 1805 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/landing/landing.component.html create mode 100644 frontend/src/app/landing/landing.component.ts create mode 100644 frontend/src/app/services/animation.service.ts create mode 100644 frontend/src/app/services/game.service.ts create mode 100644 frontend/src/app/services/jackpot.service.ts create mode 100644 frontend/src/app/services/popup.service.ts create mode 100644 frontend/src/app/services/winner.service.ts create mode 100644 frontend/tailwind.config.js 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'), + ], +} -- 2.45.3 From 50eb399fe2c3dd866f3fbf0a09a7d959e6dca88e Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:40:15 +0100 Subject: [PATCH 002/368] feat: implement animated buttons and winner ticker component --- .../src/app/landing/landing.component.html | 164 ++---------------- frontend/src/app/landing/landing.component.ts | 6 +- .../animated-button.component.ts | 48 +++++ .../game-card/game-card.component.ts | 98 +++++++++++ .../winner-ticker/winner-ticker.component.ts | 32 ++++ 5 files changed, 202 insertions(+), 146 deletions(-) create mode 100644 frontend/src/app/shared/components/animated-button/animated-button.component.ts create mode 100644 frontend/src/app/shared/components/game-card/game-card.component.ts create mode 100644 frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index 5922ccc..aeaa65c 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -6,20 +6,7 @@ [class.py-1.5]="!isScrolled" >
-
- - 🎰 - - {{ winner.name }} - {{ winner.isVIP ? '(VIP)' : '' }} - won €{{ winner.amount | number }} - ({{ winner.multiplier }}x) - - -
+
@@ -60,15 +47,7 @@ (78.9% Win Rate) - + START PLAYING @@ -157,17 +136,9 @@
- + + CLAIM YOUR €10,000 NOW +
@@ -190,25 +161,7 @@
-
-
- - - 🎰 {{ winner.name }} - {{ - winner.isVIP ? '(VIP)' : '' - }} - turned €{{ winner.betAmount }} into - €{{ winner.amount | number }} - ({{ winner.multiplier }}x) - - -
-
+
@@ -220,79 +173,12 @@
-
-
- -
-
-
-

{{ 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 }} - -
- -
-
-
-
-
+
@@ -396,17 +282,9 @@ > Maybe later - + + {{ popup.cta }} +
- + SPIN AGAIN + diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index 4aceb54..1f0ff86 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -22,10 +22,14 @@ import { WinnerService } from '../services/winner.service'; import { JackpotService } from '../services/jackpot.service'; import { AnimationService } from '../services/animation.service'; +import { AnimatedButtonComponent } from '../shared/components/animated-button/animated-button.component'; +import { WinnerTickerComponent } from '../shared/components/winner-ticker/winner-ticker.component'; +import { GameCardComponent } from '../shared/components/game-card/game-card.component'; + @Component({ selector: 'app-landing', standalone: true, - imports: [CommonModule], + imports: [CommonModule, AnimatedButtonComponent, WinnerTickerComponent, GameCardComponent], templateUrl: './landing.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts new file mode 100644 index 0000000..959ef17 --- /dev/null +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AnimationService } from '../../../services/animation.service'; + +@Component({ + selector: 'app-animated-button', + standalone: true, + imports: [CommonModule], + template: ` + + `, + styles: [], +}) +export class AnimatedButtonComponent { + @Input() variant: 'primary' | 'secondary' = 'primary'; + @Input() size: 'normal' | 'large' = 'normal'; + @Output() buttonClick = new EventEmitter(); + + constructor(private animationService: AnimationService) {} + + get buttonClass(): string { + const baseClass = + 'relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; + const variantClass = + this.variant === 'primary' + ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' + : 'bg-white/10 text-white hover:bg-white/20'; + + return `${baseClass} ${variantClass}`; + } + + handleClick(event: MouseEvent): void { + const elementRef = new ElementRef(event.currentTarget); + this.animationService.animateButtonClick(elementRef); + this.buttonClick.emit(event); + } +} diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts new file mode 100644 index 0000000..7c8778a --- /dev/null +++ b/frontend/src/app/shared/components/game-card/game-card.component.ts @@ -0,0 +1,98 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Game } from '../../../services/game.service'; +import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; +import { AnimationService } from '../../../services/animation.service'; + +@Component({ + selector: 'app-game-card', + standalone: true, + imports: [CommonModule, AnimatedButtonComponent], + template: ` +
+
+ +
+
+
+

{{ 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 }} + +
+ PLAY NOW +
+
+
+
+
+ `, + styles: [], +}) +export class GameCardComponent { + @Input() game!: Game; + @Output() play = new EventEmitter(); + + constructor(private animationService: AnimationService) {} + + onHover(event: MouseEvent): void { + const element = event.currentTarget as HTMLElement; + const elementRef = new ElementRef(element); + this.animationService.animateFloat(elementRef); + } + + onPlay(): void { + this.play.emit(); + } +} diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts new file mode 100644 index 0000000..c22e661 --- /dev/null +++ b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { default as autoAnimate } from '@formkit/auto-animate'; +import { Winner } from '../../../services/winner.service'; + +@Component({ + selector: 'app-winner-ticker', + standalone: true, + imports: [CommonModule], + template: ` +
+ + 🎰 + + {{ winner.name }} + {{ winner.isVIP ? '(VIP)' : '' }} + won €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+ `, + styles: [], +}) +export class WinnerTickerComponent implements AfterViewInit { + @Input() winners: Winner[] = []; + @ViewChild('tickerContainer') tickerContainer!: ElementRef; + + ngAfterViewInit(): void { + autoAnimate(this.tickerContainer.nativeElement); + } +} -- 2.45.3 From 849a82734af4b2aabe4553eac423ac4db862eb79 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:43:04 +0100 Subject: [PATCH 003/368] refactor: improve code structure and add change detection --- .../src/app/landing/landing.component.html | 4 ++-- frontend/src/app/landing/landing.component.ts | 18 ++++++++++++------ .../animated-button.component.ts | 10 +++++++++- .../game-card/game-card.component.ts | 10 +++++++++- .../winner-ticker/winner-ticker.component.ts | 10 +++++++++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index aeaa65c..8bb0c62 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -93,9 +93,9 @@
- €{{ currentJackpot$ | async | number }} + €{{ (currentJackpot$ | async) ?? 0 | number }} ↑
diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index 1f0ff86..e2b0afb 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -112,14 +112,20 @@ export class LandingComponent implements OnInit, OnDestroy, AfterViewInit { 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); + const gameCards = this.gamesGrid?.nativeElement.querySelectorAll('.game-card'); + if (gameCards) { + gameCards.forEach((card: HTMLElement, index: number) => { + this.animationService.animateEntrance(new ElementRef(card), 0.1 * index); + }); + } + this.animationService.animateOnScroll(this.gamesGrid, 'slideUp'); + this.currentJackpot$.pipe(takeUntil(this.destroy$)).subscribe((value) => { - this.animationService.animateJackpotCounter(this.jackpotCounter, value - 1000, value); + if (this.jackpotCounter && value) { + const prevValue = value - Math.floor(Math.random() * 1000 + 500); + this.animationService.animateJackpotCounter(this.jackpotCounter, prevValue, value); + } }); } diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts index 959ef17..1ce932d 100644 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + ElementRef, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { AnimationService } from '../../../services/animation.service'; @@ -21,6 +28,7 @@ import { AnimationService } from '../../../services/animation.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AnimatedButtonComponent { @Input() variant: 'primary' | 'secondary' = 'primary'; diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts index 7c8778a..78dacac 100644 --- a/frontend/src/app/shared/components/game-card/game-card.component.ts +++ b/frontend/src/app/shared/components/game-card/game-card.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + ElementRef, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { Game } from '../../../services/game.service'; import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; @@ -79,6 +86,7 @@ import { AnimationService } from '../../../services/animation.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class GameCardComponent { @Input() game!: Game; diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts index c22e661..7e0c733 100644 --- a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts +++ b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { + Component, + Input, + ViewChild, + ElementRef, + AfterViewInit, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { default as autoAnimate } from '@formkit/auto-animate'; import { Winner } from '../../../services/winner.service'; @@ -21,6 +28,7 @@ import { Winner } from '../../../services/winner.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WinnerTickerComponent implements AfterViewInit { @Input() winners: Winner[] = []; -- 2.45.3 From 94dba1ac323106b04404fceaf1ee17a21feb52aa Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:45:36 +0100 Subject: [PATCH 004/368] refactor: remove unused imports and clear image URLs --- frontend/src/app/landing/landing.component.ts | 1 - frontend/src/app/services/game.service.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index e2b0afb..cd5be3a 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -14,7 +14,6 @@ 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'; diff --git a/frontend/src/app/services/game.service.ts b/frontend/src/app/services/game.service.ts index 4f0509a..ee6ae08 100644 --- a/frontend/src/app/services/game.service.ts +++ b/frontend/src/app/services/game.service.ts @@ -28,7 +28,7 @@ export class GameService { id: 'mega-fortune', name: 'Mega Fortune Dreams', description: 'πŸ”₯ Progressive Jackpot at €1.2M - Must Drop Today!', - imageUrl: 'assets/games/mega-fortune.jpg', + imageUrl: '', minBet: 0.2, maxBet: 100, rtp: 96.5, @@ -45,7 +45,7 @@ export class GameService { id: 'lightning-roulette', name: 'Lightning Roulette', description: '⚑️ 500x Multipliers Active - Hot Streak!', - imageUrl: 'assets/games/lightning-roulette.jpg', + imageUrl: '', minBet: 1, maxBet: 500, rtp: 97.1, -- 2.45.3 From 76afbe1f1e92ec1f85a6423bf145136aa2ff71a0 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:46:44 +0100 Subject: [PATCH 005/368] style(animated-button): add cursor-pointer to button class --- .../components/animated-button/animated-button.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts index 1ce932d..85c433e 100644 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -39,7 +39,7 @@ export class AnimatedButtonComponent { get buttonClass(): string { const baseClass = - 'relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; + 'cursor-pointer relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; const variantClass = this.variant === 'primary' ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' -- 2.45.3 From 5ce483b23b644ef4db5c44e9553e70dd7179b8c2 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:52:52 +0100 Subject: [PATCH 006/368] style(landing): remove instant withdrawals section and update styles --- .../src/app/landing/landing.component.html | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index 8bb0c62..fb8924f 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -142,10 +142,6 @@
-
- βœ“ - Instant Withdrawals -
βœ“ 24/7 VIP Support @@ -159,7 +155,7 @@
@@ -190,13 +186,6 @@

Elite VIP Status

Up to €50,000 monthly rewards

-
-
⚑️
-

Instant Cashouts

-

Get paid in 5 minutes!

-
@@ -211,9 +200,15 @@

99.9% Win Rate*

Highest odds in the industry!

+
+
πŸš€
+

Exclusive Offers

+

Unlock special perks and rewards!

+
-
*Terms and conditions apply. Guaranteed winnings based on maximum bonus utilization. Win rate calculated on minimum bets. Withdrawal restrictions and wagering requirements apply. -- 2.45.3 From 2cd1c6d8fda9f1af8faad8c9fb2ad4eb52c2677f Mon Sep 17 00:00:00 2001 From: Huy Date: Wed, 12 Feb 2025 07:43:05 +0000 Subject: [PATCH 007/368] feat: Create initial landing page (CAS-8) (!11) Co-authored-by: Phan Huy Tran Reviewed-on: https://git.simonis.lol/projects/casino/pulls/11 Reviewed-by: Constantin Simonis Reviewed-by: lziemke --- frontend/src/app/app.routes.ts | 5 ++++- .../src/app/landing-page/landing-page.component.html | 0 .../src/app/landing-page/landing-page.component.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/landing-page/landing-page.component.html create mode 100644 frontend/src/app/landing-page/landing-page.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index dc39edb..3a96b8f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,3 +1,6 @@ import { Routes } from '@angular/router'; +import {LandingPageComponent} from "./landing-page/landing-page.component"; -export const routes: Routes = []; +export const routes: Routes = [ + { path: '', component: LandingPageComponent } +]; diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts new file mode 100644 index 0000000..ed8aba1 --- /dev/null +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-landing-page', + standalone: true, + imports: [], + templateUrl: './landing-page.component.html', +}) +export class LandingPageComponent { + +} -- 2.45.3 From bbbf0a94733c9e613e54c6939a9d1df5a2a3c5a9 Mon Sep 17 00:00:00 2001 From: Lea Date: Wed, 12 Feb 2025 08:50:46 +0100 Subject: [PATCH 008/368] homepage skeleton --- backend/SampleRequests.http | 0 frontend/src/app/app.routes.ts | 7 ++++ .../homepage/homepage/homepage.component.css | 0 .../homepage/homepage/homepage.component.html | 35 +++++++++++++++++++ .../homepage/homepage.component.spec.ts | 23 ++++++++++++ .../homepage/homepage/homepage.component.ts | 20 +++++++++++ 6 files changed, 85 insertions(+) create mode 100644 backend/SampleRequests.http create mode 100644 frontend/src/app/homepage/homepage/homepage.component.css create mode 100644 frontend/src/app/homepage/homepage/homepage.component.html create mode 100644 frontend/src/app/homepage/homepage/homepage.component.spec.ts create mode 100644 frontend/src/app/homepage/homepage/homepage.component.ts diff --git a/backend/SampleRequests.http b/backend/SampleRequests.http new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 3a96b8f..ce6d45a 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,13 @@ import { Routes } from '@angular/router'; import {LandingPageComponent} from "./landing-page/landing-page.component"; +import {HomepageComponent} from "./homepage/homepage/homepage.component"; export const routes: Routes = [ + { + path: 'home', + component: HomepageComponent + }, + { path: '', component: LandingPageComponent } ]; + diff --git a/frontend/src/app/homepage/homepage/homepage.component.css b/frontend/src/app/homepage/homepage/homepage.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/homepage/homepage/homepage.component.html b/frontend/src/app/homepage/homepage/homepage.component.html new file mode 100644 index 0000000..c0b2cc7 --- /dev/null +++ b/frontend/src/app/homepage/homepage/homepage.component.html @@ -0,0 +1,35 @@ + + + +
+
+

Spiel Vorschau

+

Spiel Name

+ +
+
+

Spiel Vorschau

+

Spiel Name

+ +
+
+

Spiel Vorschau

+

Spiel Name

+ +
+
diff --git a/frontend/src/app/homepage/homepage/homepage.component.spec.ts b/frontend/src/app/homepage/homepage/homepage.component.spec.ts new file mode 100644 index 0000000..8d5e4db --- /dev/null +++ b/frontend/src/app/homepage/homepage/homepage.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomepageComponent } from './homepage.component'; + +describe('HomepageComponent', () => { + let component: HomepageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomepageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomepageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/homepage/homepage/homepage.component.ts b/frontend/src/app/homepage/homepage/homepage.component.ts new file mode 100644 index 0000000..0b720e5 --- /dev/null +++ b/frontend/src/app/homepage/homepage/homepage.component.ts @@ -0,0 +1,20 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'app-homepage', + standalone: true, + imports: [], + templateUrl: './homepage.component.html', + styleUrl: './homepage.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HomepageComponent { + + onLogout() { + //TODO implement + } + + openUserInfo() { + //TODO implement + } +} -- 2.45.3 From a07f7a1f74cd2c4bbb8a4f4d431ab3785f12d114 Mon Sep 17 00:00:00 2001 From: Huy Date: Wed, 12 Feb 2025 07:43:05 +0000 Subject: [PATCH 009/368] feat: Create initial landing page (CAS-8) (!11) Co-authored-by: Phan Huy Tran Reviewed-on: https://git.simonis.lol/projects/casino/pulls/11 Reviewed-by: Constantin Simonis Reviewed-by: lziemke --- frontend/src/app/app.routes.ts | 7 ++----- .../src/app/landing-page/landing-page.component.html | 0 .../src/app/landing-page/landing-page.component.ts | 11 +++++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/landing-page/landing-page.component.html create mode 100644 frontend/src/app/landing-page/landing-page.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 36b5bb5..3a96b8f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,9 +1,6 @@ import { Routes } from '@angular/router'; -import { LandingComponent } from './landing/landing.component'; +import {LandingPageComponent} from "./landing-page/landing-page.component"; export const routes: Routes = [ - { - path: '', - component: LandingComponent, - }, + { path: '', component: LandingPageComponent } ]; diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts new file mode 100644 index 0000000..ed8aba1 --- /dev/null +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-landing-page', + standalone: true, + imports: [], + templateUrl: './landing-page.component.html', +}) +export class LandingPageComponent { + +} -- 2.45.3 From 475406dbaa288e8aeea0502be4e097b7c72ac0b5 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:33:31 +0100 Subject: [PATCH 010/368] feat(landing): add landing component with animations and services --- frontend/src/app/app.routes.ts | 7 +- .../src/app/landing/landing.component.html | 189 +++++++++++++++--- frontend/src/app/services/game.service.ts | 4 +- 3 files changed, 167 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 3a96b8f..36b5bb5 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,9 @@ import { Routes } from '@angular/router'; -import {LandingPageComponent} from "./landing-page/landing-page.component"; +import { LandingComponent } from './landing/landing.component'; export const routes: Routes = [ - { path: '', component: LandingPageComponent } + { + path: '', + component: LandingComponent, + }, ]; diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index fb8924f..5922ccc 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -6,7 +6,20 @@ [class.py-1.5]="!isScrolled" >
- +
+ + 🎰 + + {{ winner.name }} + {{ winner.isVIP ? '(VIP)' : '' }} + won €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
@@ -47,7 +60,15 @@ (78.9% Win Rate) - START PLAYING + @@ -93,9 +114,9 @@
- €{{ (currentJackpot$ | async) ?? 0 | number }} + €{{ currentJackpot$ | async | number }} ↑
@@ -136,12 +157,24 @@
- - CLAIM YOUR €10,000 NOW - +
+
+ βœ“ + Instant Withdrawals +
βœ“ 24/7 VIP Support @@ -155,9 +188,27 @@
- +
+
+ + + 🎰 {{ winner.name }} + {{ + winner.isVIP ? '(VIP)' : '' + }} + turned €{{ winner.betAmount }} into + €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+
@@ -169,12 +220,79 @@
- - +
+ +
+
+
+

{{ 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 }} + +
+ +
+
+
+
+
@@ -186,6 +304,13 @@

Elite VIP Status

Up to €50,000 monthly rewards

+
+
⚑️
+

Instant Cashouts

+

Get paid in 5 minutes!

+
@@ -200,15 +325,9 @@

99.9% Win Rate*

Highest odds in the industry!

-
-
πŸš€
-

Exclusive Offers

-

Unlock special perks and rewards!

-
+
*Terms and conditions apply. Guaranteed winnings based on maximum bonus utilization. Win rate calculated on minimum bets. Withdrawal restrictions and wagering requirements apply. @@ -277,9 +396,17 @@ > Maybe later - - {{ popup.cta }} - +
- - SPIN AGAIN - + SPIN AGAIN +
+ diff --git a/frontend/src/app/services/game.service.ts b/frontend/src/app/services/game.service.ts index ee6ae08..4f0509a 100644 --- a/frontend/src/app/services/game.service.ts +++ b/frontend/src/app/services/game.service.ts @@ -28,7 +28,7 @@ export class GameService { id: 'mega-fortune', name: 'Mega Fortune Dreams', description: 'πŸ”₯ Progressive Jackpot at €1.2M - Must Drop Today!', - imageUrl: '', + imageUrl: 'assets/games/mega-fortune.jpg', minBet: 0.2, maxBet: 100, rtp: 96.5, @@ -45,7 +45,7 @@ export class GameService { id: 'lightning-roulette', name: 'Lightning Roulette', description: '⚑️ 500x Multipliers Active - Hot Streak!', - imageUrl: '', + imageUrl: 'assets/games/lightning-roulette.jpg', minBet: 1, maxBet: 500, rtp: 97.1, -- 2.45.3 From f5ae0e358cd1f83f6276c7f45a46ebf83bfabce7 Mon Sep 17 00:00:00 2001 From: Lea Date: Wed, 12 Feb 2025 10:21:41 +0100 Subject: [PATCH 011/368] finished homepage skeleton --- .../homepage/homepage/homepage.component.css | 1 + .../homepage/homepage/homepage.component.html | 21 +++++++++---------- frontend/src/styles.css | 9 ++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/homepage/homepage/homepage.component.css b/frontend/src/app/homepage/homepage/homepage.component.css index e69de29..8b13789 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.css +++ b/frontend/src/app/homepage/homepage/homepage.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/homepage/homepage/homepage.component.html b/frontend/src/app/homepage/homepage/homepage.component.html index c0b2cc7..301581a 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.html +++ b/frontend/src/app/homepage/homepage/homepage.component.html @@ -3,33 +3,32 @@
-
-
- - -
-
+ +
+

Spiel Vorschau

Spiel Name

- +
-
+

Spiel Vorschau

Spiel Name

- +
-
+

Spiel Vorschau

Spiel Name

- +
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f1d8c73..ecb1a02 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1 +1,10 @@ @import "tailwindcss"; + + +.btn-primary { + @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20 +} +.btn-secondary { + @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-white/10 text-white hover:bg-white/20 +} + -- 2.45.3 From b010d49752cfc680fddee6025f7b73e0a8d9de1c Mon Sep 17 00:00:00 2001 From: Lea Date: Wed, 12 Feb 2025 10:26:28 +0100 Subject: [PATCH 012/368] deleted sample request file --- backend/SampleRequests.http | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/SampleRequests.http diff --git a/backend/SampleRequests.http b/backend/SampleRequests.http deleted file mode 100644 index e69de29..0000000 -- 2.45.3 From 7d4cfe179415590485246c03f309b8b4143baea5 Mon Sep 17 00:00:00 2001 From: Lea Date: Wed, 12 Feb 2025 10:31:12 +0100 Subject: [PATCH 013/368] removed not used stuff --- .../homepage/homepage/homepage.component.css | 1 - .../homepage/homepage/homepage.component.html | 4 ++-- .../homepage/homepage.component.spec.ts | 23 ------------------- .../homepage/homepage/homepage.component.ts | 8 ------- 4 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 frontend/src/app/homepage/homepage/homepage.component.css delete mode 100644 frontend/src/app/homepage/homepage/homepage.component.spec.ts diff --git a/frontend/src/app/homepage/homepage/homepage.component.css b/frontend/src/app/homepage/homepage/homepage.component.css deleted file mode 100644 index 8b13789..0000000 --- a/frontend/src/app/homepage/homepage/homepage.component.css +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/app/homepage/homepage/homepage.component.html b/frontend/src/app/homepage/homepage/homepage.component.html index 301581a..6377831 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.html +++ b/frontend/src/app/homepage/homepage/homepage.component.html @@ -3,12 +3,12 @@
-
-
diff --git a/frontend/src/app/homepage/homepage/homepage.component.spec.ts b/frontend/src/app/homepage/homepage/homepage.component.spec.ts deleted file mode 100644 index 8d5e4db..0000000 --- a/frontend/src/app/homepage/homepage/homepage.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HomepageComponent } from './homepage.component'; - -describe('HomepageComponent', () => { - let component: HomepageComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [HomepageComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(HomepageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/homepage/homepage/homepage.component.ts b/frontend/src/app/homepage/homepage/homepage.component.ts index 0b720e5..e0a1147 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.ts +++ b/frontend/src/app/homepage/homepage/homepage.component.ts @@ -5,16 +5,8 @@ import {ChangeDetectionStrategy, Component} from '@angular/core'; standalone: true, imports: [], templateUrl: './homepage.component.html', - styleUrl: './homepage.component.css', changeDetection: ChangeDetectionStrategy.OnPush }) export class HomepageComponent { - onLogout() { - //TODO implement - } - - openUserInfo() { - //TODO implement - } } -- 2.45.3 From 819b8a7bc99bb3328616eb03aba40a7fbd277c45 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 10:39:49 +0100 Subject: [PATCH 014/368] refactor: rename landing component and remove unused files --- frontend/src/app/app.routes.ts | 4 +- .../src/app/landing/landing.component.html | 481 ------------------ frontend/src/app/landing/landing.component.ts | 262 ---------- .../src/app/services/animation.service.ts | 380 -------------- frontend/src/app/services/game.service.ts | 104 ---- frontend/src/app/services/jackpot.service.ts | 130 ----- frontend/src/app/services/popup.service.ts | 96 ---- frontend/src/app/services/winner.service.ts | 120 ----- .../animated-button.component.ts | 56 -- .../game-card/game-card.component.ts | 106 ---- .../winner-ticker/winner-ticker.component.ts | 40 -- 11 files changed, 2 insertions(+), 1777 deletions(-) delete mode 100644 frontend/src/app/landing/landing.component.html delete mode 100644 frontend/src/app/landing/landing.component.ts delete mode 100644 frontend/src/app/services/animation.service.ts delete mode 100644 frontend/src/app/services/game.service.ts delete mode 100644 frontend/src/app/services/jackpot.service.ts delete mode 100644 frontend/src/app/services/popup.service.ts delete mode 100644 frontend/src/app/services/winner.service.ts delete mode 100644 frontend/src/app/shared/components/animated-button/animated-button.component.ts delete mode 100644 frontend/src/app/shared/components/game-card/game-card.component.ts delete mode 100644 frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 36b5bb5..dc64999 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,9 +1,9 @@ import { Routes } from '@angular/router'; -import { LandingComponent } from './landing/landing.component'; +import { LandingPageComponent } from './landing-page/landing-page.component'; export const routes: Routes = [ { path: '', - component: LandingComponent, + component: LandingPageComponent, }, ]; diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html deleted file mode 100644 index 5922ccc..0000000 --- a/frontend/src/app/landing/landing.component.html +++ /dev/null @@ -1,481 +0,0 @@ -
-
-
-
-
- - 🎰 - - {{ 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 deleted file mode 100644 index cd5be3a..0000000 --- a/frontend/src/app/landing/landing.component.ts +++ /dev/null @@ -1,262 +0,0 @@ -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 { 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'; - -import { AnimatedButtonComponent } from '../shared/components/animated-button/animated-button.component'; -import { WinnerTickerComponent } from '../shared/components/winner-ticker/winner-ticker.component'; -import { GameCardComponent } from '../shared/components/game-card/game-card.component'; - -@Component({ - selector: 'app-landing', - standalone: true, - imports: [CommonModule, AnimatedButtonComponent, WinnerTickerComponent, GameCardComponent], - 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'); - if (gameCards) { - gameCards.forEach((card: HTMLElement, index: number) => { - this.animationService.animateEntrance(new ElementRef(card), 0.1 * index); - }); - } - - this.animationService.animateOnScroll(this.gamesGrid, 'slideUp'); - - this.currentJackpot$.pipe(takeUntil(this.destroy$)).subscribe((value) => { - if (this.jackpotCounter && value) { - const prevValue = value - Math.floor(Math.random() * 1000 + 500); - this.animationService.animateJackpotCounter(this.jackpotCounter, prevValue, 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 deleted file mode 100644 index 34fe89a..0000000 --- a/frontend/src/app/services/animation.service.ts +++ /dev/null @@ -1,380 +0,0 @@ -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 deleted file mode 100644 index 4f0509a..0000000 --- a/frontend/src/app/services/game.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index d13928d..0000000 --- a/frontend/src/app/services/jackpot.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 2e8ec99..0000000 --- a/frontend/src/app/services/popup.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index 155ba3b..0000000 --- a/frontend/src/app/services/winner.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -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/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts deleted file mode 100644 index 85c433e..0000000 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ElementRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AnimationService } from '../../../services/animation.service'; - -@Component({ - selector: 'app-animated-button', - standalone: true, - imports: [CommonModule], - template: ` - - `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AnimatedButtonComponent { - @Input() variant: 'primary' | 'secondary' = 'primary'; - @Input() size: 'normal' | 'large' = 'normal'; - @Output() buttonClick = new EventEmitter(); - - constructor(private animationService: AnimationService) {} - - get buttonClass(): string { - const baseClass = - 'cursor-pointer relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; - const variantClass = - this.variant === 'primary' - ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' - : 'bg-white/10 text-white hover:bg-white/20'; - - return `${baseClass} ${variantClass}`; - } - - handleClick(event: MouseEvent): void { - const elementRef = new ElementRef(event.currentTarget); - this.animationService.animateButtonClick(elementRef); - this.buttonClick.emit(event); - } -} diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts deleted file mode 100644 index 78dacac..0000000 --- a/frontend/src/app/shared/components/game-card/game-card.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ElementRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Game } from '../../../services/game.service'; -import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; -import { AnimationService } from '../../../services/animation.service'; - -@Component({ - selector: 'app-game-card', - standalone: true, - imports: [CommonModule, AnimatedButtonComponent], - template: ` -
-
- -
-
-
-

{{ 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 }} - -
- PLAY NOW -
-
-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class GameCardComponent { - @Input() game!: Game; - @Output() play = new EventEmitter(); - - constructor(private animationService: AnimationService) {} - - onHover(event: MouseEvent): void { - const element = event.currentTarget as HTMLElement; - const elementRef = new ElementRef(element); - this.animationService.animateFloat(elementRef); - } - - onPlay(): void { - this.play.emit(); - } -} diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts deleted file mode 100644 index 7e0c733..0000000 --- a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Component, - Input, - ViewChild, - ElementRef, - AfterViewInit, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { default as autoAnimate } from '@formkit/auto-animate'; -import { Winner } from '../../../services/winner.service'; - -@Component({ - selector: 'app-winner-ticker', - standalone: true, - imports: [CommonModule], - template: ` -
- - 🎰 - - {{ winner.name }} - {{ winner.isVIP ? '(VIP)' : '' }} - won €{{ winner.amount | number }} - ({{ winner.multiplier }}x) - - -
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class WinnerTickerComponent implements AfterViewInit { - @Input() winners: Winner[] = []; - @ViewChild('tickerContainer') tickerContainer!: ElementRef; - - ngAfterViewInit(): void { - autoAnimate(this.tickerContainer.nativeElement); - } -} -- 2.45.3 From f31a959ec540e25676120f2c245bf75672ba5eac Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:45:42 +0100 Subject: [PATCH 015/368] build: add Gitea release workflow configuration --- .gitea/workflows/release.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..1e33a46 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release +on: + push: + +env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + +permissions: + contents: read # for checkout + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + steps: + - name: Create Release + uses: https://git.kjan.de/actions/semantic-release@main + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} -- 2.45.3 From ab9598950a4cb975b1625e46044c5b8604902a62 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:47:23 +0100 Subject: [PATCH 016/368] build: add release configuration for semantic release --- .gitea/workflows/release.yml | 2 +- release.config.cjs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 release.config.cjs diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1e33a46..7246d23 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -22,4 +22,4 @@ jobs: uses: https://git.kjan.de/actions/semantic-release@main with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} \ No newline at end of file diff --git a/release.config.cjs b/release.config.cjs new file mode 100644 index 0000000..e09738f --- /dev/null +++ b/release.config.cjs @@ -0,0 +1,15 @@ +module.exports = { + + branches: ['main'], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + '@semantic-release/changelog', + ["@saithodev/semantic-release-gitea", { + "giteaUrl": "https://git.simonis.lol" + }], + ], + }; + + + \ No newline at end of file -- 2.45.3 From 25ff804b76c09ebbab1e0b25c59770b06660dcb6 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:49:45 +0100 Subject: [PATCH 017/368] ci: add master branch trigger for release workflow --- .gitea/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 7246d23..1a4732d 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,6 +1,8 @@ name: Release on: push: + branches: + - "master" env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} -- 2.45.3 From 774e55c6a6cc34eb55919e7456b5124d76123970 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:50:51 +0100 Subject: [PATCH 018/368] ci: change runner to remote in release workflow --- .gitea/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1a4732d..9d7d6a2 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -13,7 +13,7 @@ permissions: jobs: release: name: Release - runs-on: ubuntu-latest + runs-on: remote permissions: contents: write # to be able to publish a GitHub release issues: write # to be able to comment on released issues -- 2.45.3 From f4541c5d865c8b778441b11ffb85b33041c0f1a3 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:51:57 +0100 Subject: [PATCH 019/368] ci: update branch name in release workflow config --- .gitea/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9d7d6a2..d449485 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: branches: - - "master" + - "main" env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} -- 2.45.3 From 94ac9bd491268d8485243c984cdebd685365cfda Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 10:52:49 +0100 Subject: [PATCH 020/368] style(release.yml): format permissions section in YAML --- .gitea/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index d449485..51e555c 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -8,17 +8,17 @@ env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} permissions: - contents: read # for checkout + contents: read jobs: release: name: Release runs-on: remote permissions: - contents: write # to be able to publish a GitHub release - issues: write # to be able to comment on released issues - pull-requests: write # to be able to comment on released pull requests - id-token: write # to enable use of OIDC for npm provenance + contents: write + issues: write + pull-requests: write + id-token: write steps: - name: Create Release uses: https://git.kjan.de/actions/semantic-release@main -- 2.45.3 From fa30fc330507e8342026b4c34334b915a4002012 Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 09:00:18 +0100 Subject: [PATCH 021/368] Protect homepage with keycloak --- frontend/src/app/app.routes.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index ce6d45a..2dcf135 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,13 +1,17 @@ -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; import {LandingPageComponent} from "./landing-page/landing-page.component"; import {HomepageComponent} from "./homepage/homepage/homepage.component"; +import {authGuard} from "./auth.guard"; export const routes: Routes = [ { - path: 'home', - component: HomepageComponent + path: '', + component: LandingPageComponent, + }, + { + path: 'home', + component: HomepageComponent, + canActivate: [authGuard], }, - - { path: '', component: LandingPageComponent } ]; -- 2.45.3 From f10de66c12ba017cd79e71c6377f613b4a1b77fc Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 09:22:20 +0100 Subject: [PATCH 022/368] Refactor auth guard --- frontend/src/app/auth.guard.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts index dd78a48..0cd16be 100644 --- a/frontend/src/app/auth.guard.ts +++ b/frontend/src/app/auth.guard.ts @@ -1,16 +1,15 @@ -import { CanActivateFn } from '@angular/router'; -import { inject } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import {CanActivateFn} from '@angular/router'; +import {inject} from '@angular/core'; +import {KeycloakService} from 'keycloak-angular'; -export const authGuard: CanActivateFn = async (route, state) => { +export const authGuard: CanActivateFn = async () => { const keycloakService = inject(KeycloakService); - const isLoggedIn = keycloakService.isLoggedIn(); if (isLoggedIn) { return true; - } else { - keycloakService.login(); - return false; } + + keycloakService.login(); + return false; }; -- 2.45.3 From 53f21a220f6bc75ab15535ecb7a05ff6b2fc8759 Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 09:27:41 +0100 Subject: [PATCH 023/368] Add logout and login functionality --- frontend/src/app/landing-page/landing-page.component.html | 1 + frontend/src/app/landing-page/landing-page.component.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html index e69de29..06f5418 100644 --- a/frontend/src/app/landing-page/landing-page.component.html +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index ed8aba1..7ea8521 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import {Component, inject} from '@angular/core'; +import {KeycloakService} from "keycloak-angular"; @Component({ selector: 'app-landing-page', @@ -7,5 +8,9 @@ import { Component } from '@angular/core'; templateUrl: './landing-page.component.html', }) export class LandingPageComponent { + private keycloakService: KeycloakService = inject(KeycloakService); + login() { + this.keycloakService.login(); + } } -- 2.45.3 From dc993b287954a6e35e918b99eacf49eec9d1e00b Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 09:38:31 +0100 Subject: [PATCH 024/368] Add alerts --- frontend/src/app/landing-page/landing-page.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index 7ea8521..56b9183 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -11,6 +11,10 @@ export class LandingPageComponent { private keycloakService: KeycloakService = inject(KeycloakService); login() { - this.keycloakService.login(); + this.keycloakService.login() + .catch(error => { + alert("Error: Unable to redirect to login page. Please try again later."); + console.error('Error redirecting to login:', error); + }); } } -- 2.45.3 From c3f9ba7bcada69d2bb072ddfa16cb11529333ce9 Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 09:41:05 +0100 Subject: [PATCH 025/368] Add nested route config for authentication --- frontend/src/app/app.routes.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 2dcf135..97623db 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,9 +9,14 @@ export const routes: Routes = [ component: LandingPageComponent, }, { - path: 'home', - component: HomepageComponent, + path: '**', canActivate: [authGuard], - }, + children: [ + { + path: 'home', + component: HomepageComponent, + }, + ] + } ]; -- 2.45.3 From f6bcc1be11bc4b1de34d9a6e722a98c00d8e6b9b Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 10:46:48 +0100 Subject: [PATCH 026/368] Prettier and rebase --- frontend/src/app/app.config.ts | 6 +++++- frontend/src/app/app.routes.ts | 13 ++++++------- frontend/src/app/auth.guard.ts | 6 +++--- .../app/homepage/homepage/homepage.component.html | 9 ++------- .../src/app/homepage/homepage/homepage.component.ts | 9 +++++++-- .../app/landing-page/landing-page.component.html | 2 +- .../src/app/landing-page/landing-page.component.ts | 10 +++------- frontend/src/styles.css | 8 +++----- 8 files changed, 30 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 6fd038e..e7761ba 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,4 +1,8 @@ -import { APP_INITIALIZER, ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { + APP_INITIALIZER, + ApplicationConfig, + provideExperimentalZonelessChangeDetection, +} from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 97623db..132b0da 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,7 +1,7 @@ -import {Routes} from '@angular/router'; -import {LandingPageComponent} from "./landing-page/landing-page.component"; -import {HomepageComponent} from "./homepage/homepage/homepage.component"; -import {authGuard} from "./auth.guard"; +import { Routes } from '@angular/router'; +import { LandingPageComponent } from './landing-page/landing-page.component'; +import { HomepageComponent } from './homepage/homepage/homepage.component'; +import { authGuard } from './auth.guard'; export const routes: Routes = [ { @@ -16,7 +16,6 @@ export const routes: Routes = [ path: 'home', component: HomepageComponent, }, - ] - } + ], + }, ]; - diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts index 0cd16be..cb2cbe9 100644 --- a/frontend/src/app/auth.guard.ts +++ b/frontend/src/app/auth.guard.ts @@ -1,6 +1,6 @@ -import {CanActivateFn} from '@angular/router'; -import {inject} from '@angular/core'; -import {KeycloakService} from 'keycloak-angular'; +import { CanActivateFn } from '@angular/router'; +import { inject } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; export const authGuard: CanActivateFn = async () => { const keycloakService = inject(KeycloakService); diff --git a/frontend/src/app/homepage/homepage/homepage.component.html b/frontend/src/app/homepage/homepage/homepage.component.html index 6377831..6a710c5 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.html +++ b/frontend/src/app/homepage/homepage/homepage.component.html @@ -1,16 +1,11 @@ - diff --git a/frontend/src/app/homepage/homepage/homepage.component.ts b/frontend/src/app/homepage/homepage/homepage.component.ts index e0a1147..1f01382 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.ts +++ b/frontend/src/app/homepage/homepage/homepage.component.ts @@ -1,12 +1,17 @@ -import {ChangeDetectionStrategy, Component} from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; @Component({ selector: 'app-homepage', standalone: true, imports: [], templateUrl: './homepage.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class HomepageComponent { + private keycloakService: KeycloakService = inject(KeycloakService); + logout() { + this.keycloakService.logout(); + } } diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html index 06f5418..f1cfa47 100644 --- a/frontend/src/app/landing-page/landing-page.component.html +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -1 +1 @@ - + diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index 56b9183..ff25911 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -1,5 +1,5 @@ -import {Component, inject} from '@angular/core'; -import {KeycloakService} from "keycloak-angular"; +import { Component, inject } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; @Component({ selector: 'app-landing-page', @@ -11,10 +11,6 @@ export class LandingPageComponent { private keycloakService: KeycloakService = inject(KeycloakService); login() { - this.keycloakService.login() - .catch(error => { - alert("Error: Unable to redirect to login page. Please try again later."); - console.error('Error redirecting to login:', error); - }); + this.keycloakService.login(); } } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ecb1a02..22329e3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,10 +1,8 @@ -@import "tailwindcss"; - +@import 'tailwindcss'; .btn-primary { - @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20 + @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20; } .btn-secondary { - @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-white/10 text-white hover:bg-white/20 + @apply px-4 py-2 cursor-pointer relative font-bold rounded-lg transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform bg-white/10 text-white hover:bg-white/20; } - -- 2.45.3 From 4b9574ac965b6e428c1575359562e9d68d7f190b Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 10:54:47 +0100 Subject: [PATCH 027/368] fix: fix routing --- frontend/src/app/app.routes.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 132b0da..2d53d28 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,13 +9,8 @@ export const routes: Routes = [ component: LandingPageComponent, }, { - path: '**', + path: 'home', + component: HomepageComponent, canActivate: [authGuard], - children: [ - { - path: 'home', - component: HomepageComponent, - }, - ], - }, + } ]; -- 2.45.3 From 13769e0df5f536a45381789ae54c403fda964462 Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 10:56:21 +0100 Subject: [PATCH 028/368] fix: Translate login to german --- frontend/src/app/landing-page/landing-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html index f1cfa47..aa8bbd8 100644 --- a/frontend/src/app/landing-page/landing-page.component.html +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -1 +1 @@ - + -- 2.45.3 From 3113eee4149ff735bfb44b72a35c5951758b39bc Mon Sep 17 00:00:00 2001 From: Constantin Simonis Date: Wed, 12 Feb 2025 11:13:18 +0100 Subject: [PATCH 029/368] chore(deployment): add deployment for backend --- backend/.docker/Dockerfile | 32 +++++++++++++++++++ backend/.dockerignore | 2 ++ .../src/main/resources/application.properties | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 backend/.docker/Dockerfile create mode 100644 backend/.dockerignore diff --git a/backend/.docker/Dockerfile b/backend/.docker/Dockerfile new file mode 100644 index 0000000..1b2524c --- /dev/null +++ b/backend/.docker/Dockerfile @@ -0,0 +1,32 @@ +# First stage: Build the application +FROM gradle:jdk22 AS builder + +WORKDIR /app + +# Copy only Gradle wrapper and configuration files first (for caching efficiency) +COPY gradlew build.gradle.kts settings.gradle.kts ./ +COPY gradle gradle + +# Give execute permissions to Gradle wrapper +RUN chmod +x gradlew + +# Download dependencies first (improves caching) +RUN ./gradlew dependencies + +# Copy the rest of the project files +COPY src src + +# Build the application (skipping tests for faster build) +RUN ./gradlew clean build -x test + +# Second stage: Run the application +FROM openjdk:22-jdk-slim + +WORKDIR /app + +# Copy the built JAR from the first stage +COPY --from=builder /app/build/libs/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..f06dfad --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +.gradle +build \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 938ce25..8271ece 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.datasource.url=jdbc:postgresql://localhost:5432/postgresdb +spring.datasource.url=jdbc:postgresql://$DB_HOST}:5432/postgresdb spring.datasource.username=postgres_user spring.datasource.password=postgres_pass server.port=8080 -- 2.45.3 From 9778a1e6d5935926463e28c1602a8b5f16028882 Mon Sep 17 00:00:00 2001 From: Constantin Simonis Date: Wed, 12 Feb 2025 11:15:00 +0100 Subject: [PATCH 030/368] refactor: Refactor dockerfile --- backend/.docker/Dockerfile | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/.docker/Dockerfile b/backend/.docker/Dockerfile index 1b2524c..d6df4f7 100644 --- a/backend/.docker/Dockerfile +++ b/backend/.docker/Dockerfile @@ -1,32 +1,20 @@ -# First stage: Build the application FROM gradle:jdk22 AS builder - WORKDIR /app -# Copy only Gradle wrapper and configuration files first (for caching efficiency) COPY gradlew build.gradle.kts settings.gradle.kts ./ COPY gradle gradle -# Give execute permissions to Gradle wrapper RUN chmod +x gradlew - -# Download dependencies first (improves caching) RUN ./gradlew dependencies -# Copy the rest of the project files COPY src src -# Build the application (skipping tests for faster build) RUN ./gradlew clean build -x test -# Second stage: Run the application FROM openjdk:22-jdk-slim - WORKDIR /app -# Copy the built JAR from the first stage COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 - ENTRYPOINT ["java", "-jar", "app.jar"] -- 2.45.3 From fab3680c07ecf1c988783dfeda19770a04c6287f Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 11:15:27 +0100 Subject: [PATCH 031/368] feat: Add proper redirects to login and logout button --- .../homepage/homepage/homepage.component.ts | 8 ++++--- .../landing-page/landing-page.component.ts | 22 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/homepage/homepage/homepage.component.ts b/frontend/src/app/homepage/homepage/homepage.component.ts index 1f01382..2b2dda8 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.ts +++ b/frontend/src/app/homepage/homepage/homepage.component.ts @@ -1,5 +1,5 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {KeycloakService} from 'keycloak-angular'; @Component({ selector: 'app-homepage', @@ -12,6 +12,8 @@ export class HomepageComponent { private keycloakService: KeycloakService = inject(KeycloakService); logout() { - this.keycloakService.logout(); + const baseUrl = window.location.origin; + + this.keycloakService.logout(`${baseUrl}/`); } } diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index ff25911..eefe0d4 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -1,16 +1,18 @@ -import { Component, inject } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; +import {Component, inject} from '@angular/core'; +import {KeycloakService} from 'keycloak-angular'; @Component({ - selector: 'app-landing-page', - standalone: true, - imports: [], - templateUrl: './landing-page.component.html', + selector: 'app-landing-page', + standalone: true, + imports: [], + templateUrl: './landing-page.component.html', }) export class LandingPageComponent { - private keycloakService: KeycloakService = inject(KeycloakService); + private keycloakService: KeycloakService = inject(KeycloakService); - login() { - this.keycloakService.login(); - } + login() { + const baseUrl = window.location.origin; + + this.keycloakService.login({redirectUri: `${baseUrl}/home`}); + } } -- 2.45.3 From a958d9f6ac91430fcf1316e253d1a835b1b95646 Mon Sep 17 00:00:00 2001 From: Constantin Simonis Date: Wed, 12 Feb 2025 11:15:33 +0100 Subject: [PATCH 032/368] refactor: Whoops --- backend/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8271ece..713c7ff 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.datasource.url=jdbc:postgresql://$DB_HOST}:5432/postgresdb +spring.datasource.url=jdbc:postgresql://${DB_HOST}:5432/postgresdb spring.datasource.username=postgres_user spring.datasource.password=postgres_pass server.port=8080 -- 2.45.3 From c64152b99fad46c310a8eb184731c4db7b373ca8 Mon Sep 17 00:00:00 2001 From: Phan Huy Tran Date: Wed, 12 Feb 2025 11:16:29 +0100 Subject: [PATCH 033/368] style: format code with prettier --- frontend/src/app/app.routes.ts | 2 +- .../homepage/homepage/homepage.component.ts | 4 ++-- .../landing-page/landing-page.component.ts | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 2d53d28..02b958c 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -12,5 +12,5 @@ export const routes: Routes = [ path: 'home', component: HomepageComponent, canActivate: [authGuard], - } + }, ]; diff --git a/frontend/src/app/homepage/homepage/homepage.component.ts b/frontend/src/app/homepage/homepage/homepage.component.ts index 2b2dda8..ff894dc 100644 --- a/frontend/src/app/homepage/homepage/homepage.component.ts +++ b/frontend/src/app/homepage/homepage/homepage.component.ts @@ -1,5 +1,5 @@ -import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; -import {KeycloakService} from 'keycloak-angular'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; @Component({ selector: 'app-homepage', diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index eefe0d4..f7509cf 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -1,18 +1,18 @@ -import {Component, inject} from '@angular/core'; -import {KeycloakService} from 'keycloak-angular'; +import { Component, inject } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; @Component({ - selector: 'app-landing-page', - standalone: true, - imports: [], - templateUrl: './landing-page.component.html', + selector: 'app-landing-page', + standalone: true, + imports: [], + templateUrl: './landing-page.component.html', }) export class LandingPageComponent { - private keycloakService: KeycloakService = inject(KeycloakService); + private keycloakService: KeycloakService = inject(KeycloakService); - login() { - const baseUrl = window.location.origin; + login() { + const baseUrl = window.location.origin; - this.keycloakService.login({redirectUri: `${baseUrl}/home`}); - } + this.keycloakService.login({ redirectUri: `${baseUrl}/home` }); + } } -- 2.45.3 From 5e861ee34c26d6a9bba38e132044fe44398b3178 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:26:48 +0100 Subject: [PATCH 034/368] feat: add navbar component to the application --- frontend/src/app/app.component.html | 1 + frontend/src/app/app.component.ts | 4 +- .../components/navbar/navbar.component.html | 42 +++++++++++ .../components/navbar/navbar.component.scss | 34 +++++++++ .../components/navbar/navbar.component.ts | 18 +++++ frontend/src/styles.css | 71 ++++++++++++++++++- frontend/tailwind.config.js | 28 ++++++++ 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.html create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.scss create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 0680b43..6659729 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1 +1,2 @@ + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7acf29a..6b8cdf7 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -2,11 +2,11 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { KeycloakAngularModule } from 'keycloak-angular'; - +import { NavbarComponent } from './shared/components/navbar/navbar.component'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, KeycloakAngularModule], + imports: [CommonModule, RouterOutlet, KeycloakAngularModule, NavbarComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.css', diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html new file mode 100644 index 0000000..cd772bd --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.scss b/frontend/src/app/shared/components/navbar/navbar.component.scss new file mode 100644 index 0000000..1f11eda --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.scss @@ -0,0 +1,34 @@ +:host { + display: block; +} + +.text-gold { + color: #FFD700; +} + +.bg-gold { + background-color: #FFD700; + &:hover { + background-color: #FFC000; + } +} + +.hover\:bg-gold-dark:hover { + background-color: #FFC000; +} + +.hover\:text-gold:hover { + color: #FFD700; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.transition-colors { + transition: all 0.2s ease-in-out; +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts new file mode 100644 index 0000000..e4eb7c2 --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'], + standalone: true, + imports: [CommonModule, RouterModule] +}) +export class NavbarComponent { + isMenuOpen = false; + + toggleMenu() { + this.isMenuOpen = !this.isMenuOpen; + } +} \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d4b5078..c69e30e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1 +1,70 @@ -@import 'tailwindcss'; +@import "tailwindcss"; + +@theme { + --color-primary-900: rgb(26, 44, 56); + --color-primary-800: #1a1a1a; + --color-primary-600: #2d2d2d; + + --color-accent-blue: #3b82f6; + --color-accent-green: #22c55e; + + --color-gold: #ffd700; + --color-gold-light: #ffe44d; + + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; +} + +body { + @apply bg-primary-900 text-gray-100; +} +/* Buttons */ +.button { + @apply px-4 py-2 rounded font-medium transition-all duration-300; +} +/* Primary Button */ +.button-primary { + @apply bg-accent-blue text-gray-100 hover:bg-blue-600 active:bg-blue-700; +} +/* Secondary Button */ +.button-secondary { + @apply bg-gray-400 text-gray-100 hover:bg-gray-300 active:bg-gray-200; +} +/* Success Button */ +.button-success { + @apply bg-accent-green text-primary-900 hover:opacity-90 active:opacity-80; +} +/* Disabled Button */ +.button-disabled { + @apply bg-gray-200 text-gray-400 cursor-not-allowed; +} +/* Navigation */ +.nav-link { + @apply text-gray-100 hover:text-gold-light transition-colors duration-300; +} +.nav-link-active { + @apply text-gold; +} +/* Cards */ +.card { + @apply bg-primary-800 rounded-lg shadow-lg p-6; +} +/* Gradients */ +.gradient-primary { + @apply bg-gradient-to-r from-primary-800 to-primary-600; +} + + +/* Button States */ +.button:hover { + @apply shadow-lg; +} +.button:active { + @apply scale-95; +} +.button:focus { + @apply outline-none ring-2 ring-offset-2 ring-accent-blue; +} + \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 843fec3..87e76e4 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -5,6 +5,34 @@ module.exports = { ], theme: { extend: { + colors: { + // Core colors from the palette + 'primary': { + 900: '#000000', // #000000 + 800: '#05080a', // #05080a + 700: '#071d2a', // #071d2a + 600: '#0f212e', // #0f212e - dark navy + 500: '#1a2c38', // #1a2c38 + 400: '#213743', // #213743 + 300: '#2f4553', // #2f4553 + }, + 'accent': { + blue: '#1475e1', // Bright blue + green: '#00e701', // Bright green + }, + 'gray': { + 400: '#557086', // Mid gray + 300: '#b1bad3', // Light gray + 200: '#d5dceb', // Lighter gray + 100: '#ffffff', // White + }, + // Semantic colors + 'gold': { + DEFAULT: '#b1bad3', + light: '#d5dceb', + dark: '#557086', + }, + }, animation: { 'fadeIn': 'fadeIn 0.3s ease-out', 'backdropBlur': 'backdropBlur 0.4s ease-out', -- 2.45.3 From 84c06de2e45cf5d9c88e395568bd84a193390b20 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:28:59 +0100 Subject: [PATCH 035/368] chore: remove unused tailwind configuration file --- frontend/tailwind.config.js | 246 ------------------------------------ 1 file changed, 246 deletions(-) delete mode 100644 frontend/tailwind.config.js diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js deleted file mode 100644 index 87e76e4..0000000 --- a/frontend/tailwind.config.js +++ /dev/null @@ -1,246 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./src/**/*.{html,ts}", - ], - theme: { - extend: { - colors: { - // Core colors from the palette - 'primary': { - 900: '#000000', // #000000 - 800: '#05080a', // #05080a - 700: '#071d2a', // #071d2a - 600: '#0f212e', // #0f212e - dark navy - 500: '#1a2c38', // #1a2c38 - 400: '#213743', // #213743 - 300: '#2f4553', // #2f4553 - }, - 'accent': { - blue: '#1475e1', // Bright blue - green: '#00e701', // Bright green - }, - 'gray': { - 400: '#557086', // Mid gray - 300: '#b1bad3', // Light gray - 200: '#d5dceb', // Lighter gray - 100: '#ffffff', // White - }, - // Semantic colors - 'gold': { - DEFAULT: '#b1bad3', - light: '#d5dceb', - dark: '#557086', - }, - }, - 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'), - ], -} -- 2.45.3 From 8ce4bb1be4010be58017e0bc2a29942e6e3c0aa2 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:29:37 +0100 Subject: [PATCH 036/368] style: Clean up CSS comments and formatting --- frontend/src/styles.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index c69e30e..b8e4ecb 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -20,51 +20,51 @@ body { @apply bg-primary-900 text-gray-100; } -/* Buttons */ + .button { @apply px-4 py-2 rounded font-medium transition-all duration-300; } -/* Primary Button */ + .button-primary { @apply bg-accent-blue text-gray-100 hover:bg-blue-600 active:bg-blue-700; } -/* Secondary Button */ + .button-secondary { @apply bg-gray-400 text-gray-100 hover:bg-gray-300 active:bg-gray-200; } -/* Success Button */ + .button-success { @apply bg-accent-green text-primary-900 hover:opacity-90 active:opacity-80; } -/* Disabled Button */ + .button-disabled { @apply bg-gray-200 text-gray-400 cursor-not-allowed; } -/* Navigation */ + .nav-link { @apply text-gray-100 hover:text-gold-light transition-colors duration-300; } + .nav-link-active { @apply text-gold; } -/* Cards */ + .card { @apply bg-primary-800 rounded-lg shadow-lg p-6; } -/* Gradients */ + .gradient-primary { @apply bg-gradient-to-r from-primary-800 to-primary-600; } - -/* Button States */ .button:hover { @apply shadow-lg; } + .button:active { @apply scale-95; } + .button:focus { @apply outline-none ring-2 ring-offset-2 ring-accent-blue; -} - \ No newline at end of file +} \ No newline at end of file -- 2.45.3 From 4a43b32133eb87d1d3991f45064ab9a69dc353df Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:31:22 +0100 Subject: [PATCH 037/368] style: Update color values in styles.css --- frontend/src/styles.css | 60 +++++------------------------------------ 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b8e4ecb..136cbe7 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2,15 +2,15 @@ @theme { --color-primary-900: rgb(26, 44, 56); - --color-primary-800: #1a1a1a; - --color-primary-600: #2d2d2d; - + --color-primary-800: rgb(30, 48, 60); + --color-primary-600: rgb(34, 52, 64); + --color-accent-blue: #3b82f6; --color-accent-green: #22c55e; - + --color-gold: #ffd700; --color-gold-light: #ffe44d; - + --color-gray-100: #f3f4f6; --color-gray-200: #e5e7eb; --color-gray-300: #d1d5db; @@ -18,53 +18,5 @@ } body { - @apply bg-primary-900 text-gray-100; -} - -.button { - @apply px-4 py-2 rounded font-medium transition-all duration-300; -} - -.button-primary { - @apply bg-accent-blue text-gray-100 hover:bg-blue-600 active:bg-blue-700; -} - -.button-secondary { - @apply bg-gray-400 text-gray-100 hover:bg-gray-300 active:bg-gray-200; -} - -.button-success { - @apply bg-accent-green text-primary-900 hover:opacity-90 active:opacity-80; -} - -.button-disabled { - @apply bg-gray-200 text-gray-400 cursor-not-allowed; -} - -.nav-link { - @apply text-gray-100 hover:text-gold-light transition-colors duration-300; -} - -.nav-link-active { - @apply text-gold; -} - -.card { - @apply bg-primary-800 rounded-lg shadow-lg p-6; -} - -.gradient-primary { - @apply bg-gradient-to-r from-primary-800 to-primary-600; -} - -.button:hover { - @apply shadow-lg; -} - -.button:active { - @apply scale-95; -} - -.button:focus { - @apply outline-none ring-2 ring-offset-2 ring-accent-blue; + @apply bg-primary-800 text-gray-100; } \ No newline at end of file -- 2.45.3 From 0836830df2be3d367856adacec40cb3296b38187 Mon Sep 17 00:00:00 2001 From: Constantin Simonis Date: Wed, 12 Feb 2025 11:35:57 +0100 Subject: [PATCH 038/368] refactor: Add default value to db host --- backend/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 713c7ff..d668f7a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.datasource.url=jdbc:postgresql://${DB_HOST}:5432/postgresdb +spring.datasource.url=jdbc:postgresql://${DB_HOST:-localhost}:5432/postgresdb spring.datasource.username=postgres_user spring.datasource.password=postgres_pass server.port=8080 -- 2.45.3 From e270b8abe5379be569e75f16efca7e36c0170a86 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:11:35 +0100 Subject: [PATCH 039/368] ci: add CI workflow configuration file --- .gitea/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c32dab9 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + pull_request: + +jobs: + test-build: + name: test-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v4 + with: + path: | + ~/.npm + key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- + - run: bun install + - run: bun run build -- 2.45.3 From d1aa0222a1167929a8177a144d8e6c24d252f12a Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:13:45 +0100 Subject: [PATCH 040/368] ci: change runner to remote in CI configuration --- .gitea/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c32dab9..158e66a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: test-build: name: test-build - runs-on: ubuntu-latest + runs-on: remote steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 -- 2.45.3 From cb80b041a63f5e46c9228bec4978ea69ed062df1 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:16:48 +0100 Subject: [PATCH 041/368] ci: update CI workflow to use new container image --- .gitea/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 158e66a..6234d3a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,6 +7,8 @@ jobs: test-build: name: test-build runs-on: remote + container: + image: catthehacker/ubuntu:act-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 -- 2.45.3 From f51d32ba5f315d191f55e7f74c06786b2c7505f0 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:20:51 +0100 Subject: [PATCH 042/368] ci: add cd command to frontend in CI workflow --- .gitea/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 6234d3a..0f1e330 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -19,5 +19,6 @@ jobs: key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- + - run: cd frontend - run: bun install - run: bun run build -- 2.45.3 From 3e1e42fa31874572da031d96c7b07cdab3dca4f5 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:21:56 +0100 Subject: [PATCH 043/368] ci: update CI script for better readability --- .gitea/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0f1e330..4b95517 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- - - run: cd frontend - - run: bun install + - run: | + cd frontend + bun install - run: bun run build -- 2.45.3 From ac3c84106dbb27660a9d37da9c6e504341896fb5 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:23:38 +0100 Subject: [PATCH 044/368] ci: improve CI workflow with step names for clarity --- .gitea/workflows/ci.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4b95517..269ef51 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -10,16 +10,23 @@ jobs: container: image: catthehacker/ubuntu:act-latest steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - uses: actions/cache@v4 + - name: Checkout Code + uses: actions/checkout@v4 + - name: Install bun + uses: oven-sh/setup-bun@v2 + - name: Cache + uses: actions/cache@v4 with: path: | ~/.npm key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- - - run: | + - name: Install dependencies + run: | cd frontend bun install - - run: bun run build + - name: Test build + run: | + cd frontend + bun run build -- 2.45.3 From 343704959bc5e61b6d5454f1c47d04ec8e408b15 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:31:32 +0100 Subject: [PATCH 045/368] ci: update runner from remote to vps-4 in workflows --- .gitea/workflows/ci.yml | 2 +- .gitea/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 269ef51..760e3f4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: test-build: name: test-build - runs-on: remote + runs-on: vps-4 container: image: catthehacker/ubuntu:act-latest steps: diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 51e555c..fa827f3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -13,7 +13,7 @@ permissions: jobs: release: name: Release - runs-on: remote + runs-on: vps-4 permissions: contents: write issues: write -- 2.45.3 From dc1482c3206823a834a355f915c53446f8a0c6c3 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:33:32 +0100 Subject: [PATCH 046/368] ci: add prettier job to CI workflow --- .gitea/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 760e3f4..e537b78 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -4,6 +4,33 @@ on: pull_request: jobs: + prettier: + name: prettier + runs-on: vps-4 + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + - name: Install bun + uses: oven-sh/setup-bun@v2 + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.npm + key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- + - name: Install dependencies + run: | + cd frontend + bun install + - name: Run prettier + run: | + cd frontend + bun run format:check + test-build: name: test-build runs-on: vps-4 -- 2.45.3 From 2d955eed1afd621efe91fe229e8abc3d6abbb4dd Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 12 Feb 2025 11:36:45 +0100 Subject: [PATCH 047/368] ci: remove caching from CI workflow configuration --- .gitea/workflows/ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e537b78..eaead1c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,14 +14,6 @@ jobs: uses: actions/checkout@v4 - name: Install bun uses: oven-sh/setup-bun@v2 - - name: Cache - uses: actions/cache@v4 - with: - path: | - ~/.npm - key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} - restore-keys: | - ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- - name: Install dependencies run: | cd frontend @@ -41,14 +33,6 @@ jobs: uses: actions/checkout@v4 - name: Install bun uses: oven-sh/setup-bun@v2 - - name: Cache - uses: actions/cache@v4 - with: - path: | - ~/.npm - key: ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} - restore-keys: | - ${{ runner.os }}-bun-${{ hashFiles('**/package-lock.json') }}- - name: Install dependencies run: | cd frontend -- 2.45.3 From f7c98ec6fe47748f4de759c5c5cedca5294e4425 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:38:28 +0100 Subject: [PATCH 048/368] style(navbar): update navbar colors to use variables --- .../components/navbar/navbar.component.html | 12 +++++------ frontend/src/styles.css | 21 ++++++------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index cd772bd..61447ad 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -1,4 +1,4 @@ -
- + START PLAYING
@@ -157,17 +136,9 @@
- + + CLAIM YOUR €10,000 NOW +
@@ -190,25 +161,7 @@
-
-
- - - 🎰 {{ winner.name }} - {{ - winner.isVIP ? '(VIP)' : '' - }} - turned €{{ winner.betAmount }} into - €{{ winner.amount | number }} - ({{ winner.multiplier }}x) - - -
-
+
@@ -220,79 +173,12 @@
-
-
- -
-
-
-

{{ 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 }} - -
- -
-
-
-
-
+
@@ -396,17 +282,9 @@ > Maybe later - + + {{ popup.cta }} +
- + SPIN AGAIN + diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index 4aceb54..1f0ff86 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -22,10 +22,14 @@ import { WinnerService } from '../services/winner.service'; import { JackpotService } from '../services/jackpot.service'; import { AnimationService } from '../services/animation.service'; +import { AnimatedButtonComponent } from '../shared/components/animated-button/animated-button.component'; +import { WinnerTickerComponent } from '../shared/components/winner-ticker/winner-ticker.component'; +import { GameCardComponent } from '../shared/components/game-card/game-card.component'; + @Component({ selector: 'app-landing', standalone: true, - imports: [CommonModule], + imports: [CommonModule, AnimatedButtonComponent, WinnerTickerComponent, GameCardComponent], templateUrl: './landing.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts new file mode 100644 index 0000000..959ef17 --- /dev/null +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AnimationService } from '../../../services/animation.service'; + +@Component({ + selector: 'app-animated-button', + standalone: true, + imports: [CommonModule], + template: ` + + `, + styles: [], +}) +export class AnimatedButtonComponent { + @Input() variant: 'primary' | 'secondary' = 'primary'; + @Input() size: 'normal' | 'large' = 'normal'; + @Output() buttonClick = new EventEmitter(); + + constructor(private animationService: AnimationService) {} + + get buttonClass(): string { + const baseClass = + 'relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; + const variantClass = + this.variant === 'primary' + ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' + : 'bg-white/10 text-white hover:bg-white/20'; + + return `${baseClass} ${variantClass}`; + } + + handleClick(event: MouseEvent): void { + const elementRef = new ElementRef(event.currentTarget); + this.animationService.animateButtonClick(elementRef); + this.buttonClick.emit(event); + } +} diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts new file mode 100644 index 0000000..7c8778a --- /dev/null +++ b/frontend/src/app/shared/components/game-card/game-card.component.ts @@ -0,0 +1,98 @@ +import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Game } from '../../../services/game.service'; +import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; +import { AnimationService } from '../../../services/animation.service'; + +@Component({ + selector: 'app-game-card', + standalone: true, + imports: [CommonModule, AnimatedButtonComponent], + template: ` +
+
+ +
+
+
+

{{ 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 }} + +
+ PLAY NOW +
+
+
+
+
+ `, + styles: [], +}) +export class GameCardComponent { + @Input() game!: Game; + @Output() play = new EventEmitter(); + + constructor(private animationService: AnimationService) {} + + onHover(event: MouseEvent): void { + const element = event.currentTarget as HTMLElement; + const elementRef = new ElementRef(element); + this.animationService.animateFloat(elementRef); + } + + onPlay(): void { + this.play.emit(); + } +} diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts new file mode 100644 index 0000000..c22e661 --- /dev/null +++ b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { default as autoAnimate } from '@formkit/auto-animate'; +import { Winner } from '../../../services/winner.service'; + +@Component({ + selector: 'app-winner-ticker', + standalone: true, + imports: [CommonModule], + template: ` +
+ + 🎰 + + {{ winner.name }} + {{ winner.isVIP ? '(VIP)' : '' }} + won €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+ `, + styles: [], +}) +export class WinnerTickerComponent implements AfterViewInit { + @Input() winners: Winner[] = []; + @ViewChild('tickerContainer') tickerContainer!: ElementRef; + + ngAfterViewInit(): void { + autoAnimate(this.tickerContainer.nativeElement); + } +} -- 2.45.3 From eede85295e4496f03c3d268eeb30c272415a6a2d Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:43:04 +0100 Subject: [PATCH 091/368] refactor: improve code structure and add change detection --- .../src/app/landing/landing.component.html | 4 ++-- frontend/src/app/landing/landing.component.ts | 18 ++++++++++++------ .../animated-button.component.ts | 10 +++++++++- .../game-card/game-card.component.ts | 10 +++++++++- .../winner-ticker/winner-ticker.component.ts | 10 +++++++++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index aeaa65c..8bb0c62 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -93,9 +93,9 @@
- €{{ currentJackpot$ | async | number }} + €{{ (currentJackpot$ | async) ?? 0 | number }} ↑
diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index 1f0ff86..e2b0afb 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -112,14 +112,20 @@ export class LandingComponent implements OnInit, OnDestroy, AfterViewInit { 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); + const gameCards = this.gamesGrid?.nativeElement.querySelectorAll('.game-card'); + if (gameCards) { + gameCards.forEach((card: HTMLElement, index: number) => { + this.animationService.animateEntrance(new ElementRef(card), 0.1 * index); + }); + } + this.animationService.animateOnScroll(this.gamesGrid, 'slideUp'); + this.currentJackpot$.pipe(takeUntil(this.destroy$)).subscribe((value) => { - this.animationService.animateJackpotCounter(this.jackpotCounter, value - 1000, value); + if (this.jackpotCounter && value) { + const prevValue = value - Math.floor(Math.random() * 1000 + 500); + this.animationService.animateJackpotCounter(this.jackpotCounter, prevValue, value); + } }); } diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts index 959ef17..1ce932d 100644 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + ElementRef, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { AnimationService } from '../../../services/animation.service'; @@ -21,6 +28,7 @@ import { AnimationService } from '../../../services/animation.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AnimatedButtonComponent { @Input() variant: 'primary' | 'secondary' = 'primary'; diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts index 7c8778a..78dacac 100644 --- a/frontend/src/app/shared/components/game-card/game-card.component.ts +++ b/frontend/src/app/shared/components/game-card/game-card.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, Output, EventEmitter, ElementRef } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + ElementRef, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { Game } from '../../../services/game.service'; import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; @@ -79,6 +86,7 @@ import { AnimationService } from '../../../services/animation.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class GameCardComponent { @Input() game!: Game; diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts index c22e661..7e0c733 100644 --- a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts +++ b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { + Component, + Input, + ViewChild, + ElementRef, + AfterViewInit, + ChangeDetectionStrategy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { default as autoAnimate } from '@formkit/auto-animate'; import { Winner } from '../../../services/winner.service'; @@ -21,6 +28,7 @@ import { Winner } from '../../../services/winner.service'; `, styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WinnerTickerComponent implements AfterViewInit { @Input() winners: Winner[] = []; -- 2.45.3 From 5e84afba90e7bbdaac56d7ceea9ad0cb71e25126 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:45:36 +0100 Subject: [PATCH 092/368] refactor: remove unused imports and clear image URLs --- frontend/src/app/landing/landing.component.ts | 1 - frontend/src/app/services/game.service.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/landing/landing.component.ts b/frontend/src/app/landing/landing.component.ts index e2b0afb..cd5be3a 100644 --- a/frontend/src/app/landing/landing.component.ts +++ b/frontend/src/app/landing/landing.component.ts @@ -14,7 +14,6 @@ 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'; diff --git a/frontend/src/app/services/game.service.ts b/frontend/src/app/services/game.service.ts index 4f0509a..ee6ae08 100644 --- a/frontend/src/app/services/game.service.ts +++ b/frontend/src/app/services/game.service.ts @@ -28,7 +28,7 @@ export class GameService { id: 'mega-fortune', name: 'Mega Fortune Dreams', description: 'πŸ”₯ Progressive Jackpot at €1.2M - Must Drop Today!', - imageUrl: 'assets/games/mega-fortune.jpg', + imageUrl: '', minBet: 0.2, maxBet: 100, rtp: 96.5, @@ -45,7 +45,7 @@ export class GameService { id: 'lightning-roulette', name: 'Lightning Roulette', description: '⚑️ 500x Multipliers Active - Hot Streak!', - imageUrl: 'assets/games/lightning-roulette.jpg', + imageUrl: '', minBet: 1, maxBet: 500, rtp: 97.1, -- 2.45.3 From 7106ebd0efdb1952a7bc354d5a595b4d5c78a413 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:46:44 +0100 Subject: [PATCH 093/368] style(animated-button): add cursor-pointer to button class --- .../components/animated-button/animated-button.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts index 1ce932d..85c433e 100644 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ b/frontend/src/app/shared/components/animated-button/animated-button.component.ts @@ -39,7 +39,7 @@ export class AnimatedButtonComponent { get buttonClass(): string { const baseClass = - 'relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; + 'cursor-pointer relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; const variantClass = this.variant === 'primary' ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' -- 2.45.3 From ff0e03003aa50b31eafa3e35a4c407e38ae55e49 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:52:52 +0100 Subject: [PATCH 094/368] style(landing): remove instant withdrawals section and update styles --- .../src/app/landing/landing.component.html | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index 8bb0c62..fb8924f 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -142,10 +142,6 @@
-
- βœ“ - Instant Withdrawals -
βœ“ 24/7 VIP Support @@ -159,7 +155,7 @@
@@ -190,13 +186,6 @@

Elite VIP Status

Up to €50,000 monthly rewards

-
-
⚑️
-

Instant Cashouts

-

Get paid in 5 minutes!

-
@@ -211,9 +200,15 @@

99.9% Win Rate*

Highest odds in the industry!

+
+
πŸš€
+

Exclusive Offers

+

Unlock special perks and rewards!

+
-
*Terms and conditions apply. Guaranteed winnings based on maximum bonus utilization. Win rate calculated on minimum bets. Withdrawal restrictions and wagering requirements apply. -- 2.45.3 From ea26f5dd80d6f8abbab67a9cbae81bfb193667fa Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 5 Feb 2025 14:33:31 +0100 Subject: [PATCH 095/368] feat(landing): add landing component with animations and services --- .../src/app/landing/landing.component.html | 189 +++++++++++++++--- frontend/src/app/services/game.service.ts | 4 +- 2 files changed, 162 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html index fb8924f..5922ccc 100644 --- a/frontend/src/app/landing/landing.component.html +++ b/frontend/src/app/landing/landing.component.html @@ -6,7 +6,20 @@ [class.py-1.5]="!isScrolled" >
- +
+ + 🎰 + + {{ winner.name }} + {{ winner.isVIP ? '(VIP)' : '' }} + won €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
@@ -47,7 +60,15 @@ (78.9% Win Rate) - START PLAYING + @@ -93,9 +114,9 @@
- €{{ (currentJackpot$ | async) ?? 0 | number }} + €{{ currentJackpot$ | async | number }} ↑
@@ -136,12 +157,24 @@
- - CLAIM YOUR €10,000 NOW - +
+
+ βœ“ + Instant Withdrawals +
βœ“ 24/7 VIP Support @@ -155,9 +188,27 @@
- +
+
+ + + 🎰 {{ winner.name }} + {{ + winner.isVIP ? '(VIP)' : '' + }} + turned €{{ winner.betAmount }} into + €{{ winner.amount | number }} + ({{ winner.multiplier }}x) + + +
+
@@ -169,12 +220,79 @@
- - +
+ +
+
+
+

{{ 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 }} + +
+ +
+
+
+
+
@@ -186,6 +304,13 @@

Elite VIP Status

Up to €50,000 monthly rewards

+
+
⚑️
+

Instant Cashouts

+

Get paid in 5 minutes!

+
@@ -200,15 +325,9 @@

99.9% Win Rate*

Highest odds in the industry!

-
-
πŸš€
-

Exclusive Offers

-

Unlock special perks and rewards!

-
+
*Terms and conditions apply. Guaranteed winnings based on maximum bonus utilization. Win rate calculated on minimum bets. Withdrawal restrictions and wagering requirements apply. @@ -277,9 +396,17 @@ > Maybe later - - {{ popup.cta }} - +
- - SPIN AGAIN - + SPIN AGAIN +
+ diff --git a/frontend/src/app/services/game.service.ts b/frontend/src/app/services/game.service.ts index ee6ae08..4f0509a 100644 --- a/frontend/src/app/services/game.service.ts +++ b/frontend/src/app/services/game.service.ts @@ -28,7 +28,7 @@ export class GameService { id: 'mega-fortune', name: 'Mega Fortune Dreams', description: 'πŸ”₯ Progressive Jackpot at €1.2M - Must Drop Today!', - imageUrl: '', + imageUrl: 'assets/games/mega-fortune.jpg', minBet: 0.2, maxBet: 100, rtp: 96.5, @@ -45,7 +45,7 @@ export class GameService { id: 'lightning-roulette', name: 'Lightning Roulette', description: '⚑️ 500x Multipliers Active - Hot Streak!', - imageUrl: '', + imageUrl: 'assets/games/lightning-roulette.jpg', minBet: 1, maxBet: 500, rtp: 97.1, -- 2.45.3 From e6f054f10eb2328c3c111d14ddd6aa68783c34de Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 10:39:49 +0100 Subject: [PATCH 096/368] refactor: rename landing component and remove unused files --- .../src/app/landing/landing.component.html | 481 ------------------ frontend/src/app/landing/landing.component.ts | 262 ---------- .../src/app/services/animation.service.ts | 380 -------------- frontend/src/app/services/game.service.ts | 104 ---- frontend/src/app/services/jackpot.service.ts | 130 ----- frontend/src/app/services/popup.service.ts | 96 ---- frontend/src/app/services/winner.service.ts | 120 ----- .../animated-button.component.ts | 56 -- .../game-card/game-card.component.ts | 106 ---- .../winner-ticker/winner-ticker.component.ts | 40 -- 10 files changed, 1775 deletions(-) delete mode 100644 frontend/src/app/landing/landing.component.html delete mode 100644 frontend/src/app/landing/landing.component.ts delete mode 100644 frontend/src/app/services/animation.service.ts delete mode 100644 frontend/src/app/services/game.service.ts delete mode 100644 frontend/src/app/services/jackpot.service.ts delete mode 100644 frontend/src/app/services/popup.service.ts delete mode 100644 frontend/src/app/services/winner.service.ts delete mode 100644 frontend/src/app/shared/components/animated-button/animated-button.component.ts delete mode 100644 frontend/src/app/shared/components/game-card/game-card.component.ts delete mode 100644 frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts diff --git a/frontend/src/app/landing/landing.component.html b/frontend/src/app/landing/landing.component.html deleted file mode 100644 index 5922ccc..0000000 --- a/frontend/src/app/landing/landing.component.html +++ /dev/null @@ -1,481 +0,0 @@ -
-
-
-
-
- - 🎰 - - {{ 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 deleted file mode 100644 index cd5be3a..0000000 --- a/frontend/src/app/landing/landing.component.ts +++ /dev/null @@ -1,262 +0,0 @@ -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 { 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'; - -import { AnimatedButtonComponent } from '../shared/components/animated-button/animated-button.component'; -import { WinnerTickerComponent } from '../shared/components/winner-ticker/winner-ticker.component'; -import { GameCardComponent } from '../shared/components/game-card/game-card.component'; - -@Component({ - selector: 'app-landing', - standalone: true, - imports: [CommonModule, AnimatedButtonComponent, WinnerTickerComponent, GameCardComponent], - 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'); - if (gameCards) { - gameCards.forEach((card: HTMLElement, index: number) => { - this.animationService.animateEntrance(new ElementRef(card), 0.1 * index); - }); - } - - this.animationService.animateOnScroll(this.gamesGrid, 'slideUp'); - - this.currentJackpot$.pipe(takeUntil(this.destroy$)).subscribe((value) => { - if (this.jackpotCounter && value) { - const prevValue = value - Math.floor(Math.random() * 1000 + 500); - this.animationService.animateJackpotCounter(this.jackpotCounter, prevValue, 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 deleted file mode 100644 index 34fe89a..0000000 --- a/frontend/src/app/services/animation.service.ts +++ /dev/null @@ -1,380 +0,0 @@ -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 deleted file mode 100644 index 4f0509a..0000000 --- a/frontend/src/app/services/game.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index d13928d..0000000 --- a/frontend/src/app/services/jackpot.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 2e8ec99..0000000 --- a/frontend/src/app/services/popup.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index 155ba3b..0000000 --- a/frontend/src/app/services/winner.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -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/app/shared/components/animated-button/animated-button.component.ts b/frontend/src/app/shared/components/animated-button/animated-button.component.ts deleted file mode 100644 index 85c433e..0000000 --- a/frontend/src/app/shared/components/animated-button/animated-button.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ElementRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AnimationService } from '../../../services/animation.service'; - -@Component({ - selector: 'app-animated-button', - standalone: true, - imports: [CommonModule], - template: ` - - `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AnimatedButtonComponent { - @Input() variant: 'primary' | 'secondary' = 'primary'; - @Input() size: 'normal' | 'large' = 'normal'; - @Output() buttonClick = new EventEmitter(); - - constructor(private animationService: AnimationService) {} - - get buttonClass(): string { - const baseClass = - 'cursor-pointer relative group font-bold rounded-full transition-all duration-300 ease-out transform-gpu hover:scale-105 will-change-transform'; - const variantClass = - this.variant === 'primary' - ? 'bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:shadow-xl hover:shadow-emerald-500/20' - : 'bg-white/10 text-white hover:bg-white/20'; - - return `${baseClass} ${variantClass}`; - } - - handleClick(event: MouseEvent): void { - const elementRef = new ElementRef(event.currentTarget); - this.animationService.animateButtonClick(elementRef); - this.buttonClick.emit(event); - } -} diff --git a/frontend/src/app/shared/components/game-card/game-card.component.ts b/frontend/src/app/shared/components/game-card/game-card.component.ts deleted file mode 100644 index 78dacac..0000000 --- a/frontend/src/app/shared/components/game-card/game-card.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ElementRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Game } from '../../../services/game.service'; -import { AnimatedButtonComponent } from '../animated-button/animated-button.component'; -import { AnimationService } from '../../../services/animation.service'; - -@Component({ - selector: 'app-game-card', - standalone: true, - imports: [CommonModule, AnimatedButtonComponent], - template: ` -
-
- -
-
-
-

{{ 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 }} - -
- PLAY NOW -
-
-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class GameCardComponent { - @Input() game!: Game; - @Output() play = new EventEmitter(); - - constructor(private animationService: AnimationService) {} - - onHover(event: MouseEvent): void { - const element = event.currentTarget as HTMLElement; - const elementRef = new ElementRef(element); - this.animationService.animateFloat(elementRef); - } - - onPlay(): void { - this.play.emit(); - } -} diff --git a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts b/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts deleted file mode 100644 index 7e0c733..0000000 --- a/frontend/src/app/shared/components/winner-ticker/winner-ticker.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Component, - Input, - ViewChild, - ElementRef, - AfterViewInit, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { default as autoAnimate } from '@formkit/auto-animate'; -import { Winner } from '../../../services/winner.service'; - -@Component({ - selector: 'app-winner-ticker', - standalone: true, - imports: [CommonModule], - template: ` -
- - 🎰 - - {{ winner.name }} - {{ winner.isVIP ? '(VIP)' : '' }} - won €{{ winner.amount | number }} - ({{ winner.multiplier }}x) - - -
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class WinnerTickerComponent implements AfterViewInit { - @Input() winners: Winner[] = []; - @ViewChild('tickerContainer') tickerContainer!: ElementRef; - - ngAfterViewInit(): void { - autoAnimate(this.tickerContainer.nativeElement); - } -} -- 2.45.3 From 61add61113ed2c2c16639a913217e39335168bc9 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:26:48 +0100 Subject: [PATCH 097/368] feat: add navbar component to the application --- frontend/src/app/app.component.html | 1 + frontend/src/app/app.component.ts | 4 +- .../components/navbar/navbar.component.html | 42 +++++++++++++++++++ .../components/navbar/navbar.component.scss | 34 +++++++++++++++ .../components/navbar/navbar.component.ts | 18 ++++++++ frontend/tailwind.config.js | 28 +++++++++++++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.html create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.scss create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 0680b43..6659729 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1 +1,2 @@ + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7dea888..0a852aa 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -2,11 +2,11 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { KeycloakAngularModule } from 'keycloak-angular'; - +import { NavbarComponent } from './shared/components/navbar/navbar.component'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, KeycloakAngularModule], + imports: [CommonModule, RouterOutlet, KeycloakAngularModule, NavbarComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.css', diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html new file mode 100644 index 0000000..cd772bd --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.scss b/frontend/src/app/shared/components/navbar/navbar.component.scss new file mode 100644 index 0000000..1f11eda --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.scss @@ -0,0 +1,34 @@ +:host { + display: block; +} + +.text-gold { + color: #FFD700; +} + +.bg-gold { + background-color: #FFD700; + &:hover { + background-color: #FFC000; + } +} + +.hover\:bg-gold-dark:hover { + background-color: #FFC000; +} + +.hover\:text-gold:hover { + color: #FFD700; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.transition-colors { + transition: all 0.2s ease-in-out; +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts new file mode 100644 index 0000000..e4eb7c2 --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'], + standalone: true, + imports: [CommonModule, RouterModule] +}) +export class NavbarComponent { + isMenuOpen = false; + + toggleMenu() { + this.isMenuOpen = !this.isMenuOpen; + } +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 843fec3..87e76e4 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -5,6 +5,34 @@ module.exports = { ], theme: { extend: { + colors: { + // Core colors from the palette + 'primary': { + 900: '#000000', // #000000 + 800: '#05080a', // #05080a + 700: '#071d2a', // #071d2a + 600: '#0f212e', // #0f212e - dark navy + 500: '#1a2c38', // #1a2c38 + 400: '#213743', // #213743 + 300: '#2f4553', // #2f4553 + }, + 'accent': { + blue: '#1475e1', // Bright blue + green: '#00e701', // Bright green + }, + 'gray': { + 400: '#557086', // Mid gray + 300: '#b1bad3', // Light gray + 200: '#d5dceb', // Lighter gray + 100: '#ffffff', // White + }, + // Semantic colors + 'gold': { + DEFAULT: '#b1bad3', + light: '#d5dceb', + dark: '#557086', + }, + }, animation: { 'fadeIn': 'fadeIn 0.3s ease-out', 'backdropBlur': 'backdropBlur 0.4s ease-out', -- 2.45.3 From 21fa5a21e2620ec282efa9152c06d2ee445526d9 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:28:59 +0100 Subject: [PATCH 098/368] chore: remove unused tailwind configuration file --- frontend/tailwind.config.js | 246 ------------------------------------ 1 file changed, 246 deletions(-) delete mode 100644 frontend/tailwind.config.js diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js deleted file mode 100644 index 87e76e4..0000000 --- a/frontend/tailwind.config.js +++ /dev/null @@ -1,246 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./src/**/*.{html,ts}", - ], - theme: { - extend: { - colors: { - // Core colors from the palette - 'primary': { - 900: '#000000', // #000000 - 800: '#05080a', // #05080a - 700: '#071d2a', // #071d2a - 600: '#0f212e', // #0f212e - dark navy - 500: '#1a2c38', // #1a2c38 - 400: '#213743', // #213743 - 300: '#2f4553', // #2f4553 - }, - 'accent': { - blue: '#1475e1', // Bright blue - green: '#00e701', // Bright green - }, - 'gray': { - 400: '#557086', // Mid gray - 300: '#b1bad3', // Light gray - 200: '#d5dceb', // Lighter gray - 100: '#ffffff', // White - }, - // Semantic colors - 'gold': { - DEFAULT: '#b1bad3', - light: '#d5dceb', - dark: '#557086', - }, - }, - 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'), - ], -} -- 2.45.3 From 70044fc3c726b929c77f3e27378b80108d51c2fc Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 11:38:28 +0100 Subject: [PATCH 099/368] style(navbar): update navbar colors to use variables --- .../shared/components/navbar/navbar.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index cd772bd..61447ad 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -1,4 +1,4 @@ - diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index 338201c..43053a3 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common'; selector: 'app-navbar', templateUrl: './navbar.component.html', standalone: true, - imports: [CommonModule, RouterModule] + imports: [CommonModule, RouterModule], }) export class NavbarComponent { isMenuOpen = false; @@ -14,4 +14,4 @@ export class NavbarComponent { toggleMenu() { this.isMenuOpen = !this.isMenuOpen; } -} \ No newline at end of file +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 12c08a1..eacf745 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -17,3 +17,4 @@ body { --mdc-dialog-supporting-text-color: #9ca3af !important; --mdc-dialog-container-shape: 6px !important; } + -- 2.45.3 From e5bd173be981e05c9f645a8427ef3791a4f7cc91 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 12:51:55 +0100 Subject: [PATCH 103/368] feat(navbar): implement login/logout buttons based on state --- .../app/feature/landing/landing.component.ts | 1 + .../landing-page/landing-page.component.html | 1 + .../components/footer/footer.component.ts | 5 +-- .../components/navbar/navbar.component.html | 42 ++++++++++++++----- .../components/navbar/navbar.component.ts | 24 +++++++++-- 5 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 frontend/src/app/landing-page/landing-page.component.html diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index d2e28c0..417ac38 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -19,3 +19,4 @@ export class LandingComponent { this.keycloakService.login({ redirectUri: `${baseUrl}/home` }); } } +export class LandingPageComponent {} diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html new file mode 100644 index 0000000..aa8bbd8 --- /dev/null +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/shared/components/footer/footer.component.ts b/frontend/src/app/shared/components/footer/footer.component.ts index 006a23b..1c3b309 100644 --- a/frontend/src/app/shared/components/footer/footer.component.ts +++ b/frontend/src/app/shared/components/footer/footer.component.ts @@ -1,5 +1,4 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faMoneyBillTransfer, faCreditCard, faWallet } from '@fortawesome/free-solid-svg-icons'; import { faPaypal, faGooglePay, faApplePay } from '@fortawesome/free-brands-svg-icons'; @@ -9,11 +8,11 @@ import { faPaypal, faGooglePay, faApplePay } from '@fortawesome/free-brands-svg- standalone: true, templateUrl: './footer.component.html', imports: [FontAwesomeModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FooterComponent { currentYear: number = new Date().getFullYear(); - // Payment method icons faPaypal = faPaypal; faCreditCard = faCreditCard; faMoneyBillTransfer = faMoneyBillTransfer; diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index fa990e6..b0abbbb 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -15,11 +15,22 @@
@@ -67,11 +78,22 @@ >Games
- + @if (!isLoggedIn) { + + } + @if (isLoggedIn) { + + }
diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index 43053a3..eb88f52 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -1,15 +1,33 @@ -import { Component } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import { KeycloakService } from 'keycloak-angular'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', standalone: true, - imports: [CommonModule, RouterModule], + imports: [RouterModule, AsyncPipe], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavbarComponent { isMenuOpen = false; + private keycloakService: KeycloakService = inject(KeycloakService); + + isLoggedIn = this.keycloakService.isLoggedIn(); + + login() { + try { + const baseUrl = window.location.origin; + this.keycloakService.login({ redirectUri: `${baseUrl}/home` }); + } catch (error) { + console.error('Login failed:', error); + } + } + + logout() { + this.keycloakService.logout(); + } toggleMenu() { this.isMenuOpen = !this.isMenuOpen; -- 2.45.3 From b2f4d9d0d85f063b9f604f1bef15b5100da27643 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 12 Feb 2025 14:06:19 +0100 Subject: [PATCH 104/368] feat: add navbar component to multiple pages --- frontend/src/app/app.component.html | 3 +-- frontend/src/app/feature/home/home.component.html | 12 +----------- frontend/src/app/feature/home/home.component.ts | 5 +++-- .../src/app/landing-page/landing-page.component.html | 1 + .../src/app/landing-page/landing-page.component.ts | 10 ++++++++++ 5 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 frontend/src/app/landing-page/landing-page.component.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 67fc2d4..828546c 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,7 +1,6 @@
-
-
+ \ No newline at end of file diff --git a/frontend/src/app/feature/home/home.component.html b/frontend/src/app/feature/home/home.component.html index b85ce29..27cec95 100644 --- a/frontend/src/app/feature/home/home.component.html +++ b/frontend/src/app/feature/home/home.component.html @@ -1,14 +1,4 @@ - +
diff --git a/frontend/src/app/feature/home/home.component.ts b/frontend/src/app/feature/home/home.component.ts index a4b2b8b..f179e2c 100644 --- a/frontend/src/app/feature/home/home.component.ts +++ b/frontend/src/app/feature/home/home.component.ts @@ -3,11 +3,12 @@ import { KeycloakService } from 'keycloak-angular'; import { MatDialog } from '@angular/material/dialog'; import { DepositComponent } from '../deposit/deposit.component'; +import { NavbarComponent } from '../../shared/components/navbar/navbar.component'; @Component({ selector: 'app-homepage', standalone: true, - imports: [], - templateUrl: './home.component.html', + imports: [NavbarComponent], + templateUrl: './homepage.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class HomeComponent { diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html index aa8bbd8..4e7c12f 100644 --- a/frontend/src/app/landing-page/landing-page.component.html +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -1 +1,2 @@ + diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts new file mode 100644 index 0000000..826d86f --- /dev/null +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { NavbarComponent } from '../shared/components/navbar/navbar.component'; + +@Component({ + selector: 'app-landing-page', + standalone: true, + imports: [NavbarComponent], + templateUrl: './landing-page.component.html', +}) +export class LandingPageComponent {} -- 2.45.3 From 8e986727ec849af9f7995b74bdb4af267d280dd1 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 13 Feb 2025 12:13:24 +0100 Subject: [PATCH 105/368] feat(landing-page): add welcome bonus and game carousel --- .../landing-page/landing-page.component.html | 127 +++++++++++++++++- .../landing-page/landing-page.component.ts | 51 ++++++- .../components/footer/footer.component.html | 6 +- .../components/navbar/navbar.component.ts | 3 +- frontend/src/styles.css | 33 ++++- 5 files changed, 208 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/landing-page/landing-page.component.html b/frontend/src/app/landing-page/landing-page.component.html index 4e7c12f..954abf9 100644 --- a/frontend/src/app/landing-page/landing-page.component.html +++ b/frontend/src/app/landing-page/landing-page.component.html @@ -1,2 +1,127 @@ - + +
+
+
+
+

+ Willkommensbonus +

+
+ 200% bis zu 500€ +
+

+ + 200 Freispiele +

+ + +
+ +
+

Beliebte Spiele

+
+
+
+
+
+
+

Slots

+

Klassische Spielautomaten

+ +
+
+
+
+

Plinko

+

Spannendes Geschicklichkeitsspiel

+ +
+
+ +
+ +
+
+
+

Poker

+

Texas Hold'em & mehr

+ +
+
+
+
+

Liars Dice

+

WΓΌrfelspiel mit Strategie

+ +
+
+ +
+
+
+ + + + +
+ +
+
+
+ +
+
+
50 Mio.€+
+
Ausgezahlt
+
+ +
+
10 Mio.+
+
Spiele
+
+ +
+
24/7
+
Support *
+
+
+
+
+
diff --git a/frontend/src/app/landing-page/landing-page.component.ts b/frontend/src/app/landing-page/landing-page.component.ts index 826d86f..1471a99 100644 --- a/frontend/src/app/landing-page/landing-page.component.ts +++ b/frontend/src/app/landing-page/landing-page.component.ts @@ -1,10 +1,55 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; import { NavbarComponent } from '../shared/components/navbar/navbar.component'; +import { NgFor } from '@angular/common'; @Component({ selector: 'app-landing-page', standalone: true, - imports: [NavbarComponent], + imports: [NavbarComponent, NgFor], templateUrl: './landing-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LandingPageComponent {} +export class LandingPageComponent implements OnInit, OnDestroy { + currentSlide = 0; + private autoplayInterval: any; + + ngOnInit() { + this.startAutoplay(); + } + + ngOnDestroy() { + this.stopAutoplay(); + } + + prevSlide() { + this.currentSlide = this.currentSlide === 0 ? 1 : 0; + this.resetAutoplay(); + } + + nextSlide() { + this.currentSlide = this.currentSlide === 1 ? 0 : 1; + this.resetAutoplay(); + } + + goToSlide(index: number) { + this.currentSlide = index; + this.resetAutoplay(); + } + + private startAutoplay() { + this.autoplayInterval = setInterval(() => { + this.nextSlide(); + }, 5000); + } + + private stopAutoplay() { + if (this.autoplayInterval) { + clearInterval(this.autoplayInterval); + } + } + + private resetAutoplay() { + this.stopAutoplay(); + this.startAutoplay(); + } +} diff --git a/frontend/src/app/shared/components/footer/footer.component.html b/frontend/src/app/shared/components/footer/footer.component.html index c5709d3..d29c406 100644 --- a/frontend/src/app/shared/components/footer/footer.component.html +++ b/frontend/src/app/shared/components/footer/footer.component.html @@ -1,4 +1,4 @@ -