Files
BetiX/resources/js/pages/settings/Profile.vue
Dolo 0280278978
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
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

408 lines
22 KiB
Vue

<script setup lang="ts">
import { Head, usePage } from '@inertiajs/vue3';
import { ref, onMounted, nextTick, computed, watch } from 'vue';
import Select from '@/components/ui/Select.vue';
import { usePrimaryColor } from '@/composables/usePrimaryColor';
import UserLayout from '../../layouts/user/userlayout.vue';
type Props = { mustVerifyEmail: boolean; status?: string };
defineProps<Props>();
const page = usePage();
const user = computed(() => (page.props.auth as any).user || {});
const form = ref({
name: '',
email: '',
first_name: '',
last_name: '',
birthdate: '',
gender: '',
country: '',
address_line1: '',
address_line2: '',
city: '',
state: '',
postal_code: '',
phone: '',
});
const locked = ref<Record<string, boolean>>({});
const isSaving = ref(false);
const showSuccess = ref(false);
// Accent color controls for quick access on Profile page
const { primaryColor, updatePrimaryColor } = usePrimaryColor();
const hexInput = ref<string>(primaryColor.value || '#ff007a');
watch(primaryColor, (val) => {
if (val && val.toLowerCase() !== hexInput.value.toLowerCase()) {
hexInput.value = val;
}
});
function onPickColor(e: Event) {
const value = (e.target as HTMLInputElement).value;
hexInput.value = value;
updatePrimaryColor(value);
}
function onHexInput(e: Event) {
const value = (e.target as HTMLInputElement).value.trim();
hexInput.value = value;
}
function applyHex() {
let v = hexInput.value.trim();
if (!v.startsWith('#')) v = `#${v}`;
const isValid = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v);
if (isValid) {
hexInput.value = v.toLowerCase();
updatePrimaryColor(hexInput.value);
}
}
function resetToDefault() {
const def = '#ff007a';
hexInput.value = def;
updatePrimaryColor(def);
}
onMounted(() => {
const u: any = user.value || {};
// Map fields
form.value = {
name: u.name || '',
email: u.email || '',
first_name: u.first_name || '',
last_name: u.last_name || '',
birthdate: u.birthdate ? String(u.birthdate).slice(0, 10) : '',
gender: u.gender || '',
country: (u.country || '').toString().toUpperCase(),
address_line1: u.address_line1 || '',
address_line2: u.address_line2 || '',
city: u.city || '',
state: u.state || '',
postal_code: u.postal_code || '',
phone: u.phone || '',
};
// Determine locks: If data exists in DB, it is locked.
const fields = Object.keys(form.value);
fields.forEach(f => {
if (u[f] && String(u[f]).trim() !== '') {
locked.value[f] = true;
}
});
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
});
async function submit() {
isSaving.value = true;
const res = await fetch('/settings/profile', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || '',
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...form.value, _method: 'PATCH' }),
});
if (res.ok) {
showSuccess.value = true;
setTimeout(() => { window.location.reload(); }, 1000);
} else {
isSaving.value = false;
}
}
</script>
<template>
<UserLayout>
<Head title="Profile Settings" />
<section class="content">
<div class="wrap">
<div class="panel main-panel">
<header class="page-head">
<div class="head-flex">
<div class="title-group">
<div class="title">User Profile</div>
<p class="subtitle">Manage your personal identity and contact details</p>
</div>
<div class="security-badge">
<i data-lucide="shield-check"></i>
<span>Verified Account</span>
</div>
</div>
</header>
<div class="settings-nav">
<a href="/settings/profile" class="nav-item active"><i data-lucide="user"></i> Profile</a>
<a href="/settings" class="nav-item"><i data-lucide="globe"></i> Public Profile</a>
<a href="/settings/kyc" class="nav-item"><i data-lucide="file-check"></i> KYC</a>
<a href="/settings/security" class="nav-item"><i data-lucide="lock"></i> Security</a>
<a href="/settings/two-factor" class="nav-item"><i data-lucide="shield"></i> 2FA</a>
</div>
<form class="profile-form" @submit.prevent="submit">
<div class="form-section">
<div class="sec-label">Account Core</div>
<div class="grid2">
<label class="field" :class="{ locked: locked.email }">
<span class="lbl">Email Address</span>
<div class="input-wrapper">
<i data-lucide="mail"></i>
<input class="inp" type="email" v-model="form.email" :disabled="locked.email" required />
<i v-if="locked.email" data-lucide="lock" class="lock-icon"></i>
</div>
<div v-if="locked.email" class="locked-hint">Contact support to change</div>
</label>
<label class="field" :class="{ locked: locked.name }">
<span class="lbl">Display Name</span>
<div class="input-wrapper">
<i data-lucide="user"></i>
<input class="inp" type="text" v-model="form.name" :disabled="locked.name" required />
<i v-if="locked.name" data-lucide="lock" class="lock-icon"></i>
</div>
<div v-if="locked.name" class="locked-hint">Contact support to change</div>
</label>
</div>
</div>
<div class="form-section">
<div class="sec-label">Identity Details</div>
<div class="grid2">
<label class="field" :class="{ locked: locked.first_name }">
<span class="lbl">First Name</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.first_name" :disabled="locked.first_name" />
<i v-if="locked.first_name" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
<label class="field" :class="{ locked: locked.last_name }">
<span class="lbl">Last Name</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.last_name" :disabled="locked.last_name" />
<i v-if="locked.last_name" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
<div class="grid3">
<label class="field" :class="{ locked: locked.birthdate }">
<span class="lbl">Birthdate</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="date" v-model="form.birthdate" :disabled="locked.birthdate" />
<i v-if="locked.birthdate" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
<label class="field" :class="{ locked: locked.gender }">
<span class="lbl">Gender</span>
<div class="input-wrapper">
<Select
v-model="form.gender"
:disabled="locked.gender"
:options="[
{ label: 'Male', value: 'male', icon: 'user' },
{ label: 'Female', value: 'female', icon: 'user' },
{ label: 'Other', value: 'other', icon: 'user' }
]"
placeholder="Select Gender"
/>
<i v-if="locked.gender" data-lucide="lock" class="lock-icon" style="right: 30px; left: auto; z-index: 10;"></i>
</div>
</label>
<label class="field" :class="{ locked: locked.country }">
<span class="lbl">Country (ISO)</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.country" placeholder="e.g. DE" maxlength="2" :disabled="locked.country" />
<i v-if="locked.country" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
</div>
<div class="form-section">
<div class="sec-label">Residential Address</div>
<div class="grid1">
<label class="field" :class="{ locked: locked.address_line1 }">
<span class="lbl">Address Line 1</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.address_line1" placeholder="Street and house number" :disabled="locked.address_line1" />
<i v-if="locked.address_line1" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
<div class="grid2">
<label class="field" :class="{ locked: locked.address_line2 }">
<span class="lbl">Address Line 2 (Optional)</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.address_line2" :disabled="locked.address_line2" />
<i v-if="locked.address_line2" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
<label class="field" :class="{ locked: locked.city }">
<span class="lbl">City</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.city" :disabled="locked.city" />
<i v-if="locked.city" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
<div class="grid2">
<label class="field" :class="{ locked: locked.state }">
<span class="lbl">State / Region</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.state" :disabled="locked.state" />
<i v-if="locked.state" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
<label class="field" :class="{ locked: locked.postal_code }">
<span class="lbl">Postal Code</span>
<div class="input-wrapper">
<input class="inp pl-reset" type="text" v-model="form.postal_code" :disabled="locked.postal_code" />
<i v-if="locked.postal_code" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
</div>
<div class="form-section">
<div class="sec-label">Contact Information</div>
<div class="grid1">
<label class="field" :class="{ locked: locked.phone }">
<span class="lbl">Mobile Phone</span>
<div class="input-wrapper">
<i data-lucide="phone"></i>
<input class="inp" type="tel" v-model="form.phone" placeholder="+49 123 456789" :disabled="locked.phone" />
<i v-if="locked.phone" data-lucide="lock" class="lock-icon"></i>
</div>
</label>
</div>
</div>
<div class="form-section">
<div class="sec-label">Appearance</div>
<div class="grid1">
<div class="field">
<span class="lbl">Accent color</span>
<div class="input-wrapper" style="gap:8px; align-items:center;">
<input class="inp pl-reset" type="color" :value="primaryColor" @input="onPickColor" style="width:46px; height:36px; padding:0;" />
<input class="inp pl-reset" type="text" v-model="hexInput" @input="onHexInput" placeholder="#ff007a" style="max-width:160px;" />
<button type="button" class="inp pl-reset" @click="applyHex" style="width:auto; cursor:pointer;">Apply</button>
<button type="button" class="inp pl-reset" @click="resetToDefault" style="width:auto; cursor:pointer;">Reset</button>
</div>
<div class="locked-hint" style="margin-top:6px; display:flex; align-items:center; gap:6px;">
<span>Preview:</span>
<span :style="{background: primaryColor, display:'inline-block', width:'18px', height:'12px', borderRadius:'4px', border:'1px solid #222'}"></span>
<code style="color:#aaa;">{{ primaryColor }}</code>
</div>
</div>
</div>
</div>
<div class="form-footer">
<button class="btn-save" type="submit" :disabled="isSaving">
<span v-if="!isSaving">{{ showSuccess ? 'Updated!' : 'Save Protocol' }}</span>
<div v-else class="loader"></div>
</button>
</div>
</form>
</div>
</div>
</section>
</UserLayout>
</template>
<style scoped>
:global(:root) {
--bg-card: #0a0a0a;
--border: #151515;
--cyan: #00f2ff;
--magenta: #ff007a;
--green: #00ff9d;
}
.content { padding: 30px; animation: fade-in 0.8s cubic-bezier(0.2, 0, 0, 1); }
.wrap { max-width: 900px; margin: 0 auto; }
.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); }
/* Header */
.page-head { padding: 25px 30px; 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; }
.title { font-size: 14px; font-weight: 900; color: #fff; letter-spacing: 3px; text-transform: uppercase; }
.subtitle { color: #555; font-size: 12px; margin-top: 4px; font-weight: 600; }
.security-badge { display: flex; align-items: center; gap: 8px; color: var(--green); background: rgba(0,255,157,0.05); padding: 6px 14px; border-radius: 50px; border: 1px solid rgba(0,255,157,0.1); font-size: 10px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px; }
.security-badge i { width: 14px; }
/* Navigation */
.settings-nav { display: flex; gap: 5px; padding: 15px 30px; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2); overflow-x: auto; }
.nav-item { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; font-size: 11px; font-weight: 800; color: #666; text-transform: uppercase; letter-spacing: 1px; transition: 0.2s; text-decoration: none; }
.nav-item:hover { color: #fff; background: rgba(255,255,255,0.05); }
.nav-item.active { color: var(--cyan); background: rgba(0,242,255,0.1); }
.nav-item i { width: 14px; }
/* Form Layout */
.profile-form { padding: 30px; display: grid; gap: 30px; }
.form-section { display: grid; gap: 15px; animation: slide-up 0.6s cubic-bezier(0.2, 0, 0, 1) backwards; }
.form-section:nth-child(1) { animation-delay: 0.1s; }
.form-section:nth-child(2) { animation-delay: 0.2s; }
.form-section:nth-child(3) { animation-delay: 0.3s; }
.form-section:nth-child(4) { animation-delay: 0.4s; }
.sec-label { font-size: 10px; font-weight: 900; color: #333; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 5px; border-bottom: 1px solid #111; padding-bottom: 8px; }
.grid1 { display: grid; gap: 15px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; }
/* Inputs */
.field { display: block; position: relative; }
.lbl { display: block; font-size: 10px; font-weight: 900; color: #555; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; transition: 0.3s; }
.input-wrapper { position: relative; display: flex; align-items: center; }
.input-wrapper i:not(.lock-icon) { position: absolute; left: 14px; width: 14px; color: #444; pointer-events: none; transition: 0.3s; }
.input-wrapper .inp { padding-left: 40px; }
.input-wrapper .inp.pl-reset { padding-left: 14px; }
.inp, select { width: 100%; background: #000; border: 1px solid var(--border); color: #fff; padding: 12px 14px; border-radius: 12px; font-size: 13px; transition: 0.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); }
/* Locked State */
.inp:disabled { background: #080808; color: #666; border-color: #111; cursor: not-allowed; }
.lock-icon { position: absolute; right: 14px; width: 12px; color: #333; pointer-events: none; }
.locked-hint { font-size: 9px; color: #444; margin-top: 6px; font-weight: 700; display: flex; align-items: center; gap: 5px; }
.locked-hint::before { content: ''; display: block; width: 4px; height: 4px; background: #333; border-radius: 50%; }
/* Custom Select */
.select-wrapper { position: relative; }
.select-wrapper::after { content: '↓'; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 10px; color: #444; pointer-events: none; }
/* Actions & Footer */
.form-footer { display: flex; justify-content: flex-end; align-items: center; margin-top: 20px; padding-top: 25px; border-top: 1px solid var(--border); animation: fade-in 1s 0.5s backwards; }
.btn-save { background: var(--cyan); color: #000; border: none; border-radius: 12px; padding: 14px 30px; font-size: 12px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px; cursor: pointer; transition: 0.3s; box-shadow: 0 0 20px rgba(0,242,255,0.2); min-width: 160px; display: flex; justify-content: center; }
.btn-save:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 25px rgba(0,242,255,0.4); }
.btn-save:disabled { opacity: 0.6; cursor: not-allowed; }
.loader { width: 16px; height: 16px; border: 2px solid rgba(0,0,0,0.1); border-top-color: #000; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slide-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 768px) {
.grid2, .grid3 { grid-template-columns: 1fr; }
.form-footer { flex-direction: column-reverse; gap: 20px; align-items: stretch; }
.profile-form { padding: 20px; }
.settings-nav { padding: 10px 15px; }
}
</style>