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 @@
+
+
+
+
+
+
+
+
+
+
+
🎰
+
+ 💎
+
+
+ 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 }}
+
+
+
+
+
+
+ CLAIM YOUR €10,000 NOW
+
+
+
+
+
+ ✓
+ 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
+
+
+
+
+
+
+
+
💎
+
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.
+
+
+
+
+
+
+
+
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'),
+ ],
+}