Neuaufbau des Repositories
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-04-10 21:14:11 +02:00
parent 3f61033d14
commit 79bea8cf56
309 changed files with 31416 additions and 0 deletions
+665
View File
@@ -0,0 +1,665 @@
<script setup lang="ts">
import { Head, useForm, Link } from '@inertiajs/vue3';
import {
Plus,
Trash2,
Link as LinkIcon,
DollarSign,
RotateCw,
Award,
XSquare,
Star,
ArrowLeft,
} from 'lucide-vue-next';
import { ref, watch } from 'vue';
interface PredefinedBadge {
label: string;
class: string;
}
const props = defineProps<{
predefinedBadges: PredefinedBadge[];
predefinedKeyFeatures: string[];
}>();
const form = useForm({
name: '',
description: '',
image_path: '', // For URL input
image_file: null as File | null, // For file upload
brand_color: '#a855f7', // Default purple
hover_color: 'rgba(168, 85, 247, 0.8)', // Default purple rgba
badges: [],
min_deposit: '',
max_bet: '',
wagering: '',
free_spins: '',
button_link: '',
key_features: [],
is_sticky: false,
is_no_deposit: false,
is_featured: false,
order: 0,
});
const newBadge = ref('');
const addBadge = () => {
if (newBadge.value.trim()) {
form.badges.push({
label: newBadge.value.trim(),
class: 'bg-white/10',
});
newBadge.value = '';
}
};
const removeBadge = (index: number) => {
form.badges.splice(index, 1);
};
const addPredefinedBadge = (badge: PredefinedBadge) => {
if (!form.badges.some((b) => b.label === badge.label)) {
form.badges.push(badge);
}
};
const newFeature = ref('');
const addFeature = () => {
if (newFeature.value.trim()) {
form.key_features.push(newFeature.value.trim());
newFeature.value = '';
}
};
const removeFeature = (index: number) => {
form.key_features.splice(index, 1);
};
const addPredefinedFeature = (feature: string) => {
if (!form.key_features.includes(feature)) {
form.key_features.push(feature);
}
};
const imagePreviewUrl = ref<string | null>(null);
const handleImageFileChange = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files[0]) {
form.image_file = target.files[0];
imagePreviewUrl.value = URL.createObjectURL(target.files[0]);
form.image_path = ''; // Clear image_path if a file is uploaded
} else {
form.image_file = null;
imagePreviewUrl.value = null;
}
};
// Watch for changes in image_path to update preview if it's a URL
watch(
() => form.image_path,
(newPath) => {
if (newPath && !form.image_file) {
imagePreviewUrl.value = newPath;
} else if (!newPath && !form.image_file) {
imagePreviewUrl.value = null;
}
},
);
const submit = () => {
form.post('/admin/bonuses', {
onSuccess: () => {
// Handled by AdminLayout flash messages
},
onError: (errors) => {
console.error(errors);
// You might want to show a generic error notification here
},
});
};
</script>
<template>
<Head title="Create Bonus" />
<div class="space-y-8">
<!-- Header Section -->
<div
class="flex items-center justify-between gap-6 rounded-[32px] border border-white/5 bg-[#0f172a] p-8 shadow-xl"
>
<div class="flex items-center gap-4">
<Link
href="/admin/bonuses"
class="flex h-10 w-10 items-center justify-center rounded-xl bg-white/5 text-gray-400 transition hover:bg-white/10 hover:text-white"
>
<ArrowLeft :size="20" />
</Link>
<h1
class="text-3xl font-black tracking-tighter uppercase italic"
>
Create New <span class="text-purple-500">Bonus</span>
</h1>
</div>
</div>
<form
@submit.prevent="submit"
class="space-y-10 rounded-[32px] border border-white/5 bg-[#0f172a] p-8 shadow-xl"
>
<div class="grid grid-cols-1 gap-12 lg:grid-cols-2">
<!-- Left Column: Content -->
<div class="space-y-8">
<section class="space-y-6">
<h2
class="flex items-center gap-3 text-xl font-black tracking-tight text-blue-400 uppercase italic"
>
<span class="h-[2px] w-8 bg-blue-400"></span> Main
Info
</h2>
<div class="space-y-4">
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Casino Name</span
>
<input
v-model="form.name"
type="text"
class="form-input"
required
placeholder="e.g. STAKE.COM"
/>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Description / Bonus Text</span
>
<textarea
v-model="form.description"
class="form-textarea"
rows="4"
placeholder="Exklusiver 200% Bonus bis zu 1000€..."
></textarea>
</label>
</div>
</section>
<section class="space-y-6">
<h2
class="flex items-center gap-3 text-xl font-black tracking-tight text-purple-400 uppercase italic"
>
<span class="h-[2px] w-8 bg-purple-400"></span>
Visuals & Branding
</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Brand Primary Color</span
>
<div class="flex gap-3">
<input
v-model="form.brand_color"
type="color"
class="form-color-input w-14"
/>
<input
v-model="form.brand_color"
type="text"
class="form-input flex-1 font-mono text-xs uppercase"
placeholder="#A855F7"
/>
</div>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Hover Glow Color (RGBA)</span
>
<input
v-model="form.hover_color"
type="text"
class="form-input font-mono text-xs"
placeholder="rgba(168, 85, 247, 0.8)"
/>
</label>
</div>
<div
class="grid grid-cols-1 items-end gap-6 sm:grid-cols-2"
>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Background Image URL</span
>
<input
v-model="form.image_path"
type="text"
class="form-input"
:disabled="!!form.image_file"
/>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Or Upload New File</span
>
<input
type="file"
@change="handleImageFileChange"
class="form-file-input"
accept="image/*"
/>
</label>
</div>
<div
v-if="imagePreviewUrl"
class="group relative aspect-video overflow-hidden rounded-2xl border border-white/5 bg-black/40"
>
<img
:src="imagePreviewUrl"
class="h-full w-full object-cover opacity-60"
/>
<div
class="absolute inset-0 flex items-center justify-center"
>
<span
class="rounded-full border border-white/10 bg-black/60 px-4 py-2 text-[10px] font-black tracking-widest uppercase backdrop-blur-md"
>Image Preview</span
>
</div>
</div>
</section>
</div>
<!-- Right Column: Stats & Features -->
<div class="space-y-8">
<section class="space-y-6">
<h2
class="flex items-center gap-3 text-xl font-black tracking-tight text-green-400 uppercase italic"
>
<span class="h-[2px] w-8 bg-green-400"></span> Bonus
Details
</h2>
<div class="grid grid-cols-2 gap-4">
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Min Deposit</span
>
<div class="relative">
<DollarSign
class="absolute top-1/2 left-4 -translate-y-1/2 text-gray-600"
:size="14"
/>
<input
v-model="form.min_deposit"
type="text"
class="form-input pl-10"
placeholder="20€"
/>
</div>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Max Bet</span
>
<div class="relative">
<XSquare
class="absolute top-1/2 left-4 -translate-y-1/2 text-gray-600"
:size="14"
/>
<input
v-model="form.max_bet"
type="text"
class="form-input pl-10"
placeholder="5€"
/>
</div>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Wagering</span
>
<div class="relative">
<RotateCw
class="absolute top-1/2 left-4 -translate-y-1/2 text-gray-600"
:size="14"
/>
<input
v-model="form.wagering"
type="text"
class="form-input pl-10"
placeholder="35x"
/>
</div>
</label>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Free Spins</span
>
<div class="relative">
<Award
class="absolute top-1/2 left-4 -translate-y-1/2 text-gray-600"
:size="14"
/>
<input
v-model="form.free_spins"
type="text"
class="form-input pl-10"
placeholder="100 FS"
/>
</div>
</label>
</div>
<label class="block">
<span
class="mb-2 block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Affiliate / Button Link</span
>
<div class="relative">
<LinkIcon
class="absolute top-1/2 left-4 -translate-y-1/2 text-gray-600"
:size="14"
/>
<input
v-model="form.button_link"
type="text"
class="form-input pl-10"
placeholder="https://..."
/>
</div>
</label>
</section>
<section class="space-y-6">
<h2
class="flex items-center gap-3 text-xl font-black tracking-tight text-orange-400 uppercase italic"
>
<span class="h-[2px] w-8 bg-orange-400"></span>
Configuration
</h2>
<div class="flex flex-wrap gap-6">
<label
class="group flex cursor-pointer items-center gap-3"
>
<div
class="relative h-6 w-12 rounded-full border border-white/10 bg-white/5 transition group-hover:border-orange-500/50"
>
<input
v-model="form.is_sticky"
type="checkbox"
class="peer sr-only"
/>
<div
class="absolute top-1 left-1 h-4 w-4 rounded-full bg-gray-500 transition-all peer-checked:left-7 peer-checked:bg-orange-500"
></div>
</div>
<span
class="text-[10px] font-black tracking-widest text-gray-400 uppercase peer-checked:text-white"
>Sticky Bonus</span
>
</label>
<label
class="group flex cursor-pointer items-center gap-3"
>
<div
class="relative h-6 w-12 rounded-full border border-white/10 bg-white/5 transition group-hover:border-cyan-500/50"
>
<input
v-model="form.is_no_deposit"
type="checkbox"
class="peer sr-only"
/>
<div
class="absolute top-1 left-1 h-4 w-4 rounded-full bg-gray-500 transition-all peer-checked:left-7 peer-checked:bg-cyan-500"
></div>
</div>
<span
class="text-[10px] font-black tracking-widest text-gray-400 uppercase"
>No Deposit</span
>
</label>
<label
class="group flex cursor-pointer items-center gap-3"
>
<div
class="relative h-6 w-12 rounded-full border border-white/10 bg-white/5 transition group-hover:border-yellow-500/50"
>
<input
v-model="form.is_featured"
type="checkbox"
class="peer sr-only"
/>
<div
class="absolute top-1 left-1 h-4 w-4 rounded-full bg-gray-500 transition-all peer-checked:left-7 peer-checked:bg-yellow-500"
></div>
</div>
<span
class="flex items-center gap-2 text-[10px] font-black tracking-widest text-gray-400 uppercase"
><Star :size="12" /> Super Card</span
>
</label>
<label class="ml-auto flex items-center gap-4">
<span
class="text-[10px] font-black tracking-widest text-gray-500 uppercase"
>Sort Order</span
>
<input
v-model="form.order"
type="number"
class="form-input w-20 text-center"
/>
</label>
</div>
<!-- Badges -->
<div class="space-y-4">
<span
class="block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Status Badges</span
>
<div class="mb-2 flex flex-wrap gap-2">
<div
v-for="(badge, idx) in form.badges"
:key="idx"
:class="[
'group flex items-center gap-2 rounded-lg px-3 py-1.5 transition hover:border-red-500/30',
badge.class ||
'border border-white/10 bg-white/5',
]"
>
<span
class="text-[10px] font-bold uppercase"
>{{ badge.label }}</span
>
<button
type="button"
@click="removeBadge(idx)"
class="text-gray-600 transition group-hover:text-red-500"
>
<Trash2 :size="12" />
</button>
</div>
</div>
<div
class="flex flex-wrap gap-2 border-t border-white/5 pt-4"
>
<span
class="w-full text-xs font-bold text-gray-500 uppercase"
>Predefined:</span
>
<button
type="button"
v-for="badge in predefinedBadges"
:key="badge.label"
@click="addPredefinedBadge(badge)"
:class="[
'rounded-lg px-3 py-1.5 text-[10px] font-bold uppercase transition',
badge.class,
form.badges.some(
(b) => b.label === badge.label,
)
? 'cursor-not-allowed opacity-50'
: 'hover:opacity-75',
]"
:disabled="
form.badges.some(
(b) => b.label === badge.label,
)
"
>
{{ badge.label }}
</button>
</div>
<div class="mt-2 flex w-full gap-2">
<input
v-model="newBadge"
type="text"
class="form-input flex-1 py-2 text-xs"
placeholder="Add Custom Badge (e.g. VIP DEAL)"
/>
<button
type="button"
@click="addBadge"
class="btn-secondary py-2"
>
<Plus :size="14" />
</button>
</div>
</div>
<!-- Key Features -->
<div class="space-y-4">
<span
class="block text-[10px] font-black tracking-[0.2em] text-gray-500 uppercase"
>Key Features (Pros)</span
>
<div class="mb-2 space-y-2">
<div
v-for="(feature, idx) in form.key_features"
:key="idx"
class="flex items-center justify-between rounded-xl border border-white/5 bg-white/[0.02] p-3"
>
<span class="text-xs text-gray-300">{{
feature
}}</span>
<button
type="button"
@click="removeFeature(idx)"
class="text-gray-600 transition hover:text-red-500"
>
<Trash2 :size="14" />
</button>
</div>
</div>
<div
class="flex flex-wrap gap-2 border-t border-white/5 pt-4"
>
<span
class="w-full text-xs font-bold text-gray-500 uppercase"
>Predefined:</span
>
<button
type="button"
v-for="feature in predefinedKeyFeatures"
:key="feature"
@click="addPredefinedFeature(feature)"
:class="[
'rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold uppercase transition',
form.key_features.includes(feature)
? 'cursor-not-allowed opacity-50'
: 'hover:opacity-75',
]"
:disabled="
form.key_features.includes(feature)
"
>
{{ feature }}
</button>
</div>
<div class="mt-2 flex w-full gap-2">
<input
v-model="newFeature"
type="text"
class="form-input flex-1 py-2 text-xs"
placeholder="Add Custom Feature (e.g. Instant Rakeback)"
/>
<button
type="button"
@click="addFeature"
class="btn-secondary py-2"
>
<Plus :size="14" />
</button>
</div>
</div>
</section>
</div>
</div>
<div class="flex justify-end gap-4 border-t border-white/5 pt-10">
<Link
href="/admin/bonuses"
class="rounded-2xl px-8 py-4 text-xs font-black tracking-widest text-gray-500 uppercase transition hover:text-white"
>Cancel</Link
>
<button
type="submit"
class="btn-primary flex items-center gap-3"
:disabled="form.processing"
>
{{ form.processing ? 'Saving...' : 'Save Bonus' }}
</button>
</div>
</form>
</div>
</template>
<style scoped>
@reference "../../../../css/app.css";
.form-input,
.form-textarea {
@apply w-full rounded-2xl border border-white/5 bg-white/[0.03] px-6 py-4 text-sm font-medium text-white placeholder-gray-600 transition outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20;
}
.form-file-input {
@apply w-full text-xs text-gray-500 transition-all duration-300 file:mr-4 file:rounded-2xl file:border-0 file:bg-white/5 file:px-6 file:py-3 file:text-[10px] file:font-black file:tracking-widest file:text-white file:uppercase hover:file:bg-white/10;
}
.form-color-input {
@apply h-14 cursor-pointer overflow-hidden rounded-2xl border-none bg-white/5 p-1;
}
.form-color-input::-webkit-color-swatch-wrapper {
padding: 0;
}
.form-color-input::-webkit-color-swatch {
border: none;
border-radius: 12px;
}
.btn-primary {
@apply rounded-2xl bg-gradient-to-r from-blue-600 to-purple-600 px-10 py-4 text-xs font-black tracking-widest text-white uppercase shadow-xl shadow-blue-500/20 transition-all duration-300 hover:scale-[1.02] active:scale-[0.98];
}
.btn-secondary {
@apply rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white transition hover:bg-white/10;
}
</style>
+381
View File
@@ -0,0 +1,381 @@
<script setup lang="ts">
import { Head, useForm, Link } from '@inertiajs/vue3';
import type { BreadcrumbItem } from '@/types';
import { ref, watch } from 'vue';
import { Save, Trash2, Image as ImageIcon, Link as LinkIcon, Palette, Droplet, DollarSign, Percent, RotateCw, Award, CheckSquare, XSquare, ArrowLeft, Star } from 'lucide-vue-next';
interface PredefinedBadge {
label: string;
class: string;
}
const props = defineProps<{
bonus: {
id: number;
name: string;
description: string;
image_path: string;
brand_color: string;
hover_color: string;
badges: any[];
min_deposit: string;
max_bet: string;
wagering: string;
free_spins: string;
button_link: string;
key_features: any[];
is_sticky: boolean;
is_no_deposit: boolean;
is_featured: boolean;
order: number;
};
predefinedBadges: PredefinedBadge[];
predefinedKeyFeatures: string[];
}>();
const breadcrumbs: BreadcrumbItem[] = [
{ title: 'Dashboard', href: '/dashboard' },
{ title: 'Bonuses', href: '/admin/bonuses' },
{ title: 'Edit', href: `/admin/bonuses/${props.bonus.id}/edit` },
];
const form = useForm({
_method: 'PUT', // Essential for multipart/form-data with PUT in Laravel
name: props.bonus.name,
description: props.bonus.description,
image_path: props.bonus.image_path,
image_file: null as File | null,
brand_color: props.bonus.brand_color || '#a855f7',
hover_color: props.bonus.hover_color || 'rgba(168, 85, 247, 0.8)',
badges: props.bonus.badges || [],
min_deposit: props.bonus.min_deposit,
max_bet: props.bonus.max_bet,
wagering: props.bonus.wagering,
free_spins: props.bonus.free_spins,
button_link: props.bonus.button_link,
key_features: props.bonus.key_features || [],
is_sticky: props.bonus.is_sticky,
is_no_deposit: props.bonus.is_no_deposit,
is_featured: props.bonus.is_featured,
order: props.bonus.order,
});
const newBadge = ref('');
const addBadge = () => {
if (newBadge.value.trim()) {
form.badges.push({ label: newBadge.value.trim(), class: 'bg-white/10' });
newBadge.value = '';
}
};
const removeBadge = (index: number) => {
form.badges.splice(index, 1);
};
const addPredefinedBadge = (badge: PredefinedBadge) => {
if (!form.badges.some(b => b.label === badge.label)) {
form.badges.push(badge);
}
};
const newFeature = ref('');
const addFeature = () => {
if (newFeature.value.trim()) {
form.key_features.push(newFeature.value.trim());
newFeature.value = '';
}
};
const removeFeature = (index: number) => {
form.key_features.splice(index, 1);
};
const addPredefinedFeature = (feature: string) => {
if (!form.key_features.includes(feature)) {
form.key_features.push(feature);
}
};
const imagePreviewUrl = ref<string | null>(props.bonus.image_path);
const handleImageFileChange = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files[0]) {
form.image_file = target.files[0];
imagePreviewUrl.value = URL.createObjectURL(target.files[0]);
form.image_path = ''; // Clear URL if file is chosen
}
};
watch(() => form.image_path, (newPath) => {
if (newPath && !form.image_file) {
imagePreviewUrl.value = newPath;
}
});
const submit = () => {
form.post(`/admin/bonuses/${props.bonus.id}`, {
onSuccess: () => {
// Handled by AdminLayout flash messages
}
});
};
</script>
<template>
<Head :title="'Edit ' + bonus.name" />
<div class="space-y-8">
<!-- Header Section -->
<div class="flex items-center justify-between gap-6 bg-[#0f172a] p-8 rounded-[32px] border border-white/5 shadow-xl">
<div class="flex items-center gap-4">
<Link href="/admin/bonuses" class="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center text-gray-400 hover:text-white hover:bg-white/10 transition">
<ArrowLeft :size="20" />
</Link>
<div>
<h1 class="text-3xl font-black italic tracking-tighter uppercase">Edit <span class="text-blue-500">Bonus</span></h1>
<p class="text-gray-500 text-xs font-bold uppercase tracking-widest">{{ bonus.name }}</p>
</div>
</div>
</div>
<form @submit.prevent="submit" class="bg-[#0f172a] rounded-[32px] border border-white/5 shadow-xl p-8 space-y-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- Left Column: Content -->
<div class="space-y-8">
<section class="space-y-6">
<h2 class="text-xl font-black italic uppercase tracking-tight text-blue-400 flex items-center gap-3">
<span class="w-8 h-[2px] bg-blue-400"></span> Main Info
</h2>
<div class="space-y-4">
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Casino Name</span>
<input v-model="form.name" type="text" class="form-input" required>
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Description / Bonus Text</span>
<textarea v-model="form.description" class="form-textarea" rows="4"></textarea>
</label>
</div>
</section>
<section class="space-y-6">
<h2 class="text-xl font-black italic uppercase tracking-tight text-purple-400 flex items-center gap-3">
<span class="w-8 h-[2px] bg-purple-400"></span> Visuals & Branding
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Brand Primary Color</span>
<div class="flex gap-3">
<input v-model="form.brand_color" type="color" class="form-color-input w-14">
<input v-model="form.brand_color" type="text" class="form-input flex-1 font-mono text-xs uppercase" placeholder="#FFFFFF">
</div>
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Hover Glow Color (RGBA)</span>
<input v-model="form.hover_color" type="text" class="form-input font-mono text-xs" placeholder="rgba(255,255,255,0.1)">
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 items-end">
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Background Image URL</span>
<input v-model="form.image_path" type="text" class="form-input" :disabled="!!form.image_file">
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Or Upload New File</span>
<input type="file" @change="handleImageFileChange" class="form-file-input" accept="image/*">
</label>
</div>
<div v-if="imagePreviewUrl" class="relative group aspect-video rounded-2xl overflow-hidden border border-white/5 bg-black/40">
<img :src="imagePreviewUrl" class="w-full h-full object-cover opacity-60" />
<div class="absolute inset-0 flex items-center justify-center">
<span class="bg-black/60 backdrop-blur-md px-4 py-2 rounded-full text-[10px] font-black uppercase tracking-widest border border-white/10">Image Preview</span>
</div>
</div>
</section>
</div>
<!-- Right Column: Stats & Features -->
<div class="space-y-8">
<section class="space-y-6">
<h2 class="text-xl font-black italic uppercase tracking-tight text-green-400 flex items-center gap-3">
<span class="w-8 h-[2px] bg-green-400"></span> Bonus Details
</h2>
<div class="grid grid-cols-2 gap-4">
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Min Deposit</span>
<div class="relative">
<DollarSign class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" :size="14" />
<input v-model="form.min_deposit" type="text" class="form-input pl-10" placeholder="20€">
</div>
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Max Bet</span>
<div class="relative">
<XSquare class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" :size="14" />
<input v-model="form.max_bet" type="text" class="form-input pl-10" placeholder="5€">
</div>
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Wagering</span>
<div class="relative">
<RotateCw class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" :size="14" />
<input v-model="form.wagering" type="text" class="form-input pl-10" placeholder="35x">
</div>
</label>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Free Spins</span>
<div class="relative">
<Award class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" :size="14" />
<input v-model="form.free_spins" type="text" class="form-input pl-10" placeholder="100 FS">
</div>
</label>
</div>
<label class="block">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] mb-2 block">Affiliate / Button Link</span>
<div class="relative">
<LinkIcon class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" :size="14" />
<input v-model="form.button_link" type="text" class="form-input pl-10" placeholder="https://...">
</div>
</label>
</section>
<section class="space-y-6">
<h2 class="text-xl font-black italic uppercase tracking-tight text-orange-400 flex items-center gap-3">
<span class="w-8 h-[2px] bg-orange-400"></span> Configuration
</h2>
<div class="flex flex-wrap gap-6">
<label class="flex items-center gap-3 group cursor-pointer">
<div class="relative w-12 h-6 bg-white/5 rounded-full border border-white/10 transition group-hover:border-orange-500/50">
<input v-model="form.is_sticky" type="checkbox" class="sr-only peer">
<div class="absolute left-1 top-1 w-4 h-4 bg-gray-500 rounded-full transition-all peer-checked:left-7 peer-checked:bg-orange-500"></div>
</div>
<span class="text-[10px] font-black uppercase tracking-widest text-gray-400 peer-checked:text-white">Sticky Bonus</span>
</label>
<label class="flex items-center gap-3 group cursor-pointer">
<div class="relative w-12 h-6 bg-white/5 rounded-full border border-white/10 transition group-hover:border-cyan-500/50">
<input v-model="form.is_no_deposit" type="checkbox" class="sr-only peer">
<div class="absolute left-1 top-1 w-4 h-4 bg-gray-500 rounded-full transition-all peer-checked:left-7 peer-checked:bg-cyan-500"></div>
</div>
<span class="text-[10px] font-black uppercase tracking-widest text-gray-400">No Deposit</span>
</label>
<label class="flex items-center gap-3 group cursor-pointer">
<div class="relative w-12 h-6 bg-white/5 rounded-full border border-white/10 transition group-hover:border-yellow-500/50">
<input v-model="form.is_featured" type="checkbox" class="sr-only peer">
<div class="absolute left-1 top-1 w-4 h-4 bg-gray-500 rounded-full transition-all peer-checked:left-7 peer-checked:bg-yellow-500"></div>
</div>
<span class="text-[10px] font-black uppercase tracking-widest text-gray-400 flex items-center gap-2"><Star :size="12" /> Super Card</span>
</label>
<label class="flex items-center gap-4 ml-auto">
<span class="text-[10px] font-black uppercase tracking-widest text-gray-500">Sort Order</span>
<input v-model="form.order" type="number" class="form-input w-20 text-center">
</label>
</div>
<!-- Badges -->
<div class="space-y-4">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] block">Status Badges</span>
<div class="flex flex-wrap gap-2 mb-2">
<div v-for="(badge, idx) in form.badges" :key="idx"
:class="['flex items-center gap-2 px-3 py-1.5 rounded-lg group hover:border-red-500/30 transition', badge.class || 'bg-white/5 border border-white/10']">
<span class="text-[10px] font-bold uppercase">{{ badge.label }}</span>
<button type="button" @click="removeBadge(idx)" class="text-gray-600 group-hover:text-red-500 transition">
<Trash2 :size="12" />
</button>
</div>
</div>
<div class="flex flex-wrap gap-2 border-t border-white/5 pt-4">
<span class="text-gray-500 text-xs font-bold uppercase w-full">Predefined:</span>
<button type="button" v-for="badge in predefinedBadges" :key="badge.label"
@click="addPredefinedBadge(badge)"
:class="['px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase transition', badge.class, form.badges.some(b => b.label === badge.label) ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-75']"
:disabled="form.badges.some(b => b.label === badge.label)">
{{ badge.label }}
</button>
</div>
<div class="flex gap-2 w-full mt-2">
<input v-model="newBadge" type="text" class="form-input flex-1 text-xs py-2" placeholder="Add Custom Badge (e.g. VIP DEAL)">
<button type="button" @click="addBadge" class="btn-secondary py-2"><Plus :size="14" /></button>
</div>
</div>
<!-- Key Features -->
<div class="space-y-4">
<span class="text-gray-500 font-black uppercase text-[10px] tracking-[0.2em] block">Key Features (Pros)</span>
<div class="space-y-2 mb-2">
<div v-for="(feature, idx) in form.key_features" :key="idx"
class="flex items-center justify-between bg-white/[0.02] border border-white/5 p-3 rounded-xl">
<span class="text-xs text-gray-300">{{ feature }}</span>
<button type="button" @click="removeFeature(idx)" class="text-gray-600 hover:text-red-500 transition">
<Trash2 :size="14" />
</button>
</div>
</div>
<div class="flex flex-wrap gap-2 border-t border-white/5 pt-4">
<span class="text-gray-500 text-xs font-bold uppercase w-full">Predefined:</span>
<button type="button" v-for="feature in predefinedKeyFeatures" :key="feature"
@click="addPredefinedFeature(feature)"
:class="['px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase bg-white/5 border border-white/10 transition', form.key_features.includes(feature) ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-75']"
:disabled="form.key_features.includes(feature)">
{{ feature }}
</button>
</div>
<div class="flex gap-2 w-full mt-2">
<input v-model="newFeature" type="text" class="form-input flex-1 text-xs py-2" placeholder="Add Custom Feature (e.g. Instant Rakeback)">
<button type="button" @click="addFeature" class="btn-secondary py-2"><Plus :size="14" /></button>
</div>
</div>
</section>
</div>
</div>
<div class="pt-10 border-t border-white/5 flex justify-end gap-4">
<Link href="/admin/bonuses" class="px-8 py-4 rounded-2xl text-xs font-black uppercase tracking-widest text-gray-500 hover:text-white transition">Cancel</Link>
<button type="submit" class="btn-primary flex items-center gap-3" :disabled="form.processing">
<Save :size="18" /> {{ form.processing ? 'Updating...' : 'Update Bonus Deal' }}
</button>
</div>
</form>
</div>
</template>
<style scoped>
@reference "../../../../css/app.css";
.form-input, .form-textarea {
@apply w-full bg-white/[0.03] border border-white/5 rounded-2xl py-4 px-6 outline-none focus:border-blue-500/50 transition focus:ring-1 focus:ring-blue-500/20 text-white placeholder-gray-600 text-sm font-medium;
}
.form-file-input {
@apply w-full text-xs text-gray-400
file:mr-4 file:py-3 file:px-6
file:rounded-2xl file:border-0
file:text-[10px] file:font-black file:uppercase file:tracking-widest
file:bg-white/5 file:text-white
hover:file:bg-white/10 transition-all duration-300;
}
.form-color-input {
@apply h-14 rounded-2xl border-none bg-white/5 cursor-pointer p-1 overflow-hidden;
}
.form-color-input::-webkit-color-swatch-wrapper { padding: 0; }
.form-color-input::-webkit-color-swatch { border: none; border-radius: 12px; }
.btn-primary {
@apply bg-gradient-to-r from-blue-600 to-purple-600 hover:scale-[1.02] active:scale-[0.98] transition-all duration-300 text-white px-10 py-4 rounded-2xl font-black uppercase tracking-widest text-xs shadow-xl shadow-blue-500/20;
}
.btn-secondary {
@apply bg-white/5 border border-white/10 text-white px-4 py-3 rounded-xl hover:bg-white/10 transition;
}
</style>
+169
View File
@@ -0,0 +1,169 @@
<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import { Gift, Plus, Search, Filter, Edit2, Trash2, Eye, EyeOff, AlertTriangle, X } from 'lucide-vue-next';
import { ref } from 'vue';
interface Bonus {
id: number;
name: string;
description: string;
is_active: boolean;
order: number;
brand_color: string;
}
defineProps<{
bonuses: Bonus[];
}>();
const isDeleteModalOpen = ref(false);
const bonusToDelete = ref<Bonus | null>(null);
const confirmDelete = (bonus: Bonus) => {
bonusToDelete.value = bonus;
isDeleteModalOpen.value = true;
};
const deleteBonus = () => {
if (bonusToDelete.value) {
router.delete(`/admin/bonuses/${bonusToDelete.value.id}`, {
onSuccess: () => {
isDeleteModalOpen.value = false;
bonusToDelete.value = null;
},
});
}
};
</script>
<template>
<Head title="Manage Bonuses" />
<div class="relative">
<!-- Delete Confirmation Modal -->
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div v-if="isDeleteModalOpen" class="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/80 backdrop-blur-sm" @click="isDeleteModalOpen = false"></div>
<div class="relative bg-[#0f172a] border border-white/10 rounded-[32px] p-10 max-w-md w-full shadow-2xl shadow-black/50">
<button @click="isDeleteModalOpen = false" class="absolute top-6 right-6 text-gray-500 hover:text-white transition">
<X :size="24" />
</button>
<div class="w-20 h-20 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-8 text-red-500">
<AlertTriangle :size="40" />
</div>
<h2 class="text-2xl font-black italic uppercase text-center mb-4 tracking-tight">Wait a <span class="text-red-500">second</span></h2>
<p class="text-gray-400 text-center mb-10 leading-relaxed">Are you sure you want to delete <span class="text-white font-bold">{{ bonusToDelete?.name }}</span>? This action cannot be undone.</p>
<div class="grid grid-cols-2 gap-4">
<button @click="isDeleteModalOpen = false" class="py-4 rounded-2xl bg-white/5 hover:bg-white/10 text-white font-black uppercase tracking-widest text-[10px] transition">Cancel</button>
<button @click="deleteBonus" class="py-4 rounded-2xl bg-red-600 hover:bg-red-700 text-white font-black uppercase tracking-widest text-[10px] transition shadow-lg shadow-red-600/20">Delete Bonus</button>
</div>
</div>
</div>
</Transition>
<div class="space-y-8">
<!-- Header Section -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#0f172a] p-8 rounded-[32px] border border-white/5 shadow-xl">
<div>
<h1 class="text-3xl font-black italic tracking-tighter uppercase mb-2">Bonus <span class="text-purple-500">Inventory</span></h1>
<p class="text-gray-400 text-sm font-medium">Manage and organize your casino deals effortlessly.</p>
</div>
<Link
href="/admin/bonuses/create"
class="bg-gradient-to-r from-purple-600 to-blue-600 hover:scale-105 transition-all duration-300 text-white px-8 py-4 rounded-2xl font-black uppercase tracking-widest text-xs flex items-center gap-3 shadow-lg shadow-purple-500/20"
>
<Plus :size="18" /> Add New Bonus
</Link>
</div>
<!-- Filters & Search -->
<div class="flex items-center gap-4">
<div class="relative flex-1">
<Search class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" :size="18" />
<input type="text" placeholder="Search bonuses..." class="w-full bg-[#0f172a] border border-white/5 rounded-2xl py-4 pl-12 pr-4 outline-none focus:border-purple-500/50 transition text-sm">
</div>
<button class="bg-[#0f172a] border border-white/5 p-4 rounded-2xl hover:bg-white/5 transition text-gray-400">
<Filter :size="18" />
</button>
</div>
<!-- Bonus Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<div
v-for="bonus in bonuses"
:key="bonus.id"
class="group bg-[#0f172a] rounded-[32px] border border-white/5 overflow-hidden hover:border-purple-500/30 transition-all duration-500 shadow-lg hover:shadow-2xl hover:shadow-purple-500/10 flex flex-col"
>
<!-- Top Section -->
<div class="p-8 flex-1">
<div class="flex justify-between items-start mb-6">
<div
class="w-12 h-12 rounded-2xl flex items-center justify-center font-black italic shadow-inner border border-white/5"
:style="{ backgroundColor: bonus.brand_color + '20', color: bonus.brand_color }"
>
{{ bonus.name.charAt(0) }}
</div>
<div class="flex gap-2">
<span
:class="[
'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest border',
bonus.is_active
? 'bg-green-500/10 text-green-400 border-green-500/20'
: 'bg-red-500/10 text-red-400 border-red-500/20'
]"
>
{{ bonus.is_active ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
<h3 class="text-xl font-black italic tracking-tight uppercase mb-2 group-hover:text-purple-400 transition">{{ bonus.name }}</h3>
<p class="text-gray-500 text-sm leading-relaxed line-clamp-2 mb-6">{{ bonus.description || 'No description provided.' }}</p>
<div class="flex items-center gap-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest border-t border-white/5 pt-6">
<span class="flex items-center gap-2"><i class="fas fa-sort"></i> Order: {{ bonus.order }}</span>
</div>
</div>
<!-- Action Bar -->
<div class="p-4 bg-white/[0.02] border-t border-white/5 grid grid-cols-2 gap-2">
<Link
:href="`/admin/bonuses/${bonus.id}/edit`"
class="flex items-center justify-center gap-2 py-3 rounded-xl bg-white/5 hover:bg-blue-500/20 hover:text-blue-400 transition-all text-xs font-bold uppercase tracking-widest"
>
<Edit2 :size="14" /> Edit
</Link>
<button
@click="confirmDelete(bonus)"
class="flex items-center justify-center gap-2 py-3 rounded-xl bg-white/5 hover:bg-red-500/20 hover:text-red-400 transition-all text-xs font-bold uppercase tracking-widest"
>
<Trash2 :size="14" /> Delete
</button>
</div>
</div>
<!-- Empty State -->
<div v-if="bonuses.length === 0" class="col-span-full bg-[#0f172a] border-2 border-dashed border-white/5 rounded-[32px] p-20 text-center">
<div class="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-6">
<Gift :size="32" class="text-gray-500" />
</div>
<h3 class="text-2xl font-black italic uppercase mb-2">No Bonuses Found</h3>
<p class="text-gray-500 mb-8 max-w-xs mx-auto text-sm">Your inventory is currently empty. Start by adding a new casino deal.</p>
<Link href="/admin/bonuses/create" class="inline-flex items-center gap-3 bg-white text-black px-8 py-4 rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-purple-500 hover:text-white transition-all shadow-xl">
<Plus :size="18" /> Create One Now
</Link>
</div>
</div>
</div>
</div>
</template>