Initialer Laravel Commit für BetiX
Some checks failed
linter / quality (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-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import { onMounted, nextTick } from 'vue';
import InputError from '@/components/InputError.vue';
import AuthLayout from '@/layouts/AuthLayout.vue';
import { store } from '@/routes/password/confirm';
onMounted(() => {
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
});
</script>
<template>
<AuthLayout>
<Head title="Passwort bestätigen" />
<div class="wrap">
<div class="main-panel">
<header class="page-head">
<div class="head-flex">
<div class="title-group">
<div class="title">Bestätigung erforderlich</div>
<p class="subtitle">Bitte bestätige dein Passwort, um fortzufahren</p>
</div>
<div class="security-badge">
<i data-lucide="lock"></i>
<span>Secure</span>
</div>
</div>
</header>
<Form v-bind="store.form()" reset-on-success v-slot="{ errors, processing }">
<div class="form-body">
<label class="field" for="password">
<span class="lbl">Passwort</span>
<div class="input-wrapper">
<i data-lucide="key-round"></i>
<input
id="password"
name="password"
type="password"
class="inp"
required
autocomplete="current-password"
autofocus
placeholder="••••••••"
/>
</div>
<InputError :message="errors.password" />
</label>
<div class="actions">
<button class="btn" type="submit" :disabled="processing" data-test="confirm-password-button">
<span v-if="processing" class="spinner" />
<span>{{ processing ? 'Wird bestätigt…' : 'Passwort bestätigen' }}</span>
<i v-if="!processing" data-lucide="arrow-right" style="width:14px"></i>
</button>
</div>
<div class="muted hint">
<i data-lucide="shield-check"></i>
<span>Sicherer Bereich. Deine Eingabe ist verschlüsselt.</span>
</div>
</div>
</Form>
</div>
</div>
</AuthLayout>
</template>
<style scoped>
:global(:root) { --bg-card:#0a0a0a; --border:#151515; --cyan:#00f2ff; --magenta:#ff007a; --green:#00ff9d; }
.wrap { width: 100%; }
.main-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.6); animation: fade-in 0.6s cubic-bezier(0.2,0,0,1); }
.page-head { padding: 22px 26px; border-bottom: 1px solid var(--border); background: linear-gradient(to right, rgba(0,242,255,0.03), transparent); }
.head-flex { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.title { font-size: 13px; font-weight: 900; color: #fff; letter-spacing: 2px; text-transform: uppercase; }
.subtitle { color: #555; font-size: 11px; margin-top: 4px; font-weight: 600; }
.security-badge { display:flex; align-items:center; gap:6px; color: var(--green); background: rgba(0,255,157,0.05); padding:5px 12px; border-radius:50px; border:1px solid rgba(0,255,157,0.1); font-size:9px; font-weight:900; text-transform:uppercase; letter-spacing:1px; }
.security-badge i { width: 12px; height: 12px; }
.form-body { padding: 24px 26px; display: grid; gap: 20px; }
.field { display: grid; gap: 8px; }
.lbl { display:block; font-size:10px; font-weight:900; color:#555; text-transform:uppercase; letter-spacing:1px; }
.input-wrapper { position: relative; display:flex; align-items:center; }
.input-wrapper i { position: absolute; left: 14px; width: 16px; color: #444; pointer-events: none; transition: .3s; }
.inp { width: 100%; background:#000; border:1px solid var(--border); color:#fff; padding:14px 14px 14px 44px; border-radius:12px; font-size:13px; transition:.3s cubic-bezier(0.2,0,0,1); }
.inp:focus { border-color: var(--cyan); outline: none; box-shadow: 0 0 15px rgba(0,242,255,0.1); background:#050505; }
.inp:focus + i { color: var(--cyan); }
.actions { display:flex; gap:10px; margin-top: 5px; }
.btn { width: 100%; background: var(--cyan); color:#000; border:none; border-radius:12px; padding:14px 16px; font-size:12px; font-weight:900; text-transform:uppercase; letter-spacing:1px; cursor:pointer; transition:.3s; box-shadow: 0 0 20px rgba(0,242,255,0.2); display:flex; align-items:center; justify-content:center; gap:10px; }
.btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 22px rgba(0,242,255,0.35); }
.btn:disabled { opacity: .6; cursor: not-allowed; }
.spinner { width: 16px; height: 16px; border: 2px solid rgba(0,0,0,0.1); border-top-color:#000; border-radius: 50%; animation: spin .8s linear infinite; }
.muted { color:#9aa0a6; font-size:11px; display:flex; align-items:center; gap:8px; justify-content: center; opacity: 0.7; }
.hint i { width: 14px; color:#444; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 560px) {
.page-head { padding: 18px 20px; }
.form-body { padding: 18px 20px; }
}
</style>

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { ChevronLeft } from 'lucide-vue-next';
import InputError from '@/components/InputError.vue';
import TextLink from '@/components/TextLink.vue';
import Button from '@/components/ui/button.vue';
import Input from '@/components/ui/input.vue';
import Label from '@/components/ui/label.vue';
import Spinner from '@/components/ui/spinner.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
defineProps<{
status?: string;
}>();
const form = useForm({
email: '',
});
const submit = () => {
form.post('/forgot-password');
};
</script>
<template>
<UserLayout>
<Head title="Forgot Password" />
<div class="login-container">
<!-- Animated Background -->
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="login-card">
<div class="card-header">
<h1 class="title">RESET <span class="highlight">PASSWORD</span></h1>
<p class="subtitle">Enter your email to recover access</p>
</div>
<div v-if="status" class="mb-4 text-center text-sm font-medium text-green-500 bg-green-500/10 p-2 rounded border border-green-500/20">
{{ status }}
</div>
<form @submit.prevent="submit" class="form-content">
<div class="input-group">
<Label for="email">Email Address</Label>
<Input
id="email"
type="email"
v-model="form.email"
required
autofocus
placeholder="you@example.com"
/>
<InputError :message="form.errors.email" />
</div>
<div class="mt-6">
<Button
type="submit"
class="w-full h-12 text-base font-bold tracking-widest uppercase neon-button relative overflow-hidden"
:disabled="form.processing"
>
<div v-if="form.processing" class="absolute inset-0 bg-black/20 flex items-center justify-center">
<Spinner class="w-5 h-5" />
</div>
<span :class="{ 'opacity-0': form.processing }">Send Reset Link</span>
</Button>
</div>
<div class="text-center mt-6">
<TextLink href="/login" class="text-[#888] hover:text-white transition-colors flex items-center justify-center gap-2">
<ChevronLeft class="w-4 h-4" /> Back to Login
</TextLink>
</div>
</form>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* Container & Layout */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 100px);
padding: 20px;
position: relative;
overflow: hidden;
}
.login-card {
width: 100%;
max-width: 450px;
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 40px;
position: relative;
z-index: 10;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Header */
.card-header {
text-align: center;
margin-bottom: 30px;
}
.title {
font-size: 1.8rem;
font-weight: 900;
color: white;
letter-spacing: 2px;
margin: 0;
}
.highlight {
color: #ff007a;
text-shadow: 0 0 20px rgba(255, 0, 122, 0.5);
}
.subtitle {
color: #888;
font-size: 0.9rem;
margin-top: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Form Content */
.form-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Neon Button */
.neon-button {
background: linear-gradient(90deg, #ff007a, #be005b);
border: none;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.neon-button:hover {
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(255, 0, 122, 0.6);
filter: brightness(1.1);
}
.neon-button:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
/* Background Orbs */
.glow-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
z-index: 0;
animation: float 10s infinite ease-in-out;
}
.orb-1 {
width: 300px;
height: 300px;
background: #ff007a;
top: -50px;
left: -100px;
}
.orb-2 {
width: 400px;
height: 400px;
background: #00f2ff;
bottom: -100px;
right: -100px;
animation-delay: -5s;
}
/* Animations */
@keyframes slideUp {
from { opacity: 0; transform: translateY(40px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(20px, 30px); }
}
</style>

View File

@@ -0,0 +1,227 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import LoginForm from '@/components/auth/LoginForm.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
defineProps<{
status?: string;
canResetPassword: boolean;
canRegister: boolean;
}>();
</script>
<template>
<UserLayout>
<Head title="Login" />
<div class="lp-wrap">
<!-- Ambient background -->
<div class="lp-bg">
<div class="orb orb-a"></div>
<div class="orb orb-b"></div>
<div class="orb orb-c"></div>
<div class="grid-overlay"></div>
</div>
<div class="lp-card">
<!-- Glowing top bar -->
<div class="card-glow-bar"></div>
<!-- Brand / Logo area -->
<div class="lp-brand">
<div class="brand-diamond">
<span class="diamond-icon"></span>
</div>
<h1 class="brand-title">WELCOME <span class="brand-accent">BACK</span></h1>
<p class="brand-sub">Sign in to continue playing</p>
</div>
<!-- Divider -->
<div class="lp-divider">
<span class="divider-dot"></span>
<span class="divider-line"></span>
<span class="divider-text">Your Account</span>
<span class="divider-line"></span>
<span class="divider-dot"></span>
</div>
<!-- Form -->
<LoginForm :status="status">
<template #forgot-password>
<Link v-if="canResetPassword" href="/forgot-password" class="forgot-link">
Forgot password?
</Link>
</template>
</LoginForm>
<!-- Footer -->
<div class="lp-footer" v-if="canRegister">
<span class="footer-text">New here?</span>
<button
class="reg-link"
@click="() => window.dispatchEvent(new Event('require-register'))"
>
Create account
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</div>
<!-- Trust badges -->
<div class="trust-row">
<div class="trust-badge"><span class="trust-icon">🔒</span> SSL Encrypted</div>
<div class="trust-badge"><span class="trust-icon"></span> Instant Access</div>
<div class="trust-badge"><span class="trust-icon">🛡</span> Secure</div>
</div>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* ── Wrapper ─────────────────────────────────────────────── */
.lp-wrap {
min-height: calc(100vh - 80px);
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
position: relative;
overflow: hidden;
}
/* ── Ambient background ──────────────────────────────────── */
.lp-bg { position: absolute; inset: 0; pointer-events: none; }
.orb {
position: absolute;
border-radius: 50%;
filter: blur(90px);
animation: orb-float 12s ease-in-out infinite;
}
.orb-a { width: 360px; height: 360px; background: var(--primary, #df006a); opacity: .18; top: -80px; left: -80px; }
.orb-b { width: 440px; height: 440px; background: #7c3aed; opacity: .12; bottom: -120px; right: -100px; animation-delay: -5s; }
.orb-c { width: 220px; height: 220px; background: #06b6d4; opacity: .10; top: 40%; left: 60%; animation-delay: -9s; }
@keyframes orb-float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(20px, 30px) scale(1.05); }
66% { transform: translate(-15px, -20px) scale(.97); }
}
.grid-overlay {
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%);
}
/* ── Card ────────────────────────────────────────────────── */
.lp-card {
width: 100%;
max-width: 440px;
background: rgba(8, 8, 12, 0.85);
backdrop-filter: blur(24px);
border: 1px solid rgba(255,255,255,.08);
border-radius: 28px;
padding: 0 0 28px;
position: relative;
z-index: 10;
box-shadow:
0 0 0 1px rgba(255,255,255,.04),
0 25px 60px rgba(0,0,0,.6),
0 0 80px rgba(223,0,106,.06);
animation: card-in .7s cubic-bezier(.16,1,.3,1) both;
}
@keyframes card-in {
from { opacity: 0; transform: translateY(32px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ── Top glow bar ────────────────────────────────────────── */
.card-glow-bar {
height: 3px;
border-radius: 28px 28px 0 0;
background: linear-gradient(90deg, transparent, var(--primary, #df006a) 30%, #a855f7 70%, transparent);
margin-bottom: 32px;
}
/* ── Brand ───────────────────────────────────────────────── */
.lp-brand { text-align: center; padding: 0 32px 8px; }
.brand-diamond {
width: 60px; height: 60px;
background: rgba(223,0,106,.1);
border: 1px solid rgba(223,0,106,.25);
border-radius: 16px;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 18px;
box-shadow: 0 0 30px rgba(223,0,106,.15);
}
.diamond-icon { font-size: 28px; color: var(--primary, #df006a); filter: drop-shadow(0 0 8px var(--primary, #df006a)); }
.brand-title {
font-size: 1.9rem;
font-weight: 900;
color: #fff;
letter-spacing: 3px;
margin: 0 0 6px;
line-height: 1.1;
}
.brand-accent {
color: var(--primary, #df006a);
text-shadow: 0 0 24px rgba(223,0,106,.5);
}
.brand-sub { color: #555; font-size: .8rem; letter-spacing: 1.5px; text-transform: uppercase; margin: 0; }
/* ── Divider ─────────────────────────────────────────────── */
.lp-divider {
display: flex; align-items: center; gap: 10px;
padding: 20px 32px 24px;
}
.divider-line { flex: 1; height: 1px; background: rgba(255,255,255,.06); }
.divider-dot { width: 4px; height: 4px; border-radius: 50%; background: rgba(223,0,106,.4); }
.divider-text { font-size: 10px; color: #444; letter-spacing: 2px; text-transform: uppercase; white-space: nowrap; }
/* ── Form wrapper ────────────────────────────────────────── */
:deep(.login-form-content) { padding: 0 32px; }
/* ── Footer ──────────────────────────────────────────────── */
.lp-footer {
display: flex; align-items: center; justify-content: center; gap: 10px;
padding: 24px 32px 8px;
border-top: 1px solid rgba(255,255,255,.05);
margin-top: 24px;
}
.footer-text { color: #555; font-size: .82rem; }
.reg-link {
display: flex; align-items: center; gap: 5px;
background: none; border: none; cursor: pointer;
color: var(--primary, #df006a); font-size: .82rem; font-weight: 700;
letter-spacing: .5px; transition: .2s; padding: 0;
}
.reg-link:hover { gap: 8px; filter: brightness(1.2); }
/* ── Trust badges ────────────────────────────────────────── */
.trust-row {
display: flex; align-items: center; justify-content: center; gap: 12px;
padding: 16px 32px 0;
flex-wrap: wrap;
}
.trust-badge {
display: flex; align-items: center; gap: 5px;
font-size: 10px; color: #444; letter-spacing: .5px;
}
.trust-icon { font-size: 11px; }
/* ── Forgot link (rendered inside LoginForm slot) ────────── */
:deep(.forgot-link) {
font-size: .75rem;
color: #555;
text-decoration: none;
transition: .2s;
letter-spacing: .3px;
}
:deep(.forgot-link:hover) { color: var(--primary, #df006a); }
</style>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import RegisterForm from '@/components/auth/RegisterForm.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
</script>
<template>
<UserLayout>
<Head title="Register" />
<div class="register-container">
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="register-card">
<div class="card-header">
<h1 class="title">CREATE <span class="highlight">ACCOUNT</span></h1>
<p class="subtitle">Join the ultimate crypto protocol</p>
</div>
<div class="form-content">
<RegisterForm />
</div>
<div class="card-footer">
<p class="footer-text">
Already have an account?
<button @click="() => window.dispatchEvent(new Event('require-login'))" class="link-btn">Log in here</button>
</p>
</div>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* Styles remain largely the same, but are now more robust due to API-first approach */
.register-container { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 100px); padding: 20px; position: relative; overflow: hidden; }
.register-card { width: 100%; max-width: 500px; background: rgba(10, 10, 10, 0.8); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; padding: 40px; position: relative; z-index: 10; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1); }
.progress-container { margin-bottom: 30px; }
.progress-bar { height: 4px; background: #1a1a1a; border-radius: 2px; overflow: hidden; margin-bottom: 10px; }
.progress-fill { height: 100%; background: #00f2ff; box-shadow: 0 0 10px #00f2ff; transition: width 0.4s ease; }
.steps-indicator { display: flex; justify-content: space-between; font-size: 10px; text-transform: uppercase; color: #444; font-weight: 700; }
.steps-indicator span.active { color: #00f2ff; text-shadow: 0 0 5px rgba(0, 242, 255, 0.5); }
.card-header { text-align: center; margin-bottom: 30px; }
.title { font-size: 1.8rem; font-weight: 900; color: white; letter-spacing: 2px; margin: 0; }
.highlight { color: #ff007a; text-shadow: 0 0 20px rgba(255, 0, 122, 0.5); }
.subtitle { color: #888; font-size: 0.8rem; margin-top: 5px; text-transform: uppercase; letter-spacing: 1px; }
.step-content { display: flex; flex-direction: column; gap: 20px; }
.input-group { display: flex; flex-direction: column; gap: 8px; position: relative; }
.input-wrapper { position: relative; }
.status-icon { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); z-index: 10; }
.input-group.invalid :deep(input) { border-color: #ef4444; animation: shake 0.4s ease-in-out; }
.input-group.valid :deep(input) { border-color: #22c55e; }
.error-msg { font-size: 11px; color: #ef4444; font-weight: 600; margin-top: -4px; }
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
.cyber-checkbox-wrapper { display: flex; align-items: center; gap: 15px; background: rgba(0, 0, 0, 0.4); border: 1px solid #222; padding: 15px; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; }
.cyber-checkbox-wrapper:hover { border-color: #333; background: rgba(255, 255, 255, 0.02); }
.cyber-checkbox-wrapper.checked { border-color: #00f2ff; background: rgba(0, 242, 255, 0.05); box-shadow: 0 0 20px rgba(0, 242, 255, 0.1); }
.cyber-box { width: 24px; height: 24px; background: #000; border: 2px solid #333; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: 0.3s; }
.cyber-checkbox-wrapper.checked .cyber-box { border-color: #00f2ff; box-shadow: 0 0 10px #00f2ff; }
.cyber-tick { width: 12px; height: 12px; background: #00f2ff; border-radius: 2px; box-shadow: 0 0 10px #00f2ff; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.cyber-label { display: flex; flex-direction: column; }
.neon-button { background: linear-gradient(90deg, #ff007a, #be005b); border: none; position: relative; overflow: hidden; transition: all 0.3s ease; }
.neon-button:hover { transform: translateY(-2px); box-shadow: 0 0 30px rgba(255, 0, 122, 0.6); filter: brightness(1.1); }
.neon-button:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(1); }
.glow-orb { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.4; z-index: 0; animation: float 10s infinite ease-in-out; }
.orb-1 { width: 300px; height: 300px; background: #ff007a; top: -50px; left: -100px; }
.orb-2 { width: 400px; height: 400px; background: #00f2ff; bottom: -100px; right: -100px; animation-delay: -5s; }
@keyframes slideUp { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes popIn { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(20px, 30px); } }
.fade-slide-enter-active, .fade-slide-leave-active { transition: all 0.3s ease; }
.fade-slide-enter-from { opacity: 0; transform: translateX(20px); }
.fade-slide-leave-to { opacity: 0; transform: translateX(-20px); }
:deep(input[type="date"]::-webkit-calendar-picker-indicator) { filter: invert(1); cursor: pointer; opacity: 0.6; transition: 0.2s; }
:deep(input[type="date"]::-webkit-calendar-picker-indicator:hover) { opacity: 1; }
</style>

View File

@@ -0,0 +1,281 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { Eye, EyeOff } from 'lucide-vue-next';
import { computed, ref } from 'vue';
import InputError from '@/components/InputError.vue';
import Button from '@/components/ui/button.vue';
import Input from '@/components/ui/input.vue';
import Label from '@/components/ui/label.vue';
import Spinner from '@/components/ui/spinner.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
const props = defineProps<{
token: string;
email: string;
}>();
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const showPassword = ref(false);
const showConfirmPassword = ref(false);
const submit = () => {
form.post('/reset-password', {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
// Password Strength Logic
const passwordStrength = computed(() => {
const pwd = form.password;
let score = 0;
if (!pwd) return 0;
if (pwd.length > 6) score += 20;
if (pwd.length > 10) score += 20;
if (/[A-Z]/.test(pwd)) score += 20;
if (/[0-9]/.test(pwd)) score += 20;
if (/[^A-Za-z0-9]/.test(pwd)) score += 20;
return score;
});
const strengthColor = computed(() => {
const s = passwordStrength.value;
if (s < 40) return 'bg-red-500';
if (s < 80) return 'bg-yellow-500';
return 'bg-green-500';
});
const strengthLabel = computed(() => {
const s = passwordStrength.value;
if (s < 40) return 'Weak';
if (s < 80) return 'Medium';
return 'Strong';
});
</script>
<template>
<UserLayout>
<Head title="Reset Password" />
<div class="login-container">
<!-- Animated Background -->
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="login-card">
<div class="card-header">
<h1 class="title">NEW <span class="highlight">PASSWORD</span></h1>
<p class="subtitle">Secure your account</p>
</div>
<form @submit.prevent="submit" class="form-content">
<div class="input-group">
<Label for="email">Email Address</Label>
<Input
id="email"
type="email"
v-model="form.email"
required
readonly
class="opacity-50 cursor-not-allowed"
/>
<InputError :message="form.errors.email" />
</div>
<div class="input-group">
<Label for="password">New Password</Label>
<div class="input-wrapper">
<Input
id="password"
:type="showPassword ? 'text' : 'password'"
v-model="form.password"
required
autofocus
autocomplete="new-password"
placeholder="••••••••"
/>
<div class="status-icon cursor-pointer pointer-events-auto hover:text-white text-[#666] transition-colors" @click="showPassword = !showPassword">
<Eye v-if="!showPassword" class="w-4 h-4" />
<EyeOff v-else class="w-4 h-4" />
</div>
</div>
<!-- Password Strength Indicator -->
<div class="h-1 w-full bg-gray-800 rounded-full mt-2 overflow-hidden">
<div class="h-full transition-all duration-500" :class="strengthColor" :style="{ width: `${passwordStrength}%` }"></div>
</div>
<p class="text-xs text-right mt-1" :class="{'text-red-500': passwordStrength < 40, 'text-yellow-500': passwordStrength >= 40 && passwordStrength < 80, 'text-green-500': passwordStrength >= 80}">{{ strengthLabel }}</p>
<InputError :message="form.errors.password" />
</div>
<div class="input-group">
<Label for="password_confirmation">Confirm Password</Label>
<div class="input-wrapper">
<Input
id="password_confirmation"
:type="showConfirmPassword ? 'text' : 'password'"
v-model="form.password_confirmation"
required
autocomplete="new-password"
placeholder="••••••••"
/>
<div class="status-icon cursor-pointer pointer-events-auto hover:text-white text-[#666] transition-colors" @click="showConfirmPassword = !showConfirmPassword">
<Eye v-if="!showConfirmPassword" class="w-4 h-4" />
<EyeOff v-else class="w-4 h-4" />
</div>
</div>
<InputError :message="form.errors.password_confirmation" />
</div>
<div class="mt-6">
<Button
type="submit"
class="w-full h-12 text-base font-bold tracking-widest uppercase neon-button relative overflow-hidden"
:disabled="form.processing"
>
<div v-if="form.processing" class="absolute inset-0 bg-black/20 flex items-center justify-center">
<Spinner class="w-5 h-5" />
</div>
<span :class="{ 'opacity-0': form.processing }">Reset Password</span>
</Button>
</div>
</form>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* Container & Layout */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 100px);
padding: 20px;
position: relative;
overflow: hidden;
}
.login-card {
width: 100%;
max-width: 450px;
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 40px;
position: relative;
z-index: 10;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Header */
.card-header {
text-align: center;
margin-bottom: 30px;
}
.title {
font-size: 1.8rem;
font-weight: 900;
color: white;
letter-spacing: 2px;
margin: 0;
}
.highlight {
color: #ff007a;
text-shadow: 0 0 20px rgba(255, 0, 122, 0.5);
}
.subtitle {
color: #888;
font-size: 0.9rem;
margin-top: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Form Content */
.form-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
/* Input Wrapper for Icons */
.input-wrapper {
position: relative;
}
.status-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
/* Neon Button */
.neon-button {
background: linear-gradient(90deg, #ff007a, #be005b);
border: none;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.neon-button:hover {
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(255, 0, 122, 0.6);
filter: brightness(1.1);
}
.neon-button:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
/* Background Orbs */
.glow-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
z-index: 0;
animation: float 10s infinite ease-in-out;
}
.orb-1 {
width: 300px;
height: 300px;
background: #ff007a;
top: -50px;
left: -100px;
}
.orb-2 {
width: 400px;
height: 400px;
background: #00f2ff;
bottom: -100px;
right: -100px;
animation-delay: -5s;
}
/* Animations */
@keyframes slideUp {
from { opacity: 0; transform: translateY(40px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(20px, 30px); }
}
</style>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import { computed, ref } 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 AuthLayout from '@/layouts/AuthLayout.vue';
import type { TwoFactorConfigContent } from '@/types';
import { store } from '@/routes/two-factor/login';
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',
};
});
const showRecoveryInput = ref<boolean>(false);
const toggleRecoveryMode = (clearErrors: () => void): void => {
showRecoveryInput.value = !showRecoveryInput.value;
clearErrors();
code.value = '';
};
const code = ref<string>('');
</script>
<template>
<AuthLayout
:title="authConfigContent.title"
:description="authConfigContent.description"
>
<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>
</AuthLayout>
</template>

View File

@@ -0,0 +1,267 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { Mail, LogOut, ArrowRight } from 'lucide-vue-next';
import Button from '@/components/ui/button.vue';
import Spinner from '@/components/ui/spinner.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
defineProps<{
status?: string;
verificationUrl?: string | null;
hasCode?: boolean;
}>();
const form = useForm({});
const codeForm = useForm({ code: '' });
const submit = () => {
form.post('/email/verification-notification');
};
const submitCode = () => {
// Normalize to digits-only and limit to 6 characters before submitting
const digits = (codeForm.code || '').toString().replace(/\D+/g, '').slice(0, 6);
// @ts-expect-error inertia form model is mutable
codeForm.code = digits;
codeForm.post('/email/verify/code');
};
const logout = () => {
useForm({}).post('/logout');
};
</script>
<template>
<UserLayout>
<Head title="Verify Email" />
<div class="login-container">
<!-- Animated Background -->
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="login-card">
<div class="card-header">
<div class="icon-wrapper mb-6">
<Mail class="w-10 h-10 text-[#ff007a] animate-pulse-slow" />
</div>
<h1 class="title">UNLOCK <span class="highlight">ACCESS</span></h1>
<p class="subtitle">Verify your email to start playing</p>
</div>
<div v-if="status === 'verification-link-sent'" class="mb-6 text-center text-sm font-bold text-green-400 bg-green-500/10 p-4 rounded-xl border border-green-500/20 shadow-[0_0_15px_rgba(74,222,128,0.1)]">
<div class="flex items-center justify-center gap-2">
<span class="w-2 h-2 bg-green-400 rounded-full animate-ping"></span>
Link sent! Check your inbox.
</div>
</div>
<div class="text-center text-[#888] text-sm mb-8 leading-relaxed px-4">
We've sent a verification link to your email address. <br>
Please click it to activate your account and claim your welcome bonus.
</div>
<form @submit.prevent="submit" class="form-content">
<Button
type="submit"
class="w-full h-14 text-base font-black tracking-widest uppercase neon-button group"
:disabled="form.processing"
>
<Spinner v-if="form.processing" class="mr-2" />
<span v-else class="flex items-center justify-center gap-2">
Resend Email <ArrowRight class="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</Button>
<!-- Code entry (fallback #3) -->
<div v-if="hasCode" class="mt-6">
<div class="text-xs text-[#888] text-center mb-2">Oder gib den VerifizierungsCode aus der Email ein:</div>
<div class="flex items-center gap-2">
<input
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
placeholder="XXXXXX"
class="flex-1 bg-[#0b0b0b] border border-[#222] rounded-lg px-3 py-3 text-sm tracking-[0.4em] text-center text-[#ddd]"
v-model="codeForm.code"
@input="codeForm.code = String(($event.target as HTMLInputElement).value).replace(/\D+/g,'').slice(0,6)"
@keydown.enter.prevent="submitCode"
/>
<Button type="button" class="px-4 py-3" :disabled="codeForm.processing" @click="submitCode">
<Spinner v-if="codeForm.processing" class="mr-2" />
<span v-else>Verify</span>
</Button>
</div>
<div v-if="codeForm.errors.code" class="mt-2 text-xs text-red-400 text-center">{{ codeForm.errors.code }}</div>
</div>
<div class="text-center mt-8">
<button
type="button"
@click="logout"
class="text-[#444] hover:text-white transition-colors flex items-center justify-center gap-2 text-xs font-bold uppercase tracking-widest border border-transparent hover:border-[#333] px-4 py-2 rounded-lg"
>
<LogOut class="w-3 h-3" /> Log Out
</button>
</div>
</form>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* Container & Layout */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 100px);
padding: 20px;
position: relative;
overflow: hidden;
}
.login-card {
width: 100%;
max-width: 480px;
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 50px 40px;
position: relative;
z-index: 10;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Header */
.card-header {
text-align: center;
margin-bottom: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
.icon-wrapper {
background: rgba(255, 0, 122, 0.05);
padding: 20px;
border-radius: 50%;
border: 1px solid rgba(255, 0, 122, 0.2);
box-shadow: 0 0 30px rgba(255, 0, 122, 0.1);
position: relative;
}
.icon-wrapper::after {
content: '';
position: absolute;
inset: -5px;
border-radius: 50%;
border: 1px solid rgba(255, 0, 122, 0.1);
animation: spin 10s linear infinite;
border-top-color: transparent;
border-bottom-color: transparent;
}
.title {
font-size: 2rem;
font-weight: 900;
color: white;
letter-spacing: 2px;
margin: 0;
line-height: 1;
}
.highlight {
color: #ff007a;
text-shadow: 0 0 20px rgba(255, 0, 122, 0.5);
}
.subtitle {
color: #666;
font-size: 0.85rem;
margin-top: 8px;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 700;
}
/* Neon Button */
.neon-button {
background: linear-gradient(90deg, #ff007a, #be005b);
border: none;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.neon-button:hover {
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(255, 0, 122, 0.6);
filter: brightness(1.1);
}
.neon-button:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
/* Background Orbs */
.glow-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
z-index: 0;
animation: float 10s infinite ease-in-out;
}
.orb-1 {
width: 300px;
height: 300px;
background: #ff007a;
top: -50px;
left: -100px;
}
.orb-2 {
width: 400px;
height: 400px;
background: #00f2ff;
bottom: -100px;
right: -100px;
animation-delay: -5s;
}
/* Animations */
@keyframes slideUp {
from { opacity: 0; transform: translateY(40px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(20px, 30px); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-pulse-slow {
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .7; }
}
/* Mobile tweaks */
@media (max-width: 480px) {
.login-container { padding: calc(env(safe-area-inset-top, 0) + 10px) 12px 12px; }
.login-card { padding: 28px 16px; border-radius: 18px; }
.title { font-size: clamp(20px, 6vw, 28px); letter-spacing: 1px; }
.subtitle { font-size: 12px; }
.icon-wrapper { padding: 14px; }
.glow-orb { filter: blur(60px); }
.orb-1 { width: 180px; height: 180px; top: -40px; left: -60px; }
.orb-2 { width: 220px; height: 220px; bottom: -60px; right: -60px; }
/* Prevent iOS zoom on input */
input[type="text"] { font-size: 16px; }
}
</style>