initial commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="AGB" />
|
||||
<div class="min-h-screen bg-[#020617] text-white p-8 font-sans">
|
||||
<h1 class="text-4xl font-black italic mb-4">AGB</h1>
|
||||
<p>Allgemeine Geschäftsbedingungen.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,368 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { Plus, Trash2, Image as ImageIcon, Link as LinkIcon, Palette, Droplet, DollarSign, Percent, RotateCw, Award, CheckSquare, XSquare, Star, ArrowLeft } from 'lucide-vue-next';
|
||||
|
||||
interface PredefinedBadge {
|
||||
label: string;
|
||||
class: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
predefinedBadges: PredefinedBadge[];
|
||||
predefinedKeyFeatures: string[];
|
||||
}>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{ title: 'Dashboard', href: '/dashboard' },
|
||||
{ title: 'Bonuses', href: '/admin/bonuses' },
|
||||
{ title: 'Create', href: '/admin/bonuses/create' },
|
||||
];
|
||||
|
||||
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 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>
|
||||
<h1 class="text-3xl font-black italic tracking-tighter uppercase">Create New <span class="text-purple-500">Bonus</span></h1>
|
||||
</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 placeholder="e.g. STAKE.COM">
|
||||
</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" placeholder="Exklusiver 200% Bonus bis zu 1000€..."></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="#A855F7">
|
||||
</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(168, 85, 247, 0.8)">
|
||||
</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">
|
||||
{{ form.processing ? 'Saving...' : 'Save Bonus' }}
|
||||
</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-500
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Gift, Eye, MousePointerClick, TrendingUp, Activity, Zap, Server } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps<{
|
||||
bonuses: any[];
|
||||
totalViews: number;
|
||||
totalClicks: number;
|
||||
todayViews: number;
|
||||
todayClicks: number;
|
||||
socialClicks: {
|
||||
twitch: number;
|
||||
instagram: number;
|
||||
kick: number;
|
||||
};
|
||||
uptimeMonitors: {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
// Calculate Conversion Rate
|
||||
const conversionRate = props.totalViews > 0
|
||||
? ((props.totalClicks / props.totalViews) * 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Dashboard">
|
||||
<!-- We use FontAwesome for social icons because lucide deprecated theirs -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</Head>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<h1 class="text-2xl font-bold">Dashboard Statistiken</h1>
|
||||
|
||||
<!-- Top Level Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Views</p>
|
||||
<h3 class="text-3xl font-bold mt-1">{{ totalViews }}</h3>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 text-blue-500 rounded-xl">
|
||||
<Eye :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-green-500 flex items-center font-medium">
|
||||
<TrendingUp :size="16" class="mr-1" /> +{{ todayViews }}
|
||||
</span>
|
||||
<span class="text-gray-400 ml-2">heute</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Clicks</p>
|
||||
<h3 class="text-3xl font-bold mt-1">{{ totalClicks }}</h3>
|
||||
</div>
|
||||
<div class="p-3 bg-purple-50 dark:bg-purple-900/20 text-purple-500 rounded-xl">
|
||||
<MousePointerClick :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-green-500 flex items-center font-medium">
|
||||
<TrendingUp :size="16" class="mr-1" /> +{{ todayClicks }}
|
||||
</span>
|
||||
<span class="text-gray-400 ml-2">heute</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Conversion Rate</p>
|
||||
<h3 class="text-3xl font-bold mt-1">{{ conversionRate }}%</h3>
|
||||
</div>
|
||||
<div class="p-3 bg-green-50 dark:bg-green-900/20 text-green-500 rounded-xl">
|
||||
<Activity :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-gray-400">Clicks / Views</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Aktive Deals</p>
|
||||
<h3 class="text-3xl font-bold mt-1">{{ bonuses.filter(b => b.is_active).length }}</h3>
|
||||
</div>
|
||||
<div class="p-3 bg-orange-50 dark:bg-orange-900/20 text-orange-500 rounded-xl">
|
||||
<Gift :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<Link href="/admin/bonuses" class="text-blue-500 hover:text-blue-600 font-medium">Alle verwalten →</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Stats -->
|
||||
<h2 class="text-xl font-bold mt-8 mb-4">Social Media Klicks</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-4 bg-purple-100 dark:bg-purple-900/30 text-[#9146FF] rounded-xl">
|
||||
<i class="fab fa-twitch text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Twitch</p>
|
||||
<h3 class="text-2xl font-bold">{{ socialClicks.twitch }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-4 bg-pink-100 dark:bg-pink-900/30 text-pink-600 rounded-xl">
|
||||
<i class="fab fa-instagram text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Instagram</p>
|
||||
<h3 class="text-2xl font-bold">{{ socialClicks.instagram }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-4 bg-green-100 dark:bg-green-900/30 text-[#53FC18] rounded-xl">
|
||||
<Zap :size="24" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Kick</p>
|
||||
<h3 class="text-2xl font-bold">{{ socialClicks.kick }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Deals Stats Table -->
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-xl font-bold mt-8 mb-4">Deal Performance</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-900 border-b border-gray-100 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<th class="p-4">Casino</th>
|
||||
<th class="p-4 text-center">Views</th>
|
||||
<th class="p-4 text-center">Clicks</th>
|
||||
<th class="p-4 text-center">Conv. Rate</th>
|
||||
<th class="p-4 text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<tr v-for="bonus in bonuses" :key="bonus.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition">
|
||||
<td class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg overflow-hidden flex items-center justify-center" :style="{ backgroundColor: bonus.hover_color ? bonus.hover_color + '20' : '#e5e7eb' }">
|
||||
<img v-if="bonus.image_path" :src="bonus.image_path" class="w-full h-full object-cover" alt="Bonus" />
|
||||
<Gift v-else :size="20" :color="bonus.brand_color || '#9ca3af'" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{{ bonus.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-4 text-center font-medium">{{ bonus.views_count }}</td>
|
||||
<td class="p-4 text-center font-medium">{{ bonus.clicks_count }}</td>
|
||||
<td class="p-4 text-center">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200">
|
||||
{{ bonus.views_count > 0 ? ((bonus.clicks_count / bonus.views_count) * 100).toFixed(1) : '0.0' }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-4 text-right">
|
||||
<span v-if="bonus.is_active" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
Aktiv
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||
Inaktiv
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="bonuses.length === 0">
|
||||
<td colspan="5" class="p-8 text-center text-gray-500">
|
||||
Keine Deals gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uptime Monitors -->
|
||||
<div class="lg:col-span-1">
|
||||
<h2 class="text-xl font-bold mt-8 mb-4">Uptime Kuma Status</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
|
||||
<ul class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<li v-for="monitor in uptimeMonitors" :key="monitor.id" class="p-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Server :size="20" class="text-gray-400" />
|
||||
<span class="font-medium">{{ monitor.name }}</span>
|
||||
<span class="text-xs text-gray-500">#{{ monitor.id }}</span>
|
||||
</div>
|
||||
<span v-if="monitor.status === 'up'" class="flex items-center gap-1.5 text-sm font-medium text-green-600 dark:text-green-400">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500"></span> UP
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1.5 text-sm font-medium text-red-600 dark:text-red-400">
|
||||
<span class="w-2 h-2 rounded-full bg-red-500"></span> DOWN
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="!uptimeMonitors || uptimeMonitors.length === 0" class="p-8 text-center text-gray-500">
|
||||
Keine Statusdaten verfügbar oder API nicht erreichbar.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Datenschutz" />
|
||||
<div class="min-h-screen bg-[#020617] text-white p-8 font-sans">
|
||||
<h1 class="text-4xl font-black italic mb-4">Datenschutz</h1>
|
||||
<p>Hier steht die Datenschutzerklärung.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Impressum" />
|
||||
<div class="min-h-screen bg-[#020617] text-white p-8 font-sans">
|
||||
<h1 class="text-4xl font-black italic mb-4">Impressum</h1>
|
||||
<p>Angaben gemäß § 5 TMG.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Verantwortungsbewusstes Spielen" />
|
||||
<div class="min-h-screen bg-[#020617] text-white p-8 font-sans">
|
||||
<h1 class="text-4xl font-black italic mb-4">Verantwortungsbewusstes Spielen</h1>
|
||||
<p>Glücksspiel kann süchtig machen. Bitte spiele verantwortungsbewusst.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { onMounted, ref, onUnmounted } from 'vue';
|
||||
|
||||
import BackgroundStars from '@/components/Welcome/BackgroundStars.vue';
|
||||
import HeroCanvas from '@/components/Welcome/HeroCanvas.vue';
|
||||
import BonusSection from '@/components/Welcome/BonusSection.vue';
|
||||
import SocialSection from '@/components/Welcome/SocialSection.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
canRegister: boolean;
|
||||
bonuses: any[];
|
||||
}>();
|
||||
|
||||
const isScrolled = ref(false);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
isScrolled.value = window.scrollY > 80;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Welcome">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<div
|
||||
class="bratanbonus-theme relative min-h-screen overflow-x-hidden scroll-smooth font-sans text-white"
|
||||
>
|
||||
<BackgroundStars />
|
||||
|
||||
<nav
|
||||
:class="[
|
||||
'fixed z-100 flex w-full items-center justify-between border-b border-transparent px-8 py-5 shadow-lg transition-all duration-400 ease-in-out',
|
||||
isScrolled
|
||||
? 'border-white/10 bg-[#0f172a]/70 pt-3 pb-3 backdrop-blur-md hover:bg-gradient-to-r hover:from-purple-500 hover:to-blue-500 hover:backdrop-blur-none'
|
||||
: 'bg-gradient-to-r from-purple-500 to-blue-500',
|
||||
]"
|
||||
>
|
||||
<div class="text-2xl font-black tracking-tighter uppercase italic">
|
||||
BRATANBONUS.NET
|
||||
</div>
|
||||
<div
|
||||
class="hidden items-center gap-10 text-xs font-bold tracking-widest uppercase md:flex"
|
||||
>
|
||||
<a href="#bonuses" class="transition hover:opacity-70"
|
||||
>Bonus Deals</a
|
||||
>
|
||||
<a href="#find-me" class="transition hover:opacity-70"
|
||||
>Community</a
|
||||
>
|
||||
|
||||
<template v-if="$page.props.auth.user">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
class="rounded-full bg-white px-6 py-2 text-black transition hover:scale-105"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Link
|
||||
href="/login"
|
||||
class="rounded-full bg-white px-6 py-2 text-black transition hover:scale-105"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
v-if="canRegister"
|
||||
href="/register"
|
||||
class="rounded-full border border-white/30 bg-white/20 px-6 py-2 text-white transition hover:scale-105 hover:bg-white hover:text-black"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<HeroCanvas />
|
||||
|
||||
<div
|
||||
class="h-[1px] bg-gradient-to-r from-transparent via-purple-500/30 to-blue-500/30 to-transparent"
|
||||
></div>
|
||||
|
||||
<BonusSection :bonuses="bonuses" />
|
||||
|
||||
<div
|
||||
class="h-[1px] bg-gradient-to-r from-transparent via-purple-500/30 to-blue-500/30 to-transparent"
|
||||
></div>
|
||||
|
||||
<SocialSection />
|
||||
|
||||
<footer
|
||||
class="relative z-10 border-t border-white/5 bg-black/50 py-16 text-center"
|
||||
>
|
||||
<div class="container mx-auto px-6">
|
||||
<div
|
||||
class="mb-8 text-2xl font-black text-white italic opacity-60"
|
||||
>
|
||||
BRATANBONUS.NET
|
||||
</div>
|
||||
<div
|
||||
class="mb-10 flex flex-wrap justify-center gap-8 text-sm text-gray-500"
|
||||
>
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
class="transition hover:text-white"
|
||||
>Datenschutz</Link
|
||||
>
|
||||
<Link href="/agb" class="transition hover:text-white"
|
||||
>AGB</Link
|
||||
>
|
||||
<Link href="/impressum" class="transition hover:text-white"
|
||||
>Impressum</Link
|
||||
>
|
||||
<Link
|
||||
href="/verantwortungsbewusstes-spielen"
|
||||
class="transition hover:text-white"
|
||||
>Verantwortungsbewusstes Spielen</Link
|
||||
>
|
||||
</div>
|
||||
<p class="mb-4 text-xs tracking-widest text-gray-700 uppercase">
|
||||
© 2026 | Alle Rechte vorbehalten
|
||||
</p>
|
||||
<p
|
||||
class="mx-auto max-w-lg rounded-xl border border-red-500/20 p-4 text-[10px] text-red-500/50"
|
||||
>
|
||||
Glücksspiel kann süchtig machen. Bitte spiele
|
||||
verantwortungsbewusst. Teilnahme erst ab 18 Jahren.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Base theme variables to override whatever tailwind might inherit */
|
||||
.bratanbonus-theme {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import PasswordInput from '@/components/PasswordInput.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { store } from '@/routes/password/confirm';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Confirm your password',
|
||||
description:
|
||||
'This is a secure area of the application. Please confirm your password before continuing.',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Confirm password" />
|
||||
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
reset-on-success
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="processing"
|
||||
data-test="confirm-password-button"
|
||||
>
|
||||
<Spinner v-if="processing" />
|
||||
Confirm password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import TextLink from '@/components/TextLink.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { login } from '@/routes';
|
||||
import { email } from '@/routes/password';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Forgot password',
|
||||
description: 'Enter your email to receive a password reset link',
|
||||
},
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
status?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Forgot password" />
|
||||
|
||||
<div
|
||||
v-if="status"
|
||||
class="mb-4 text-center text-sm font-medium text-green-600"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Form v-bind="email.form()" v-slot="{ errors, processing }">
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError :message="errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="my-6 flex items-center justify-start">
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="processing"
|
||||
data-test="email-password-reset-link-button"
|
||||
>
|
||||
<Spinner v-if="processing" />
|
||||
Email password reset link
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div class="space-x-1 text-center text-sm text-muted-foreground">
|
||||
<span>Or, return to</span>
|
||||
<TextLink :href="login()">log in</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import PasswordInput from '@/components/PasswordInput.vue';
|
||||
import TextLink from '@/components/TextLink.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { store } from '@/routes/login';
|
||||
import { request } from '@/routes/password';
|
||||
// Hintergrund-Komponente importieren
|
||||
import BackgroundStars from '@/components/Welcome/BackgroundStars.vue';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Bratanbonus.net Admin',
|
||||
description: 'Bitte logge dich ein, um fortzufahren.',
|
||||
},
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
status?: string;
|
||||
canResetPassword: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Log in" />
|
||||
|
||||
<div
|
||||
class="relative flex min-h-screen w-full flex-col items-center justify-center overflow-hidden bg-gray-950 p-4"
|
||||
>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<BackgroundStars />
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 mx-auto w-full max-w-[440px]">
|
||||
<div
|
||||
v-if="status"
|
||||
class="mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-center text-sm font-medium text-green-400 backdrop-blur-md"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div
|
||||
class="absolute -inset-1 rounded-[2rem] bg-gradient-to-r from-purple-600 via-blue-600 to-cyan-500 opacity-20 blur transition duration-1000 group-hover:opacity-30"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden rounded-[1.75rem] border border-white/10 bg-gray-950/80 p-8 shadow-2xl backdrop-blur-2xl sm:p-10"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 right-0 -mt-12 -mr-12 h-24 w-24 rounded-full bg-purple-500/10 blur-[50px]"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-0 left-0 -mb-12 -ml-12 h-24 w-24 rounded-full bg-blue-500/10 blur-[50px]"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-10 text-center">
|
||||
<h1
|
||||
class="text-2xl font-bold tracking-tight text-white"
|
||||
>
|
||||
Admin Login
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-gray-400">
|
||||
Bratanbonus.net Control Panel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
:reset-on-success="['password']"
|
||||
v-slot="{ errors, processing }"
|
||||
class="flex flex-col gap-5"
|
||||
>
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
for="email"
|
||||
class="ml-1 text-[11px] font-bold tracking-[0.15em] text-gray-500 uppercase"
|
||||
>
|
||||
Email Adresse
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autofocus
|
||||
:tabindex="1"
|
||||
autocomplete="email"
|
||||
placeholder="admin@bratanbonus.net"
|
||||
class="h-11 rounded-xl border-white/10 bg-white/[0.03] text-white transition-all duration-300 placeholder:text-gray-600 hover:bg-white/[0.06] focus:border-purple-500/40 focus:ring-2 focus:ring-purple-500/40"
|
||||
/>
|
||||
<InputError :message="errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center justify-between px-1"
|
||||
>
|
||||
<Label
|
||||
for="password"
|
||||
class="text-[11px] font-bold tracking-[0.15em] text-gray-500 uppercase"
|
||||
>
|
||||
Passwort
|
||||
</Label>
|
||||
<TextLink
|
||||
v-if="canResetPassword"
|
||||
:href="request()"
|
||||
class="text-[10px] font-bold tracking-wider text-purple-400 uppercase transition-colors hover:text-purple-300"
|
||||
:tabindex="5"
|
||||
>
|
||||
Vergessen?
|
||||
</TextLink>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
:tabindex="2"
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
class="h-11 rounded-xl border-white/10 bg-white/[0.03] text-white transition-all duration-300 placeholder:text-gray-600 hover:bg-white/[0.06] focus:border-purple-500/40 focus:ring-2 focus:ring-purple-500/40"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center px-1 pt-1">
|
||||
<Label
|
||||
for="remember"
|
||||
class="group/check flex cursor-pointer items-center"
|
||||
>
|
||||
<Checkbox
|
||||
id="remember"
|
||||
name="remember"
|
||||
:tabindex="3"
|
||||
class="rounded-md border-white/20 bg-white/5 transition-transform active:scale-90 data-[state=checked]:border-purple-600 data-[state=checked]:bg-purple-600"
|
||||
/>
|
||||
<span
|
||||
class="ml-3 text-xs font-semibold tracking-wide text-gray-400 uppercase transition-colors group-hover/check:text-gray-200"
|
||||
>
|
||||
Stay logged in
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="group/btn relative mt-6 h-11 w-full overflow-hidden rounded-xl bg-white text-[11px] font-bold tracking-[0.2em] text-black uppercase transition-all duration-300 hover:shadow-[0_0_25px_rgba(168,85,247,0.4)] active:scale-[0.97]"
|
||||
:tabindex="4"
|
||||
:disabled="processing"
|
||||
data-test="login-button"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 opacity-0 transition-opacity duration-300 group-hover/btn:opacity-100"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="relative z-10 flex items-center justify-center gap-2 transition-colors duration-300 group-hover/btn:text-white"
|
||||
>
|
||||
<Spinner
|
||||
v-if="processing"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<span v-else>Authorize</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Die Animation gilt nur für die Login-Karte */
|
||||
.mx-auto {
|
||||
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import PasswordInput from '@/components/PasswordInput.vue';
|
||||
import TextLink from '@/components/TextLink.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { login } from '@/routes';
|
||||
import { store } from '@/routes/register';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Create an account',
|
||||
description: 'Enter your details below to create your account',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Register" />
|
||||
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
:reset-on-success="['password', 'password_confirmation']"
|
||||
v-slot="{ errors, processing }"
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
:tabindex="1"
|
||||
autocomplete="name"
|
||||
name="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
:tabindex="2"
|
||||
autocomplete="email"
|
||||
name="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError :message="errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
:tabindex="3"
|
||||
autocomplete="new-password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
required
|
||||
:tabindex="4"
|
||||
autocomplete="new-password"
|
||||
name="password_confirmation"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError :message="errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-2 w-full"
|
||||
tabindex="5"
|
||||
:disabled="processing"
|
||||
data-test="register-user-button"
|
||||
>
|
||||
<Spinner v-if="processing" />
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<TextLink
|
||||
:href="login()"
|
||||
class="underline underline-offset-4"
|
||||
:tabindex="6"
|
||||
>Log in</TextLink
|
||||
>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import PasswordInput from '@/components/PasswordInput.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { update } from '@/routes/password';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Reset password',
|
||||
description: 'Please enter your new password below',
|
||||
},
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
email: string;
|
||||
}>();
|
||||
|
||||
const inputEmail = ref(props.email);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Reset password" />
|
||||
|
||||
<Form
|
||||
v-bind="update.form()"
|
||||
:transform="(data) => ({ ...data, token, email })"
|
||||
:reset-on-success="['password', 'password_confirmation']"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
v-model="inputEmail"
|
||||
class="mt-1 block w-full"
|
||||
readonly
|
||||
/>
|
||||
<InputError :message="errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
class="mt-1 block w-full"
|
||||
autofocus
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation"> Confirm password </Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
autocomplete="new-password"
|
||||
class="mt-1 block w-full"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError :message="errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-4 w-full"
|
||||
:disabled="processing"
|
||||
data-test="reset-password-button"
|
||||
>
|
||||
<Spinner v-if="processing" />
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head, setLayoutProps } from '@inertiajs/vue3';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from '@/components/ui/input-otp';
|
||||
import { store } from '@/routes/two-factor/login';
|
||||
import type { TwoFactorConfigContent } from '@/types';
|
||||
|
||||
const authConfigContent = computed<TwoFactorConfigContent>(() => {
|
||||
if (showRecoveryInput.value) {
|
||||
return {
|
||||
title: 'Recovery code',
|
||||
description:
|
||||
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||
buttonText: 'login using an authentication code',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Authentication code',
|
||||
description:
|
||||
'Enter the authentication code provided by your authenticator application.',
|
||||
buttonText: 'login using a recovery code',
|
||||
};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
setLayoutProps({
|
||||
title: authConfigContent.value.title,
|
||||
description: authConfigContent.value.description,
|
||||
});
|
||||
});
|
||||
|
||||
const showRecoveryInput = ref<boolean>(false);
|
||||
|
||||
const toggleRecoveryMode = (clearErrors: () => void): void => {
|
||||
showRecoveryInput.value = !showRecoveryInput.value;
|
||||
clearErrors();
|
||||
code.value = '';
|
||||
};
|
||||
|
||||
const code = ref<string>('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Two-factor authentication" />
|
||||
|
||||
<div class="space-y-6">
|
||||
<template v-if="!showRecoveryInput">
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
class="space-y-4"
|
||||
reset-on-error
|
||||
@error="code = ''"
|
||||
#default="{ errors, processing, clearErrors }"
|
||||
>
|
||||
<input type="hidden" name="code" :value="code" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center space-y-3 text-center"
|
||||
>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<InputOTP
|
||||
id="otp"
|
||||
v-model="code"
|
||||
:maxlength="6"
|
||||
:disabled="processing"
|
||||
autofocus
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot
|
||||
v-for="index in 6"
|
||||
:key="index"
|
||||
:index="index - 1"
|
||||
/>
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
<InputError :message="errors.code" />
|
||||
</div>
|
||||
<Button type="submit" class="w-full" :disabled="processing"
|
||||
>Continue</Button
|
||||
>
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
<span>or you can </span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
@click="() => toggleRecoveryMode(clearErrors)"
|
||||
>
|
||||
{{ authConfigContent.buttonText }}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
class="space-y-4"
|
||||
reset-on-error
|
||||
#default="{ errors, processing, clearErrors }"
|
||||
>
|
||||
<Input
|
||||
name="recovery_code"
|
||||
type="text"
|
||||
placeholder="Enter recovery code"
|
||||
:autofocus="showRecoveryInput"
|
||||
required
|
||||
/>
|
||||
<InputError :message="errors.recovery_code" />
|
||||
<Button type="submit" class="w-full" :disabled="processing"
|
||||
>Continue</Button
|
||||
>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
<span>or you can </span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
@click="() => toggleRecoveryMode(clearErrors)"
|
||||
>
|
||||
{{ authConfigContent.buttonText }}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import TextLink from '@/components/TextLink.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { logout } from '@/routes';
|
||||
import { send } from '@/routes/verification';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
title: 'Verify email',
|
||||
description:
|
||||
'Please verify your email address by clicking on the link we just emailed to you.',
|
||||
},
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
status?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Email verification" />
|
||||
|
||||
<div
|
||||
v-if="status === 'verification-link-sent'"
|
||||
class="mb-4 text-center text-sm font-medium text-green-600"
|
||||
>
|
||||
A new verification link has been sent to the email address you provided
|
||||
during registration.
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-bind="send.form()"
|
||||
class="space-y-6 text-center"
|
||||
v-slot="{ processing }"
|
||||
>
|
||||
<Button :disabled="processing" variant="secondary">
|
||||
<Spinner v-if="processing" />
|
||||
Resend verification email
|
||||
</Button>
|
||||
|
||||
<TextLink :href="logout()" as="button" class="mx-auto block text-sm">
|
||||
Log out
|
||||
</TextLink>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import AppearanceTabs from '@/components/AppearanceTabs.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import { edit } from '@/routes/appearance';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Appearance settings',
|
||||
href: edit(),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Appearance settings" />
|
||||
|
||||
<h1 class="sr-only">Appearance settings</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Appearance settings"
|
||||
description="Update your account's appearance settings"
|
||||
/>
|
||||
<AppearanceTabs />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
|
||||
import { computed } from 'vue';
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
import DeleteUser from '@/components/DeleteUser.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { edit } from '@/routes/profile';
|
||||
import { send } from '@/routes/verification';
|
||||
|
||||
type Props = {
|
||||
mustVerifyEmail: boolean;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Profile settings',
|
||||
href: edit(),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
const user = computed(() => page.props.auth.user);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Profile settings" />
|
||||
|
||||
<h1 class="sr-only">Profile settings</h1>
|
||||
|
||||
<div class="flex flex-col space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Profile information"
|
||||
description="Update your name and email address"
|
||||
/>
|
||||
|
||||
<Form
|
||||
v-bind="ProfileController.update.form()"
|
||||
class="space-y-6"
|
||||
v-slot="{ errors, processing, recentlySuccessful }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
class="mt-1 block w-full"
|
||||
name="name"
|
||||
:default-value="user.name"
|
||||
required
|
||||
autocomplete="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError class="mt-2" :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
name="email"
|
||||
:default-value="user.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
<InputError class="mt-2" :message="errors.email" />
|
||||
</div>
|
||||
|
||||
<div v-if="mustVerifyEmail && !user.email_verified_at">
|
||||
<p class="-mt-4 text-sm text-muted-foreground">
|
||||
Your email address is unverified.
|
||||
<Link
|
||||
:href="send()"
|
||||
as="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
>
|
||||
Click here to resend the verification email.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600"
|
||||
>
|
||||
A new verification link has been sent to your email address.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button :disabled="processing" data-test="update-profile-button"
|
||||
>Save</Button
|
||||
>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-show="recentlySuccessful"
|
||||
class="text-sm text-neutral-600"
|
||||
>
|
||||
Saved.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<DeleteUser />
|
||||
</template>
|
||||
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import { ShieldCheck } from 'lucide-vue-next';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import PasswordInput from '@/components/PasswordInput.vue';
|
||||
import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.vue';
|
||||
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
||||
import { edit } from '@/routes/security';
|
||||
import { disable, enable } from '@/routes/two-factor';
|
||||
|
||||
type Props = {
|
||||
canManageTwoFactor?: boolean;
|
||||
requiresConfirmation?: boolean;
|
||||
twoFactorEnabled?: boolean;
|
||||
};
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
canManageTwoFactor: false,
|
||||
requiresConfirmation: false,
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Security settings',
|
||||
href: edit(),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { hasSetupData, clearTwoFactorAuthData } = useTwoFactorAuth();
|
||||
const showSetupModal = ref<boolean>(false);
|
||||
|
||||
onUnmounted(() => clearTwoFactorAuthData());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Security settings" />
|
||||
|
||||
<h1 class="sr-only">Security settings</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Update password"
|
||||
description="Ensure your account is using a long, random password to stay secure"
|
||||
/>
|
||||
|
||||
<Form
|
||||
v-bind="SecurityController.update.form()"
|
||||
:options="{
|
||||
preserveScroll: true,
|
||||
}"
|
||||
reset-on-success
|
||||
:reset-on-error="[
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'current_password',
|
||||
]"
|
||||
class="space-y-6"
|
||||
v-slot="{ errors, processing, recentlySuccessful }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label for="current_password">Current password</Label>
|
||||
<PasswordInput
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
placeholder="Current password"
|
||||
/>
|
||||
<InputError :message="errors.current_password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">New password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
placeholder="New password"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError :message="errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
:disabled="processing"
|
||||
data-test="update-password-button"
|
||||
>
|
||||
Save password
|
||||
</Button>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-show="recentlySuccessful"
|
||||
class="text-sm text-neutral-600"
|
||||
>
|
||||
Saved.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div v-if="canManageTwoFactor" class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Two-factor authentication"
|
||||
description="Manage your two-factor authentication settings"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!twoFactorEnabled"
|
||||
class="flex flex-col items-start justify-start space-y-4"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
When you enable two-factor authentication, you will be prompted
|
||||
for a secure pin during login. This pin can be retrieved from a
|
||||
TOTP-supported application on your phone.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Button v-if="hasSetupData" @click="showSetupModal = true">
|
||||
<ShieldCheck />Continue setup
|
||||
</Button>
|
||||
<Form
|
||||
v-else
|
||||
v-bind="enable.form()"
|
||||
@success="showSetupModal = true"
|
||||
#default="{ processing }"
|
||||
>
|
||||
<Button type="submit" :disabled="processing">
|
||||
Enable 2FA
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-start justify-start space-y-4">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
You will be prompted for a secure, random pin during login,
|
||||
which you can retrieve from the TOTP-supported application on
|
||||
your phone.
|
||||
</p>
|
||||
|
||||
<div class="relative inline">
|
||||
<Form v-bind="disable.form()" #default="{ processing }">
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<TwoFactorRecoveryCodes />
|
||||
</div>
|
||||
|
||||
<TwoFactorSetupModal
|
||||
v-model:isOpen="showSetupModal"
|
||||
:requiresConfirmation="requiresConfirmation"
|
||||
:twoFactorEnabled="twoFactorEnabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user