Initialer Laravel Commit für BetiX
This commit is contained in:
323
resources/js/components/auth/AuthModals.vue
Normal file
323
resources/js/components/auth/AuthModals.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import LoginForm from './LoginForm.vue';
|
||||
import RegisterForm from './RegisterForm.vue';
|
||||
import { X } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps<{
|
||||
showLogin: boolean;
|
||||
showRegister: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'switch', type: 'login' | 'register'): void;
|
||||
}>();
|
||||
|
||||
const handleClose = () => emit('close');
|
||||
const switchTo = (type: 'login' | 'register') => emit('switch', type);
|
||||
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') handleClose();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKey));
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', onKey));
|
||||
|
||||
// Lock body scroll when modal is open
|
||||
watch(() => props.showLogin || props.showRegister, (open) => {
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="auth-fade">
|
||||
<div v-if="showLogin || showRegister" class="auth-overlay" @click.self="handleClose">
|
||||
<div class="auth-card" :class="{ 'wide': showRegister }">
|
||||
|
||||
<!-- Decorative glow bg -->
|
||||
<div class="auth-glow-1"></div>
|
||||
<div class="auth-glow-2"></div>
|
||||
|
||||
<!-- Close -->
|
||||
<button class="auth-close" @click="handleClose" aria-label="Close">
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Brand header -->
|
||||
<div class="auth-brand">
|
||||
<div class="brand-logo">Beti<span>X</span></div>
|
||||
<div class="brand-tagline">The Ultimate Crypto Casino</div>
|
||||
</div>
|
||||
|
||||
<!-- Social proof bar -->
|
||||
<div class="auth-proof">
|
||||
<span class="proof-dot"></span>
|
||||
<span>50,000+ active players</span>
|
||||
<span class="proof-sep">·</span>
|
||||
<span>Instant withdrawals</span>
|
||||
<span class="proof-sep">·</span>
|
||||
<span>Provably fair</span>
|
||||
</div>
|
||||
|
||||
<!-- Login view -->
|
||||
<div v-if="showLogin" class="auth-body">
|
||||
<div class="auth-title-block">
|
||||
<h2 class="auth-title">Welcome <span>Back</span></h2>
|
||||
<p class="auth-sub">Log in to access your account</p>
|
||||
</div>
|
||||
<LoginForm :onSuccess="handleClose">
|
||||
<template #forgot-password>
|
||||
<a href="/forgot-password" class="forgot-link">Forgot password?</a>
|
||||
</template>
|
||||
</LoginForm>
|
||||
<div class="auth-switch">
|
||||
Don't have an account?
|
||||
<button @click="switchTo('register')" class="switch-btn">Create one free</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register view -->
|
||||
<div v-if="showRegister" class="auth-body">
|
||||
<div class="auth-title-block">
|
||||
<h2 class="auth-title">Create <span>Account</span></h2>
|
||||
<p class="auth-sub">Join the ultimate crypto protocol</p>
|
||||
</div>
|
||||
<RegisterForm :onSuccess="handleClose" />
|
||||
<div class="auth-switch">
|
||||
Already have an account?
|
||||
<button @click="switchTo('login')" class="switch-btn">Log in</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9000;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
position: relative;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(223, 0, 106, 0.08),
|
||||
0 40px 80px rgba(0, 0, 0, 0.7),
|
||||
0 0 60px rgba(223, 0, 106, 0.06);
|
||||
}
|
||||
|
||||
.auth-card.wide {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
/* Decorative glows */
|
||||
.auth-glow-1 {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
right: -60px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(223, 0, 106, 0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.auth-glow-2 {
|
||||
position: absolute;
|
||||
bottom: -60px;
|
||||
left: -40px;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
background: radial-gradient(circle, rgba(0, 242, 255, 0.07) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.auth-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-close:hover {
|
||||
color: #fff;
|
||||
background: rgba(223, 0, 106, 0.2);
|
||||
border-color: rgba(223, 0, 106, 0.4);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Brand header */
|
||||
.auth-brand {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 28px 32px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.brand-logo span {
|
||||
color: var(--primary, #df006a);
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 11px;
|
||||
color: #444;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-weight: 700;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Social proof */
|
||||
.auth-proof {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
padding: 12px 32px 0;
|
||||
}
|
||||
|
||||
.proof-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #00ff9d;
|
||||
box-shadow: 0 0 6px #00ff9d;
|
||||
animation: proof-pulse 2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes proof-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.proof-sep { color: #333; }
|
||||
|
||||
/* Body */
|
||||
.auth-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 24px 32px 32px;
|
||||
max-height: calc(90vh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-body::-webkit-scrollbar { width: 4px; }
|
||||
.auth-body::-webkit-scrollbar-track { background: transparent; }
|
||||
.auth-body::-webkit-scrollbar-thumb { background: #222; border-radius: 2px; }
|
||||
|
||||
.auth-title-block {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 4px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.auth-title span {
|
||||
color: var(--primary, #df006a);
|
||||
}
|
||||
|
||||
.auth-sub {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Switch prompt */
|
||||
.auth-switch {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
color: var(--primary, #df006a);
|
||||
font-weight: 700;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0 0 4px;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.switch-btn:hover { opacity: 0.75; text-decoration: underline; }
|
||||
|
||||
/* Forgot link */
|
||||
.forgot-link {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.forgot-link:hover { color: var(--primary, #df006a); }
|
||||
|
||||
/* Transition */
|
||||
.auth-fade-enter-active, .auth-fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.auth-fade-enter-active .auth-card,
|
||||
.auth-fade-leave-active .auth-card {
|
||||
transition: transform 0.25s cubic-bezier(0.2, 0, 0, 1), opacity 0.2s ease;
|
||||
}
|
||||
.auth-fade-enter-from { opacity: 0; }
|
||||
.auth-fade-leave-to { opacity: 0; }
|
||||
.auth-fade-enter-from .auth-card { transform: scale(0.95) translateY(12px); }
|
||||
.auth-fade-leave-to .auth-card { transform: scale(0.97); }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-card { border-radius: 16px; }
|
||||
.auth-body { padding: 20px 20px 24px; }
|
||||
.auth-brand { padding: 24px 20px 0; }
|
||||
.auth-proof { padding: 10px 20px 0; flex-wrap: wrap; gap: 4px; }
|
||||
.auth-title { font-size: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
289
resources/js/components/auth/LoginForm.vue
Normal file
289
resources/js/components/auth/LoginForm.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Check, Eye, EyeOff, X, AtSign, Lock } from 'lucide-vue-next';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import Spinner from '@/components/ui/spinner.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
status?: string;
|
||||
onSuccess?: () => void;
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
login: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const showPassword = ref(false);
|
||||
|
||||
const validation = reactive({
|
||||
login: { valid: false, error: '' },
|
||||
password: { valid: false, error: '' },
|
||||
});
|
||||
|
||||
const validateField = (field: string, value: any) => {
|
||||
if (field === 'login') {
|
||||
validation.login = value.length >= 3
|
||||
? { valid: true, error: '' }
|
||||
: { valid: false, error: 'Too short' };
|
||||
}
|
||||
if (field === 'password') {
|
||||
validation.password = value
|
||||
? { valid: true, error: '' }
|
||||
: { valid: false, error: 'Required' };
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => form.login, (v) => validateField('login', v));
|
||||
watch(() => form.password, (v) => validateField('password', v));
|
||||
|
||||
const submit = () => {
|
||||
form.transform((data) => ({ ...data, email: data.login })).post('/login', {
|
||||
onSuccess: () => { if (props.onSuccess) props.onSuccess(); },
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-form-content">
|
||||
<!-- Status message -->
|
||||
<div v-if="status" class="lf-status">{{ status }}</div>
|
||||
|
||||
<form @submit.prevent="submit" class="lf-form">
|
||||
<!-- Login -->
|
||||
<div class="lf-field" :class="{ valid: validation.login.valid, invalid: validation.login.error || form.errors.login }">
|
||||
<label class="lf-label" for="login">Email oder Benutzername</label>
|
||||
<div class="lf-input-wrap">
|
||||
<span class="lf-icon-left"><AtSign :size="15" /></span>
|
||||
<input
|
||||
id="login"
|
||||
class="lf-input"
|
||||
type="text"
|
||||
v-model="form.login"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
placeholder="CryptoKing oder email@example.com"
|
||||
/>
|
||||
<span class="lf-status-icon">
|
||||
<Check v-if="validation.login.valid" :size="14" class="icon-valid" />
|
||||
<X v-if="validation.login.error || form.errors.login" :size="14" class="icon-invalid" />
|
||||
</span>
|
||||
</div>
|
||||
<InputError :message="form.errors.login" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="lf-field" :class="{ valid: validation.password.valid, invalid: validation.password.error || form.errors.password }">
|
||||
<label class="lf-label" for="password">Passwort</label>
|
||||
<div class="lf-input-wrap">
|
||||
<span class="lf-icon-left"><Lock :size="15" /></span>
|
||||
<input
|
||||
id="password"
|
||||
class="lf-input lf-input-pw"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button type="button" class="lf-eye-btn" @click="showPassword = !showPassword" :title="showPassword ? 'Verbergen' : 'Anzeigen'">
|
||||
<EyeOff v-if="showPassword" :size="15" />
|
||||
<Eye v-else :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
<InputError :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<!-- Remember + Forgot -->
|
||||
<div class="lf-row-between">
|
||||
<label class="lf-remember">
|
||||
<input type="checkbox" name="remember" v-model="form.remember" class="sr-only peer" />
|
||||
<span class="lf-check-box">
|
||||
<Check :size="11" class="lf-tick" stroke-width="3.5" />
|
||||
</span>
|
||||
<span class="lf-remember-text">Angemeldet bleiben</span>
|
||||
</label>
|
||||
|
||||
<slot name="forgot-password"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button
|
||||
type="submit"
|
||||
class="lf-submit"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
<span class="lf-submit-shine"></span>
|
||||
<Spinner v-if="form.processing" class="lf-spinner" />
|
||||
<span :class="{ 'opacity-0': form.processing }">Einloggen</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-form-content { width: 100%; }
|
||||
|
||||
/* Status */
|
||||
.lf-status {
|
||||
margin-bottom: 18px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: .8rem;
|
||||
color: #4ade80;
|
||||
background: rgba(74,222,128,.08);
|
||||
border: 1px solid rgba(74,222,128,.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.lf-form { display: flex; flex-direction: column; gap: 18px; }
|
||||
|
||||
/* Field */
|
||||
.lf-field { display: flex; flex-direction: column; gap: 7px; }
|
||||
.lf-label { font-size: .75rem; font-weight: 700; color: #666; letter-spacing: .8px; text-transform: uppercase; }
|
||||
|
||||
/* Input wrapper */
|
||||
.lf-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lf-icon-left {
|
||||
position: absolute; left: 13px;
|
||||
color: #444; pointer-events: none;
|
||||
display: flex; align-items: center;
|
||||
transition: color .2s;
|
||||
}
|
||||
|
||||
.lf-input {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
background: rgba(255,255,255,.04);
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
font-size: .9rem;
|
||||
padding: 0 40px 0 38px;
|
||||
outline: none;
|
||||
transition: border-color .2s, background .2s, box-shadow .2s;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.lf-input::placeholder { color: #333; }
|
||||
.lf-input:focus {
|
||||
border-color: rgba(223,0,106,.4);
|
||||
background: rgba(223,0,106,.03);
|
||||
box-shadow: 0 0 0 3px rgba(223,0,106,.08);
|
||||
}
|
||||
.lf-input:focus ~ .lf-icon-left,
|
||||
.lf-input-wrap:focus-within .lf-icon-left { color: var(--primary, #df006a); }
|
||||
|
||||
/* Extra padding for password (eye button on right) */
|
||||
.lf-input-pw { padding-right: 46px; }
|
||||
|
||||
/* Status icon (check / X) */
|
||||
.lf-status-icon {
|
||||
position: absolute; right: 12px;
|
||||
pointer-events: none; display: flex; align-items: center;
|
||||
}
|
||||
.icon-valid { color: #4ade80; }
|
||||
.icon-invalid{ color: #f87171; }
|
||||
|
||||
/* Password eye button */
|
||||
.lf-eye-btn {
|
||||
position: absolute; right: 12px;
|
||||
width: 28px; height: 28px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: #444; border-radius: 8px; transition: .2s;
|
||||
}
|
||||
.lf-eye-btn:hover { color: #aaa; background: rgba(255,255,255,.05); }
|
||||
|
||||
/* Valid / Invalid field states */
|
||||
.valid .lf-input { border-color: rgba(74,222,128,.3); }
|
||||
.invalid .lf-input { border-color: rgba(248,113,113,.35); background: rgba(248,113,113,.025); animation: shake .35s ease; }
|
||||
|
||||
@keyframes shake {
|
||||
0%,100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
/* Remember row */
|
||||
.lf-row-between {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.lf-remember {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
cursor: pointer; user-select: none;
|
||||
}
|
||||
.lf-check-box {
|
||||
width: 18px; height: 18px;
|
||||
background: rgba(0,0,0,.4);
|
||||
border: 1.5px solid #2a2a2a;
|
||||
border-radius: 5px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: .2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sr-only:checked ~ .lf-check-box {
|
||||
background: var(--primary, #df006a);
|
||||
border-color: var(--primary, #df006a);
|
||||
box-shadow: 0 0 10px rgba(223,0,106,.35);
|
||||
}
|
||||
.lf-tick {
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: .15s cubic-bezier(.175,.885,.32,1.275);
|
||||
}
|
||||
.sr-only:checked ~ .lf-check-box .lf-tick { opacity: 1; transform: scale(1); }
|
||||
/* Tailwind peer trick doesn't work in scoped — use sibling selector */
|
||||
input[type="checkbox"]:checked + .lf-check-box { background: var(--primary, #df006a); border-color: var(--primary, #df006a); box-shadow: 0 0 10px rgba(223,0,106,.3); }
|
||||
input[type="checkbox"]:checked + .lf-check-box .lf-tick { opacity: 1; transform: scale(1); }
|
||||
.lf-remember-text { font-size: .78rem; color: #555; transition: .2s; }
|
||||
.lf-remember:hover .lf-remember-text { color: #aaa; }
|
||||
|
||||
/* Submit button */
|
||||
.lf-submit {
|
||||
position: relative; overflow: hidden;
|
||||
width: 100%; height: 48px;
|
||||
background: linear-gradient(90deg, var(--primary, #df006a), color-mix(in srgb, var(--primary, #df006a) 65%, #7c3aed));
|
||||
border: none; border-radius: 14px;
|
||||
color: #fff; font-size: .9rem; font-weight: 800;
|
||||
letter-spacing: 1.5px; text-transform: uppercase;
|
||||
cursor: pointer; transition: .25s;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(223,0,106,.3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.lf-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(223,0,106,.45);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
.lf-submit:active:not(:disabled) { transform: translateY(0); }
|
||||
.lf-submit:disabled { opacity: .55; cursor: not-allowed; }
|
||||
|
||||
.lf-submit-shine {
|
||||
position: absolute; top: 0; left: -100%; width: 60%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.15), transparent);
|
||||
transform: skewX(-15deg);
|
||||
animation: shine 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes shine {
|
||||
0% { left: -100%; }
|
||||
40% { left: 150%; }
|
||||
100% { left: 150%; }
|
||||
}
|
||||
|
||||
.lf-spinner { width: 18px; height: 18px; position: absolute; }
|
||||
|
||||
/* sr-only */
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
</style>
|
||||
567
resources/js/components/auth/RegisterForm.vue
Normal file
567
resources/js/components/auth/RegisterForm.vue
Normal file
@@ -0,0 +1,567 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { ChevronRight, ChevronLeft, Check, X } from 'lucide-vue-next';
|
||||
import { ref, computed, watch, reactive } from 'vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import Button from '@/components/ui/button.vue';
|
||||
import CountrySelect from '@/components/ui/CountrySelect.vue';
|
||||
import DatePicker from '@/components/ui/DatePicker.vue';
|
||||
import Input from '@/components/ui/input.vue';
|
||||
import Label from '@/components/ui/label.vue';
|
||||
import Select from '@/components/ui/Select.vue';
|
||||
import Spinner from '@/components/ui/spinner.vue';
|
||||
import { csrfFetch } from '@/utils/csrfFetch';
|
||||
|
||||
const props = defineProps<{
|
||||
onSuccess?: () => void;
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
full_name: '',
|
||||
email: '',
|
||||
birthdate: '',
|
||||
gender: '',
|
||||
phone: '',
|
||||
country: '',
|
||||
address_line1: '',
|
||||
address_line2: '',
|
||||
city: '',
|
||||
postal_code: '',
|
||||
currency: 'EUR',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
is_adult: false,
|
||||
terms_accepted: false,
|
||||
});
|
||||
|
||||
const currentStep = ref(1);
|
||||
const totalSteps = 4;
|
||||
const showPassword = ref(false);
|
||||
const showConfirmPassword = ref(false);
|
||||
|
||||
const validation = reactive({
|
||||
username: { valid: false, error: '' },
|
||||
first_name: { valid: false, error: '' },
|
||||
last_name: { valid: false, error: '' },
|
||||
email: { valid: false, error: '' },
|
||||
phone: { valid: false, error: '' },
|
||||
birthdate: { valid: false, error: '' },
|
||||
gender: { valid: false, error: '' },
|
||||
address_line1: { valid: false, error: '' },
|
||||
city: { valid: false, error: '' },
|
||||
postal_code: { valid: false, error: '' },
|
||||
password: { valid: false, error: '' },
|
||||
password_confirmation: { valid: false, error: '' },
|
||||
});
|
||||
|
||||
const availability = reactive({
|
||||
username: { checked: false, checking: false, available: false, error: '' },
|
||||
email: { checked: false, checking: false, available: false, error: '' },
|
||||
});
|
||||
|
||||
const debounceTimers: Record<string, any> = { username: null, email: null };
|
||||
|
||||
const checkAvailability = (field: 'username' | 'email', value: string) => {
|
||||
if (debounceTimers[field]) clearTimeout(debounceTimers[field]);
|
||||
|
||||
if (!value || (field === 'username' && validation.username.error) || (field === 'email' && validation.email.error)) {
|
||||
availability[field].checked = false;
|
||||
availability[field].available = false;
|
||||
availability[field].error = '';
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimers[field] = setTimeout(async () => {
|
||||
availability[field].checking = true;
|
||||
availability[field].error = '';
|
||||
const currentValue = value;
|
||||
|
||||
try {
|
||||
const url = `/api/auth/availability?field=${field}&value=${encodeURIComponent(currentValue)}`;
|
||||
const res = await csrfFetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (form[field] !== currentValue) return;
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok && data.available) {
|
||||
availability[field].available = true;
|
||||
} else {
|
||||
availability[field].available = false;
|
||||
availability[field].error = data.message || (field === 'username' ? 'Username taken' : 'Email in use');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Availability check failed:', error);
|
||||
availability[field].available = false;
|
||||
availability[field].error = 'Error checking availability. Please try again.';
|
||||
} finally {
|
||||
availability[field].checking = false;
|
||||
availability[field].checked = true;
|
||||
}
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const validateField = (field: string, value: any) => {
|
||||
switch (field) {
|
||||
case 'username':
|
||||
if ((value || '').length < 3) {
|
||||
validation.username = { valid: false, error: 'Min. 3 characters' };
|
||||
} else {
|
||||
validation.username = { valid: true, error: '' };
|
||||
checkAvailability('username', value);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value || '')) {
|
||||
validation.email = { valid: false, error: 'Invalid email' };
|
||||
} else {
|
||||
validation.email = { valid: true, error: '' };
|
||||
checkAvailability('email', value);
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
if ((value || '').length < 8) {
|
||||
validation.password = { valid: false, error: 'Min. 8 characters' };
|
||||
} else {
|
||||
validation.password = { valid: true, error: '' };
|
||||
}
|
||||
break;
|
||||
case 'password_confirmation':
|
||||
if (value !== form.password) {
|
||||
validation.password_confirmation = { valid: false, error: 'Passwords mismatch' };
|
||||
} else {
|
||||
validation.password_confirmation = { valid: true, error: '' };
|
||||
}
|
||||
break;
|
||||
case 'first_name':
|
||||
case 'last_name':
|
||||
case 'city':
|
||||
case 'address_line1':
|
||||
case 'postal_code':
|
||||
validation[field] = (value || '').length > 0
|
||||
? { valid: true, error: '' }
|
||||
: { valid: false, error: 'Required' };
|
||||
break;
|
||||
case 'phone':
|
||||
validation.phone = (value || '').length > 5
|
||||
? { valid: true, error: '' }
|
||||
: { valid: false, error: 'Required' };
|
||||
break;
|
||||
case 'birthdate':
|
||||
validation.birthdate = value ? { valid: true, error: '' } : { valid: false, error: 'Required' };
|
||||
break;
|
||||
case 'gender':
|
||||
validation.gender = value ? { valid: true, error: '' } : { valid: false, error: 'Required' };
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => form.username, (v) => validateField('username', v));
|
||||
watch(() => form.email, (v) => validateField('email', v));
|
||||
watch(() => form.password, (v) => {
|
||||
validateField('password', v);
|
||||
validateField('password_confirmation', form.password_confirmation);
|
||||
});
|
||||
watch(() => form.password_confirmation, (v) => validateField('password_confirmation', v));
|
||||
watch(() => form.first_name, (v) => validateField('first_name', v));
|
||||
watch(() => form.last_name, (v) => validateField('last_name', v));
|
||||
watch(() => form.city, (v) => validateField('city', v));
|
||||
watch(() => form.address_line1, (v) => validateField('address_line1', v));
|
||||
watch(() => form.postal_code, (v) => validateField('postal_code', v));
|
||||
watch(() => form.phone, (v) => validateField('phone', v));
|
||||
watch(() => form.birthdate, (v) => validateField('birthdate', v));
|
||||
watch(() => form.gender, (v) => validateField('gender', v));
|
||||
|
||||
const step1Valid = computed(() =>
|
||||
validation.username.valid && availability.username.available &&
|
||||
validation.first_name.valid && validation.last_name.valid
|
||||
);
|
||||
|
||||
const step2Valid = computed(() =>
|
||||
validation.email.valid && availability.email.available &&
|
||||
validation.phone.valid && validation.birthdate.valid && validation.gender.valid
|
||||
);
|
||||
|
||||
const step3Valid = computed(() =>
|
||||
validation.address_line1.valid && validation.city.valid &&
|
||||
validation.postal_code.valid && form.country && form.currency
|
||||
);
|
||||
|
||||
const step4Valid = computed(() =>
|
||||
validation.password.valid && validation.password_confirmation.valid &&
|
||||
form.is_adult && form.terms_accepted
|
||||
);
|
||||
|
||||
const nextStep = () => { if (currentStep.value < totalSteps) currentStep.value++; };
|
||||
const prevStep = () => { if (currentStep.value > 1) currentStep.value--; };
|
||||
|
||||
const submit = () => {
|
||||
form.post('/register', {
|
||||
onSuccess: () => {
|
||||
if (props.onSuccess) props.onSuccess();
|
||||
},
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="register-form-content">
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container mb-8">
|
||||
<div class="flex justify-between mb-2">
|
||||
<span class="text-xs font-bold text-[var(--primary, #df006a)] uppercase tracking-wider">Step {{ currentStep }} of {{ totalSteps }}</span>
|
||||
<span class="text-xs font-bold text-[#888] uppercase tracking-wider">{{ Math.round((currentStep / totalSteps) * 100) }}% Complete</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" :style="{ width: `${(currentStep / totalSteps) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit" class="form-content">
|
||||
<!-- Step 1: Basic Info -->
|
||||
<div v-if="currentStep === 1" class="step-container space-y-4">
|
||||
<div class="input-group" :class="{ 'valid': validation.username.valid && availability.username.available, 'invalid': validation.username.error || availability.username.error }">
|
||||
<Label for="username">Username</Label>
|
||||
<div class="input-wrapper">
|
||||
<Input id="username" v-model="form.username" placeholder="CryptoKing" />
|
||||
<div class="status-icon">
|
||||
<Spinner v-if="availability.username.checking" class="w-4 h-4" />
|
||||
<Check v-else-if="availability.username.available && validation.username.valid" class="text-green-500 w-4 h-4" />
|
||||
<X v-else-if="availability.username.error || validation.username.error" class="text-red-500 w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="availability.username.error" class="text-[10px] text-red-500 mt-1 uppercase font-bold">{{ availability.username.error }}</span>
|
||||
<InputError :message="form.errors.username" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="input-group" :class="{ 'valid': validation.first_name.valid, 'invalid': validation.first_name.error }">
|
||||
<Label for="first_name">First Name</Label>
|
||||
<Input id="first_name" v-model="form.first_name" placeholder="John" />
|
||||
<InputError :message="form.errors.first_name" />
|
||||
</div>
|
||||
<div class="input-group" :class="{ 'valid': validation.last_name.valid, 'invalid': validation.last_name.error }">
|
||||
<Label for="last_name">Last Name</Label>
|
||||
<Input id="last_name" v-model="form.last_name" placeholder="Doe" />
|
||||
<InputError :message="form.errors.last_name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="button" @click="nextStep" class="w-full mt-4" :disabled="!step1Valid">
|
||||
NEXT STEP <ChevronRight class="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Contact & Identity -->
|
||||
<div v-if="currentStep === 2" class="step-container space-y-4">
|
||||
<div class="input-group" :class="{ 'valid': validation.email.valid && availability.email.available, 'invalid': validation.email.error || availability.email.error }">
|
||||
<Label for="email">Email Address</Label>
|
||||
<div class="input-wrapper">
|
||||
<Input id="email" type="email" v-model="form.email" placeholder="john@example.com" />
|
||||
<div class="status-icon">
|
||||
<Spinner v-if="availability.email.checking" class="w-4 h-4" />
|
||||
<Check v-else-if="availability.email.available && validation.email.valid" class="text-green-500 w-4 h-4" />
|
||||
<X v-else-if="availability.email.error || validation.email.error" class="text-red-500 w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="availability.email.error" class="text-[10px] text-red-500 mt-1 uppercase font-bold">{{ availability.email.error }}</span>
|
||||
<InputError :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="input-group" :class="{ 'valid': validation.phone.valid, 'invalid': validation.phone.error }">
|
||||
<Label for="phone">Phone Number</Label>
|
||||
<Input id="phone" v-model="form.phone" placeholder="+49 123 456789" />
|
||||
<InputError :message="form.errors.phone" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="input-group">
|
||||
<Label>Birthdate</Label>
|
||||
<DatePicker v-model="form.birthdate" />
|
||||
<InputError :message="form.errors.birthdate" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<Label>Gender</Label>
|
||||
<Select
|
||||
v-model="form.gender"
|
||||
:options="[
|
||||
{ label: 'Male', value: 'male', icon: 'user' },
|
||||
{ label: 'Female', value: 'female', icon: 'user' },
|
||||
{ label: 'Other', value: 'other', icon: 'user' }
|
||||
]"
|
||||
placeholder="Select Gender"
|
||||
/>
|
||||
<InputError :message="form.errors.gender" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<Button type="button" variant="outline" @click="prevStep" class="flex-1">
|
||||
<ChevronLeft class="mr-2 w-4 h-4" /> BACK
|
||||
</Button>
|
||||
<Button type="button" @click="nextStep" class="flex-[2]" :disabled="!step2Valid">
|
||||
NEXT STEP <ChevronRight class="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Address -->
|
||||
<div v-if="currentStep === 3" class="step-container space-y-4">
|
||||
<div class="input-group">
|
||||
<Label>Country</Label>
|
||||
<CountrySelect v-model="form.country" />
|
||||
<InputError :message="form.errors.country" />
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<Label for="address">Address</Label>
|
||||
<Input id="address" v-model="form.address_line1" placeholder="Main Street 123" />
|
||||
<InputError :message="form.errors.address_line1" />
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<Label for="address_line2">Address Line 2 (Optional)</Label>
|
||||
<Input id="address_line2" v-model="form.address_line2" placeholder="Apartment, suite, etc." />
|
||||
<InputError :message="form.errors.address_line2" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="input-group">
|
||||
<Label for="city">City</Label>
|
||||
<Input id="city" v-model="form.city" placeholder="Berlin" />
|
||||
<InputError :message="form.errors.city" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<Label for="postal">Postal Code</Label>
|
||||
<Input id="postal" v-model="form.postal_code" placeholder="10115" />
|
||||
<InputError :message="form.errors.postal_code" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<Label>Preferred Currency</Label>
|
||||
<Select
|
||||
v-model="form.currency"
|
||||
:options="[
|
||||
{ label: 'EUR - Euro', value: 'EUR', icon: 'euro' },
|
||||
{ label: 'USD - US Dollar', value: 'USD', icon: 'dollar-sign' },
|
||||
{ label: 'BTC - Bitcoin', value: 'BTC', icon: 'bitcoin' }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<Button type="button" variant="outline" @click="prevStep" class="flex-1">
|
||||
<ChevronLeft class="mr-2 w-4 h-4" /> BACK
|
||||
</Button>
|
||||
<Button type="button" @click="nextStep" class="flex-[2]" :disabled="!step3Valid">
|
||||
NEXT STEP <ChevronRight class="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Security -->
|
||||
<div v-if="currentStep === 4" class="step-container space-y-4">
|
||||
<div class="input-group">
|
||||
<div class="flex justify-between">
|
||||
<Label for="password">Password</Label>
|
||||
<button type="button" @click="showPassword = !showPassword" class="text-xs text-[#888]">{{ showPassword ? 'Hide' : 'Show' }}</button>
|
||||
</div>
|
||||
<Input :type="showPassword ? 'text' : 'password'" v-model="form.password" placeholder="Min. 8 characters" />
|
||||
<InputError :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="flex justify-between">
|
||||
<Label for="password_confirmation">Confirm Password</Label>
|
||||
<button type="button" @click="showConfirmPassword = !showConfirmPassword" class="text-xs text-[#888]">{{ showConfirmPassword ? 'Hide' : 'Show' }}</button>
|
||||
</div>
|
||||
<Input :type="showConfirmPassword ? 'text' : 'password'" v-model="form.password_confirmation" placeholder="Repeat password" />
|
||||
<InputError :message="form.errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<div class="reg-checks">
|
||||
<InputError :message="form.errors.is_adult" />
|
||||
<InputError :message="form.errors.terms_accepted" />
|
||||
|
||||
<label class="reg-check-label" :class="{ 'reg-check-active': form.is_adult }">
|
||||
<input type="checkbox" v-model="form.is_adult" class="reg-sr-only" />
|
||||
<span class="reg-box">
|
||||
<Check :size="11" class="reg-tick" stroke-width="3.5" />
|
||||
</span>
|
||||
<span class="reg-check-text">
|
||||
Ich bestätige, dass ich <strong>18 Jahre oder älter</strong> bin und berechtigt bin, an Online-Spielen teilzunehmen.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="reg-check-label" :class="{ 'reg-check-active': form.terms_accepted }">
|
||||
<input type="checkbox" v-model="form.terms_accepted" class="reg-sr-only" />
|
||||
<span class="reg-box">
|
||||
<Check :size="11" class="reg-tick" stroke-width="3.5" />
|
||||
</span>
|
||||
<span class="reg-check-text">
|
||||
Ich habe die <a href="/terms" target="_blank" class="reg-link">Nutzungsbedingungen</a> und <a href="/privacy" target="_blank" class="reg-link">Datenschutzerklärung</a> gelesen und akzeptiert.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<Button type="button" variant="outline" @click="prevStep" class="flex-1">
|
||||
<ChevronLeft class="mr-2 w-4 h-4" /> BACK
|
||||
</Button>
|
||||
<Button type="submit" class="flex-[2] neon-button" :disabled="form.processing || !step4Valid">
|
||||
<Spinner v-if="form.processing" class="mr-2 w-4 h-4" />
|
||||
FINISH REGISTRATION
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.progress-track {
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary, #df006a), #00f2ff);
|
||||
box-shadow: 0 0 10px rgba(223, 0, 106, 0.5);
|
||||
transition: width 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.neon-button {
|
||||
background: linear-gradient(90deg, var(--primary, #df006a), color-mix(in srgb, var(--primary, #df006a) 70%, #000));
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 0 20px rgba(223, 0, 106, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.neon-button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 40px rgba(223, 0, 106, 0.7);
|
||||
}
|
||||
|
||||
.valid :deep(input) {
|
||||
border-color: rgba(34, 197, 94, 0.3) !important;
|
||||
}
|
||||
|
||||
.invalid :deep(input) {
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
}
|
||||
|
||||
/* ── Custom Checkboxes (Step 4) ─────────────────────────── */
|
||||
.reg-checks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.reg-sr-only {
|
||||
position: absolute; width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px; overflow: hidden;
|
||||
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
||||
}
|
||||
|
||||
.reg-check-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 11px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.06);
|
||||
background: rgba(255,255,255,.02);
|
||||
cursor: pointer;
|
||||
transition: border-color .2s, background .2s;
|
||||
user-select: none;
|
||||
}
|
||||
.reg-check-label:hover {
|
||||
border-color: rgba(223,0,106,.2);
|
||||
background: rgba(223,0,106,.03);
|
||||
}
|
||||
.reg-check-active {
|
||||
border-color: rgba(223,0,106,.3) !important;
|
||||
background: rgba(223,0,106,.05) !important;
|
||||
box-shadow: 0 0 0 1px rgba(223,0,106,.1);
|
||||
}
|
||||
|
||||
.reg-box {
|
||||
flex-shrink: 0;
|
||||
width: 20px; height: 20px;
|
||||
margin-top: 1px;
|
||||
background: rgba(0,0,0,.4);
|
||||
border: 1.5px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: .2s;
|
||||
}
|
||||
.reg-check-active .reg-box {
|
||||
background: var(--primary, #df006a);
|
||||
border-color: var(--primary, #df006a);
|
||||
box-shadow: 0 0 12px rgba(223,0,106,.4);
|
||||
}
|
||||
|
||||
.reg-tick {
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: .15s cubic-bezier(.175,.885,.32,1.275);
|
||||
}
|
||||
.reg-check-active .reg-tick {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.reg-check-text {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
line-height: 1.55;
|
||||
transition: color .2s;
|
||||
}
|
||||
.reg-check-label:hover .reg-check-text,
|
||||
.reg-check-active .reg-check-text { color: #aaa; }
|
||||
|
||||
.reg-check-text strong { color: #bbb; font-weight: 700; }
|
||||
|
||||
.reg-link {
|
||||
color: var(--primary, #df006a);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: .15s;
|
||||
}
|
||||
.reg-link:hover { text-decoration: underline; filter: brightness(1.2); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user