initial commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
interface Star {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
opacity: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
const starCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
let backgroundStars: Star[] = [];
|
||||
let animationFrameId: number;
|
||||
|
||||
function initBackgroundStars() {
|
||||
if (typeof window === 'undefined' || !starCanvasRef.value) return;
|
||||
|
||||
const canvas = starCanvasRef.value;
|
||||
// Explizite Zuweisung der Fenstergröße an die Canvas-Attribute
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
backgroundStars = [];
|
||||
const count = Math.min(window.innerWidth / 3, 400); // Etwas mehr Sterne für bessere Sichtbarkeit
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
backgroundStars.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 1.5 + 0.5,
|
||||
opacity: Math.random(),
|
||||
speed: Math.random() * 0.15 + 0.05,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawBackgroundStars() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const canvas = starCanvasRef.value;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < backgroundStars.length; i++) {
|
||||
const s = backgroundStars[i];
|
||||
|
||||
s.opacity += (Math.random() - 0.5) * 0.03;
|
||||
if (s.opacity <= 0.1) s.opacity = 0.1;
|
||||
if (s.opacity >= 0.8) s.opacity = 0.8;
|
||||
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${s.opacity})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
s.y -= s.speed;
|
||||
if (s.y < -10) {
|
||||
s.y = canvas.height + 10;
|
||||
s.x = Math.random() * canvas.width;
|
||||
}
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(drawBackgroundStars);
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
initBackgroundStars();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initBackgroundStars();
|
||||
drawBackgroundStars();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-none fixed inset-0 h-full w-full"
|
||||
style="z-index: 0"
|
||||
>
|
||||
<div class="absolute inset-0 bg-[#020617]"></div>
|
||||
|
||||
<canvas
|
||||
ref="starCanvasRef"
|
||||
class="absolute inset-0 block h-full w-full"
|
||||
></canvas>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,#020617_100%)] opacity-60"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,279 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
interface Badge {
|
||||
label: string;
|
||||
class: string;
|
||||
}
|
||||
|
||||
interface Bonus {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
image_path: string;
|
||||
brand_color: string;
|
||||
hover_color: string;
|
||||
badges: Badge[];
|
||||
min_deposit: string;
|
||||
max_bet: string;
|
||||
wagering: string;
|
||||
free_spins: string;
|
||||
button_link: string;
|
||||
key_features: string[];
|
||||
is_sticky: boolean;
|
||||
is_no_deposit: boolean;
|
||||
is_featured: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
bonuses?: Bonus[];
|
||||
}>();
|
||||
|
||||
const bonusGridRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const handleMouseMoveGrid = (e: MouseEvent) => {
|
||||
if (!bonusGridRef.value) return;
|
||||
|
||||
const gridRect = bonusGridRef.value.getBoundingClientRect();
|
||||
const mx = e.clientX - gridRect.left;
|
||||
const my = e.clientY - gridRect.top;
|
||||
|
||||
bonusGridRef.value.style.setProperty('--mouse-x', `${mx}px`);
|
||||
bonusGridRef.value.style.setProperty('--mouse-y', `${my}px`);
|
||||
|
||||
const cards = bonusGridRef.value.querySelectorAll('.bonus-card') as NodeListOf<HTMLElement>;
|
||||
cards.forEach(card => {
|
||||
card.style.setProperty('--card-left', `${card.offsetLeft}`);
|
||||
card.style.setProperty('--card-top', `${card.offsetTop}`);
|
||||
});
|
||||
};
|
||||
|
||||
const trackClick = async (bonusId: number) => {
|
||||
try {
|
||||
await fetch('/api/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bonus_id: bonusId,
|
||||
type: 'click'
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Tracking failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
const trackView = async (bonusId: number) => {
|
||||
try {
|
||||
await fetch('/api/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bonus_id: bonusId,
|
||||
type: 'view'
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Tracking failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (bonusGridRef.value) {
|
||||
bonusGridRef.value.addEventListener('mousemove', handleMouseMoveGrid);
|
||||
}
|
||||
|
||||
if (props.bonuses && props.bonuses.length > 0) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const card = entry.target as HTMLElement;
|
||||
const bonusId = card.dataset.id;
|
||||
if (bonusId && !card.dataset.viewed) {
|
||||
trackView(Number(bonusId));
|
||||
card.dataset.viewed = 'true';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
setTimeout(() => {
|
||||
if (bonusGridRef.value) {
|
||||
const cards = bonusGridRef.value.querySelectorAll('.bonus-card');
|
||||
cards.forEach(card => observer.observe(card));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (bonusGridRef.value) {
|
||||
bonusGridRef.value.removeEventListener('mousemove', handleMouseMoveGrid);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="bonuses" class="container mx-auto px-6 py-28 relative z-30">
|
||||
<h2 class="text-5xl font-black italic text-center mb-20 uppercase tracking-tighter">Premium <span class="text-blue-500">Deals</span></h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-16 items-end bg-gray-950/50 p-6 rounded-3xl border border-white/5 backdrop-blur-sm">
|
||||
<div class="xl:col-span-2 relative">
|
||||
<label class="text-xs text-gray-500 mb-2 block uppercase font-bold tracking-wider">Schnellsuche</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-5 top-1/2 -translate-y-1/2 text-gray-600"></i>
|
||||
<input type="text" placeholder="Casino Name oder Bonusart..." class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-14 outline-none focus:border-purple-500 transition focus:ring-1 focus:ring-purple-500 text-white placeholder-gray-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative min-w-[200px] group">
|
||||
<label class="text-xs text-gray-500 mb-2 block uppercase font-bold tracking-wider">Land</label>
|
||||
<div class="bg-white/5 border border-white/10 p-[14px_20px] rounded-xl cursor-pointer flex justify-between items-center transition group-hover:border-white/30 text-white relative z-10">
|
||||
<span>🇩🇪 Germany</span>
|
||||
<i class="fas fa-chevron-down opacity-50 text-xs transition-transform"></i>
|
||||
</div>
|
||||
<div class="absolute top-[115%] left-0 right-0 bg-[#111827] border border-white/10 rounded-xl hidden group-hover:block z-50 overflow-hidden shadow-[0_10px_25px_rgba(0,0,0,0.5)]">
|
||||
<div class="p-[12px_20px] cursor-pointer transition hover:bg-white/5">🇩🇪 Germany</div>
|
||||
<div class="p-[12px_20px] cursor-pointer transition hover:bg-white/5">🇦🇹 Austria</div>
|
||||
<div class="p-[12px_20px] cursor-pointer transition hover:bg-white/5">🇨🇭 Switzerland</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative min-w-[200px] group">
|
||||
<label class="text-xs text-gray-500 mb-2 block uppercase font-bold tracking-wider">Methode</label>
|
||||
<div class="bg-white/5 border border-white/10 p-[14px_20px] rounded-xl cursor-pointer flex justify-between items-center transition group-hover:border-white/30 text-white relative z-10">
|
||||
<span><i class="fas fa-wallet text-blue-400 mr-2"></i> Hybrid</span>
|
||||
<i class="fas fa-chevron-down opacity-50 text-xs transition-transform"></i>
|
||||
</div>
|
||||
<div class="absolute top-[115%] left-0 right-0 bg-[#111827] border border-white/10 rounded-xl hidden group-hover:block z-50 overflow-hidden shadow-[0_10px_25px_rgba(0,0,0,0.5)]">
|
||||
<div class="p-[12px_20px] cursor-pointer transition hover:bg-white/5"><i class="fas fa-wallet text-blue-400 mr-2"></i> Hybrid</div>
|
||||
<div class="p-[12px_20px] cursor-pointer transition hover:bg-white/5"><i class="fab fa-bitcoin text-orange-400 mr-2"></i> Crypto</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 bonus-grid relative" ref="bonusGridRef">
|
||||
|
||||
<div v-for="bonus in bonuses" :key="bonus.id"
|
||||
:data-id="bonus.id"
|
||||
:class="[
|
||||
'bonus-card relative bg-[#111827] rounded-[28px] border overflow-hidden transition-transform duration-400 hover:-translate-y-2 hover:scale-[1.01] h-full z-[1] flex flex-col group',
|
||||
bonus.is_featured ? 'border-yellow-500/50 shadow-lg shadow-yellow-500/20' : 'border-white/5'
|
||||
]"
|
||||
:style="{ '--spot-clr': bonus.hover_color || 'rgba(255,255,255,0.1)' }">
|
||||
|
||||
<img v-if="bonus.image_path" :src="bonus.image_path" class="absolute inset-0 w-full h-full object-cover opacity-15 z-[-1] transition-opacity duration-300 group-hover:opacity-25" :alt="bonus.name">
|
||||
<div class="spotlight"></div>
|
||||
|
||||
<div class="p-8 relative z-10 flex flex-col h-full">
|
||||
<h3 class="text-4xl font-black italic tracking-tighter mb-2" :style="{ color: bonus.brand_color || '#ffffff' }">
|
||||
{{ bonus.name }}
|
||||
</h3>
|
||||
<p class="text-gray-300 text-sm leading-relaxed mb-6">{{ bonus.description }}</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<span v-if="bonus.is_sticky" class="px-3 py-1 bg-amber-500/20 text-amber-300 border border-amber-500/30 rounded-full text-[10px] font-bold uppercase tracking-wider">Sticky</span>
|
||||
<span v-if="bonus.is_no_deposit" class="px-3 py-1 bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 rounded-full text-[10px] font-bold uppercase tracking-wider">No Deposit</span>
|
||||
<span v-if="bonus.is_featured" class="px-3 py-1 bg-yellow-500/20 text-yellow-300 border border-yellow-500/30 rounded-full text-[10px] font-bold uppercase tracking-wider">Featured</span>
|
||||
|
||||
<span v-for="(badge, index) in bonus.badges" :key="index"
|
||||
class="px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-white/10 text-white border border-white/20">
|
||||
{{ typeof badge === 'string' ? badge : badge.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul v-if="bonus.key_features && bonus.key_features.length" class="mb-8 space-y-1">
|
||||
<li v-for="feature in bonus.key_features" :key="feature" class="text-[11px] text-gray-400 flex items-center">
|
||||
<i class="fas fa-check text-green-500 mr-2 text-[9px]"></i> {{ feature }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto space-y-3 text-xs text-gray-400 border-t border-white/10 pt-6 mb-8">
|
||||
<div class="flex justify-between"><span>Min Deposit</span><span class="text-white font-bold">{{ bonus.min_deposit || 'N/A' }}</span></div>
|
||||
<div class="flex justify-between"><span>Wagering</span><span class="text-white font-bold">{{ bonus.wagering || 'N/A' }}</span></div>
|
||||
<div class="flex justify-between"><span>Max Bet</span><span class="text-white font-bold">{{ bonus.max_bet || 'N/A' }}</span></div>
|
||||
<div v-if="bonus.free_spins" class="flex justify-between"><span>Free Spins</span><span class="text-white font-bold">{{ bonus.free_spins }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<a :href="bonus.button_link" target="_blank" @click="trackClick(bonus.id)"
|
||||
class="grow bg-white text-black font-black py-4 rounded-xl hover:text-white transition uppercase tracking-tighter cursor-pointer text-center"
|
||||
:style="{ '--hover-bg': bonus.brand_color || '#a855f7' }">
|
||||
Deal Sichern
|
||||
</a>
|
||||
<button class="bg-white/5 border border-white/10 hover:bg-white/15 hover:border-white transition-all duration-300 px-5 rounded-xl text-white cursor-pointer" title="Mehr Infos"><i class="fas fa-info"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!bonuses || bonuses.length === 0" class="col-span-full text-center py-20 text-gray-500">
|
||||
Keine Deals gefunden.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bonus-grid {
|
||||
--mouse-x: -1000px;
|
||||
--mouse-y: -1000px;
|
||||
}
|
||||
|
||||
.bonus-card {
|
||||
--spot-clr: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.spotlight {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, var(--spot-clr) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
mix-blend-mode: screen;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
left: calc(var(--mouse-x) - (var(--card-left, 0) * 1px));
|
||||
top: calc(var(--mouse-y) - (var(--card-top, 0) * 1px));
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.bonus-grid:hover .spotlight {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.bonus-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 28px;
|
||||
padding: 2px;
|
||||
background: radial-gradient(
|
||||
350px circle at calc(var(--mouse-x) - var(--card-left, 0) * 1px) calc(var(--mouse-y) - var(--card-top, 0) * 1px),
|
||||
var(--spot-clr),
|
||||
transparent 80%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.bonus-grid:hover .bonus-card::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a[style*="--hover-bg"]:hover {
|
||||
background-color: var(--hover-bg) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const heroCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
let particles: Particle[] = [];
|
||||
const mouse = { x: -1000, y: -1000, radius: 120 };
|
||||
let animationFrameIdHero: number;
|
||||
|
||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
};
|
||||
|
||||
class Particle {
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
isWhite: boolean;
|
||||
blinkTimer: number;
|
||||
opacity: number;
|
||||
ease: number;
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
private canvasWidth: number,
|
||||
private canvasHeight: number,
|
||||
) {
|
||||
this.baseX = x;
|
||||
this.baseY = y;
|
||||
this.x = Math.random() * canvasWidth;
|
||||
this.y = Math.random() * canvasHeight;
|
||||
this.size = 1.6;
|
||||
this.isWhite = false;
|
||||
this.blinkTimer = 0;
|
||||
this.opacity = 1;
|
||||
this.ease = 0.06;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
if (this.isWhite) {
|
||||
this.opacity = Math.sin(this.blinkTimer) * 0.5 + 0.5;
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0.3, this.opacity)})`;
|
||||
this.blinkTimer += 0.1;
|
||||
if (this.blinkTimer > Math.PI) {
|
||||
this.isWhite = false;
|
||||
this.opacity = 1;
|
||||
}
|
||||
} else {
|
||||
const ratio = this.x / this.canvasWidth;
|
||||
const r = 168 - (168 - 59) * ratio;
|
||||
const g = 85 + (130 - 85) * ratio;
|
||||
ctx.fillStyle = `rgb(${r},${g},255)`;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
update() {
|
||||
let dx = mouse.x - this.x;
|
||||
let dy = mouse.y - this.y;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance < mouse.radius) {
|
||||
const force = (mouse.radius - distance) / mouse.radius;
|
||||
this.x -= (dx / distance) * force * 7;
|
||||
this.y -= (dy / distance) * force * 7;
|
||||
} else {
|
||||
this.x += (this.baseX - this.x) * this.ease;
|
||||
this.y += (this.baseY - this.y) * this.ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initHero() {
|
||||
if (!heroCanvasRef.value) return;
|
||||
const canvas = heroCanvasRef.value;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
const tCanvas = document.createElement('canvas');
|
||||
const tCtx = tCanvas.getContext('2d');
|
||||
if (!tCtx) return;
|
||||
|
||||
tCanvas.width = canvas.width;
|
||||
tCanvas.height = canvas.height;
|
||||
|
||||
// Wir verschieben den Text im Partikel-Canvas etwas nach unten,
|
||||
// um Platz für das "Welcome" darüber zu schaffen
|
||||
const fontSize = Math.min(canvas.width / 9, 90);
|
||||
tCtx.fillStyle = 'white';
|
||||
tCtx.font = `bold ${fontSize}px Arial`;
|
||||
tCtx.textAlign = 'center';
|
||||
tCtx.fillText(
|
||||
'BRATANBONUS.NET',
|
||||
tCanvas.width / 2,
|
||||
tCanvas.height / 2 + 40,
|
||||
);
|
||||
|
||||
const pixels = tCtx.getImageData(0, 0, tCanvas.width, tCanvas.height);
|
||||
particles = [];
|
||||
const step = 5;
|
||||
for (let y = 0; y < pixels.height; y += step) {
|
||||
for (let x = 0; x < pixels.width; x += step) {
|
||||
if (pixels.data[y * 4 * pixels.width + x * 4 + 3] > 128) {
|
||||
particles.push(new Particle(x, y, canvas.width, canvas.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function animateHero() {
|
||||
if (!heroCanvasRef.value) return;
|
||||
const ctx = heroCanvasRef.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, heroCanvasRef.value.width, heroCanvasRef.value.height);
|
||||
if (Math.random() > 0.93 && particles.length > 0) {
|
||||
const p = particles[Math.floor(Math.random() * particles.length)];
|
||||
if (!p && !p.isWhite) {
|
||||
p.isWhite = true;
|
||||
p.blinkTimer = 0;
|
||||
}
|
||||
}
|
||||
particles.forEach((p) => {
|
||||
p.update();
|
||||
p.draw(ctx);
|
||||
});
|
||||
animationFrameIdHero = requestAnimationFrame(animateHero);
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
initHero();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleGlobalMouseMove);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
setTimeout(() => {
|
||||
initHero();
|
||||
animateHero();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleGlobalMouseMove);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (animationFrameIdHero) cancelAnimationFrame(animationFrameIdHero);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="relative flex h-screen flex-col items-center justify-center overflow-hidden bg-transparent"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute top-[32%] left-1/2 z-10 w-full -translate-x-1/2 text-center"
|
||||
>
|
||||
<h2
|
||||
class="text-2xl font-thin tracking-[1em] uppercase opacity-30 select-none md:text-4xl"
|
||||
>
|
||||
Welcome
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<canvas
|
||||
ref="heroCanvasRef"
|
||||
class="pointer-events-none absolute inset-0 z-20 block h-screen w-full bg-transparent"
|
||||
></canvas>
|
||||
|
||||
<a
|
||||
href="#bonuses"
|
||||
class="group absolute bottom-10 left-1/2 z-30 flex -translate-x-1/2 flex-col items-center gap-2 opacity-50 transition-all duration-300 hover:opacity-100"
|
||||
>
|
||||
<span
|
||||
class="text-[10px] font-bold tracking-[0.3em] text-white/40 uppercase group-hover:text-white/80"
|
||||
>Scroll</span
|
||||
>
|
||||
<div
|
||||
class="flex h-10 w-6 justify-center rounded-full border-2 border-white/20 p-1"
|
||||
>
|
||||
<div
|
||||
class="animate-scroll-dot h-2 w-1 rounded-full bg-purple-500"
|
||||
></div>
|
||||
</div>
|
||||
<i
|
||||
class="fas fa-chevron-down mt-1 animate-bounce text-sm text-white/30"
|
||||
></i>
|
||||
</a>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes scroll-dot {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(15px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scroll-dot {
|
||||
animation: scroll-dot 2s infinite;
|
||||
}
|
||||
|
||||
/* Verhindert Text-Markierung während man mit der Maus über den Canvas fährt */
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const isTwitchLive = ref(false);
|
||||
const isKickLive = ref(false);
|
||||
|
||||
const checkLiveStatus = async () => {
|
||||
try {
|
||||
const { data } = await axios.get('/api/live-status');
|
||||
isTwitchLive.value = data.twitch;
|
||||
isKickLive.value = data.kick;
|
||||
} catch (e) {
|
||||
console.error('Status-Check fehlgeschlagen', e);
|
||||
}
|
||||
};
|
||||
|
||||
const trackSocial = async (platform: string) => {
|
||||
try {
|
||||
await axios.post('/api/track-social', { platform });
|
||||
} catch (e) {
|
||||
console.error('Tracking failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkLiveStatus();
|
||||
// Alle 5 Minuten im Hintergrund aktualisieren
|
||||
setInterval(checkLiveStatus, 300000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
id="find-me"
|
||||
class="social-section relative z-30 bg-gray-950/20 py-32"
|
||||
>
|
||||
<div class="container mx-auto px-6">
|
||||
<h2
|
||||
class="mb-24 text-center text-5xl font-black tracking-tighter text-white uppercase italic"
|
||||
>
|
||||
Join the <span class="text-purple-500">Squad</span>
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="mx-auto grid max-w-7xl grid-cols-1 gap-8 md:grid-cols-3"
|
||||
>
|
||||
<a
|
||||
href="https://www.twitch.tv/bratander1ste"
|
||||
target="_blank"
|
||||
@click="trackSocial('twitch')"
|
||||
class="social-card group twitch flex cursor-pointer flex-col items-center gap-6 rounded-[30px] border border-white/10 bg-white/5 p-8 transition-all duration-500 ease-out hover:border-[#9146FF] hover:bg-white/10"
|
||||
:class="{ 'live-active-twitch': isTwitchLive }"
|
||||
>
|
||||
<div v-if="isTwitchLive" class="live-indicator">
|
||||
<span class="pulse-dot"></span> LIVE
|
||||
</div>
|
||||
|
||||
<i
|
||||
class="fab fa-twitch text-6xl text-[#9146FF] drop-shadow-[0_0_15px_rgba(145,70,255,0.4)] transition-transform duration-500 group-hover:scale-110"
|
||||
></i>
|
||||
<div class="text-center">
|
||||
<h4
|
||||
class="mb-2 text-2xl font-black tracking-tight text-white"
|
||||
>
|
||||
TWITCH
|
||||
</h4>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
Action jeden Abend live
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
@click="trackSocial('instagram')"
|
||||
class="social-card group instagram flex cursor-pointer flex-col items-center gap-6 rounded-[30px] border border-white/10 bg-white/5 p-8 transition-all duration-500 ease-out hover:border-[#E1306C] hover:bg-white/10"
|
||||
>
|
||||
<i
|
||||
class="fab fa-instagram text-6xl text-pink-500 drop-shadow-[0_0_15px_rgba(236,72,153,0.4)] transition-transform duration-500 group-hover:scale-110"
|
||||
></i>
|
||||
<div class="text-center">
|
||||
<h4
|
||||
class="mb-2 text-2xl font-black tracking-tight text-white"
|
||||
>
|
||||
INSTAGRAM
|
||||
</h4>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
News & Giveaways
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://kick.com/Bratander1ste"
|
||||
target="_blank"
|
||||
@click="trackSocial('kick')"
|
||||
class="social-card group kick flex cursor-pointer flex-col items-center gap-6 rounded-[30px] border border-white/10 bg-white/5 p-8 transition-all duration-500 ease-out hover:border-[#53FC18] hover:bg-white/10"
|
||||
:class="{ 'live-active-kick': isKickLive }"
|
||||
>
|
||||
<div v-if="isKickLive" class="live-indicator">
|
||||
<span class="pulse-dot"></span> LIVE
|
||||
</div>
|
||||
|
||||
<i
|
||||
class="fas fa-bolt text-6xl text-[#53FC18] drop-shadow-[0_0_15px_rgba(83,252,24,0.4)] transition-transform duration-500 group-hover:scale-110"
|
||||
></i>
|
||||
<div class="text-center">
|
||||
<h4
|
||||
class="mb-2 text-2xl font-black tracking-tight text-[#53FC18]"
|
||||
>
|
||||
KICK
|
||||
</h4>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
The home of high stakes
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.social-section {
|
||||
perspective: 2000px;
|
||||
}
|
||||
.social-card {
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Initiale 3D Lage */
|
||||
.social-card.twitch {
|
||||
transform: rotateY(-10deg) rotateX(5deg);
|
||||
}
|
||||
.social-card.kick {
|
||||
transform: rotateY(10deg) rotateX(5deg);
|
||||
}
|
||||
.social-card:hover {
|
||||
transform: rotateY(0deg) rotateX(0deg) translateZ(20px) !important;
|
||||
}
|
||||
|
||||
/* LIVE INDICATOR */
|
||||
.live-indicator {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 0, 0, 0.15);
|
||||
border: 1px solid #ff0000;
|
||||
color: #ff4d4d;
|
||||
padding: 4px 12px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 0 15px rgba(255, 0, 0, 0.4);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ff0000;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* LIVE HIGHLIGHTS */
|
||||
.live-active-twitch {
|
||||
border-color: rgba(145, 70, 255, 0.8) !important;
|
||||
box-shadow: 0 0 40px rgba(145, 70, 255, 0.2);
|
||||
background: rgba(145, 70, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
.live-active-kick {
|
||||
border-color: rgba(83, 252, 24, 0.8) !important;
|
||||
box-shadow: 0 0 40px rgba(83, 252, 24, 0.2);
|
||||
background: rgba(83, 252, 24, 0.05) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user