generated from nhcarrigan/template
154 lines
5.5 KiB
Svelte
154 lines
5.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { fade, fly } from 'svelte/transition';
|
|
import { cubicOut } from 'svelte/easing';
|
|
import { listen } from '@tauri-apps/api/event';
|
|
import type { AchievementUnlockedEvent } from '$lib/types/achievements';
|
|
|
|
let achievements = $state<AchievementUnlockedEvent[]>([]);
|
|
let currentAchievement = $state<AchievementUnlockedEvent | null>(null);
|
|
let showNotification = $state(false);
|
|
|
|
onMount(async () => {
|
|
const unlisten = await listen<AchievementUnlockedEvent>('achievement:unlocked', (event) => {
|
|
achievements.push(event.payload);
|
|
if (!showNotification) {
|
|
showNext();
|
|
}
|
|
});
|
|
|
|
return unlisten;
|
|
});
|
|
|
|
function showNext() {
|
|
if (achievements.length > 0) {
|
|
currentAchievement = achievements.shift() || null;
|
|
showNotification = true;
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
showNotification = false;
|
|
// Show next achievement after animation completes
|
|
setTimeout(() => showNext(), 300);
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function dismiss() {
|
|
showNotification = false;
|
|
// Show next achievement after animation completes
|
|
setTimeout(() => showNext(), 300);
|
|
}
|
|
|
|
function getRarityColor(rarity: string): string {
|
|
switch (rarity) {
|
|
case 'legendary': return 'from-yellow-400 to-orange-500';
|
|
case 'epic': return 'from-purple-400 to-pink-500';
|
|
case 'rare': return 'from-blue-400 to-indigo-500';
|
|
default: return 'from-green-400 to-emerald-500';
|
|
}
|
|
}
|
|
|
|
function getAchievementRarity(id: string): string {
|
|
// Determine rarity based on achievement ID
|
|
if (id === 'TokenMaster') return 'legendary';
|
|
if (['CodeMachine', 'Unstoppable'].includes(id)) return 'epic';
|
|
if (['BlossomingCoder', 'CodeWizard', 'MasterBuilder', 'EnduranceChamp', 'DeepDive', 'CreativeCoder'].includes(id)) return 'rare';
|
|
return 'common';
|
|
}
|
|
</script>
|
|
|
|
{#if showNotification && currentAchievement}
|
|
<div
|
|
class="fixed top-20 right-4 z-50 max-w-sm"
|
|
in:fly={{ x: 300, duration: 500, easing: cubicOut }}
|
|
out:fade={{ duration: 300 }}
|
|
>
|
|
<!-- Backdrop with animated gradient border -->
|
|
<div class="relative p-[2px] rounded-lg overflow-hidden">
|
|
<!-- Animated gradient border -->
|
|
<div class="absolute inset-0 bg-gradient-to-r {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} animate-pulse"></div>
|
|
|
|
<!-- Main notification content -->
|
|
<div class="relative bg-[var(--bg-primary)] rounded-lg p-4 shadow-2xl backdrop-blur-sm">
|
|
<button
|
|
onclick={dismiss}
|
|
onkeydown={(e) => e.key === 'Enter' && dismiss()}
|
|
class="absolute top-2 right-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
|
aria-label="Dismiss notification"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<!-- Icon with animated sparkles -->
|
|
<div class="relative flex-shrink-0">
|
|
<div class="text-5xl animate-bounce">{currentAchievement.achievement.icon}</div>
|
|
|
|
<!-- Sparkle animations -->
|
|
<div class="absolute -top-1 -right-1 text-yellow-400 animate-ping">✨</div>
|
|
<div class="absolute -bottom-1 -left-1 text-yellow-400 animate-ping animation-delay-200">✨</div>
|
|
<div class="absolute top-1/2 -right-2 text-yellow-400 animate-ping animation-delay-400">✨</div>
|
|
</div>
|
|
|
|
<!-- Text content -->
|
|
<div class="flex-1 min-w-0 pt-1">
|
|
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
Achievement Unlocked!
|
|
</h3>
|
|
<p class="text-lg font-bold text-[var(--text-primary)] mt-1">
|
|
{currentAchievement.achievement.name}
|
|
</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
{currentAchievement.achievement.description}
|
|
</p>
|
|
|
|
<!-- Rarity badge -->
|
|
<div class="mt-2 inline-flex items-center">
|
|
<span class="px-2 py-1 text-xs font-medium rounded-full bg-gradient-to-r {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} text-white capitalize">
|
|
{getAchievementRarity(currentAchievement.achievement.id)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Celebration confetti effect (CSS only) -->
|
|
<div class="absolute inset-0 pointer-events-none overflow-hidden rounded-lg">
|
|
{#each Array(10) as _, i}
|
|
<div
|
|
class="absolute w-2 h-2 bg-gradient-to-br {getRarityColor(getAchievementRarity(currentAchievement.achievement.id))} rounded-full animate-fall"
|
|
style="left: {Math.random() * 100}%; animation-delay: {Math.random() * 2}s; animation-duration: {2 + Math.random() * 2}s;"
|
|
></div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
@keyframes fall {
|
|
0% {
|
|
transform: translateY(-20px) rotate(0deg);
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translateY(400px) rotate(720deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.animate-fall {
|
|
animation: fall linear infinite;
|
|
}
|
|
|
|
.animation-delay-200 {
|
|
animation-delay: 200ms;
|
|
}
|
|
|
|
.animation-delay-400 {
|
|
animation-delay: 400ms;
|
|
}
|
|
</style> |