786 lines
23 KiB
Vue
786 lines
23 KiB
Vue
<!-- pages/Profile/Profil.vue -->
|
|
<template>
|
|
<v-container fluid class="pa-0">
|
|
<!-- Hero Header Section -->
|
|
<div class="hero-header">
|
|
<v-container class="py-8">
|
|
<div class="d-flex align-center">
|
|
<v-icon color="white" size="40" class="mr-3">mdi-account-circle</v-icon>
|
|
<div>
|
|
<h1 class="text-h4 font-weight-bold text-white mb-1">Profil Saya</h1>
|
|
<p class="text-body-1 text-white opacity-90">Kelola informasi profil dan pengaturan akun Anda</p>
|
|
</div>
|
|
</div>
|
|
</v-container>
|
|
</div>
|
|
|
|
<v-container class="content-container pb-12">
|
|
<v-row>
|
|
<!-- Left Sidebar - Profile Card -->
|
|
<v-col cols="12" lg="4">
|
|
<v-card class="rounded-xl elevation-8 sticky-card profile-card">
|
|
<div class="text-center profile-content">
|
|
<div class="profile-avatar-container">
|
|
<v-avatar size="140" class="profile-avatar elevation-8">
|
|
<v-img
|
|
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
|
|
:alt="`${user?.name || 'User'} Profile`"
|
|
></v-img>
|
|
</v-avatar>
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
color="orange-darken-2"
|
|
class="avatar-edit-btn elevation-4"
|
|
@click="openPhotoDialog"
|
|
>
|
|
<v-icon size="18">mdi-camera</v-icon>
|
|
</v-btn>
|
|
</div>
|
|
|
|
<h2 class="text-h5 font-weight-bold mb-2">
|
|
{{ user?.name || user?.preferred_username || 'User' }}
|
|
</h2>
|
|
<p class="text-body-2 text-grey-darken-1 mb-1">
|
|
@{{ user?.preferred_username || profileData.username }}
|
|
</p>
|
|
<p class="text-body-2 text-grey mb-4">
|
|
{{ user?.email || 'No email' }}
|
|
</p>
|
|
|
|
<v-chip
|
|
color="success"
|
|
variant="flat"
|
|
size="small"
|
|
prepend-icon="mdi-check-circle"
|
|
class="mb-4"
|
|
>
|
|
Akun Terverifikasi
|
|
</v-chip>
|
|
|
|
<v-divider class="my-4"></v-divider>
|
|
|
|
<!-- Quick Info -->
|
|
<div class="text-left px-6">
|
|
<div class="info-item mb-3">
|
|
<v-icon size="20" color="blue-darken-2" class="mr-2">mdi-identifier</v-icon>
|
|
<span class="text-body-2 text-grey-darken-1">
|
|
ID: {{ user?.id ? user.id.substring(0, 12) : 'N/A' }}
|
|
</span>
|
|
</div>
|
|
<div class="info-item mb-3">
|
|
<v-icon size="20" color="orange-darken-2" class="mr-2">mdi-calendar-check</v-icon>
|
|
<span class="text-body-2 text-grey-darken-1">
|
|
Bergabung: {{ profileData.joinDate }}
|
|
</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<v-icon size="20" color="green-darken-2" class="mr-2">mdi-clock-outline</v-icon>
|
|
<span class="text-body-2 text-grey-darken-1">
|
|
Login: {{ profileData.lastLogin }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<v-card-actions class="pa-4">
|
|
<v-btn
|
|
color="blue-darken-2"
|
|
variant="outlined"
|
|
block
|
|
class="rounded-lg"
|
|
prepend-icon="mdi-cog"
|
|
@click="navigateTo('/Profile/Pengaturan')"
|
|
>
|
|
Pengaturan Akun
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Right Content - Profile Details -->
|
|
<v-col cols="12" lg="8">
|
|
<!-- Personal Information -->
|
|
<v-card class="rounded-xl elevation-4 mb-4">
|
|
<v-card-title class="d-flex align-center pa-6 pb-4">
|
|
<v-icon color="blue-darken-2" class="mr-2">mdi-account-details</v-icon>
|
|
<span class="font-weight-bold">Informasi Pribadi</span>
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
v-if="!isEditing"
|
|
color="orange-darken-2"
|
|
variant="flat"
|
|
size="small"
|
|
class="rounded-lg"
|
|
prepend-icon="mdi-pencil"
|
|
@click="startEdit"
|
|
>
|
|
Edit
|
|
</v-btn>
|
|
</v-card-title>
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-text class="pa-6">
|
|
<v-form ref="profileForm">
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">NAMA LENGKAP</label>
|
|
<v-text-field
|
|
v-model="profileData.name"
|
|
placeholder="Masukkan nama lengkap"
|
|
prepend-inner-icon="mdi-account"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
:readonly="!isEditing"
|
|
hide-details="auto"
|
|
class="mb-4"
|
|
></v-text-field>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">USERNAME</label>
|
|
<v-text-field
|
|
v-model="profileData.username"
|
|
placeholder="Masukkan username"
|
|
prepend-inner-icon="mdi-at"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
:readonly="!isEditing"
|
|
hide-details="auto"
|
|
class="mb-4"
|
|
></v-text-field>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">EMAIL</label>
|
|
<v-text-field
|
|
v-model="profileData.email"
|
|
placeholder="Masukkan email"
|
|
prepend-inner-icon="mdi-email"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
:readonly="!isEditing"
|
|
hide-details="auto"
|
|
class="mb-4"
|
|
></v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-divider v-if="isEditing"></v-divider>
|
|
<v-card-actions v-if="isEditing" class="pa-6 pt-4">
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
variant="outlined"
|
|
color="grey-darken-1"
|
|
class="rounded-lg px-6"
|
|
@click="cancelEdit"
|
|
>
|
|
Batal
|
|
</v-btn>
|
|
<v-btn
|
|
color="blue-darken-2"
|
|
variant="flat"
|
|
class="rounded-lg px-8"
|
|
prepend-icon="mdi-check"
|
|
@click="saveProfile"
|
|
:loading="isSaving"
|
|
>
|
|
Simpan Perubahan
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
|
|
<!-- Security Section -->
|
|
<v-card class="rounded-xl elevation-4 mb-4">
|
|
<v-card-title class="d-flex align-center pa-6 pb-4">
|
|
<v-icon color="orange-darken-2" class="mr-2">mdi-shield-check</v-icon>
|
|
<span class="font-weight-bold">Keamanan</span>
|
|
</v-card-title>
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-text class="pa-6">
|
|
<v-list class="transparent">
|
|
<v-list-item
|
|
class="rounded-lg mb-2 px-4"
|
|
prepend-icon="mdi-lock-reset"
|
|
@click="openPasswordDialog"
|
|
>
|
|
<v-list-item-title class="font-weight-medium">Ubah Password</v-list-item-title>
|
|
<v-list-item-subtitle class="text-caption">Perbarui password akun Anda</v-list-item-subtitle>
|
|
<template v-slot:append>
|
|
<v-icon color="grey">mdi-chevron-right</v-icon>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
|
|
<!-- Change Password Dialog -->
|
|
<v-dialog v-model="passwordDialog" max-width="500" persistent>
|
|
<v-card class="rounded-xl">
|
|
<v-card-title class="pa-6 pb-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon color="orange-darken-2" class="mr-2">mdi-lock-reset</v-icon>
|
|
<span class="font-weight-bold">Ubah Password</span>
|
|
</div>
|
|
</v-card-title>
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-text class="pa-6">
|
|
<v-form ref="passwordForm">
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">PASSWORD SAAT INI</label>
|
|
<v-text-field
|
|
v-model="passwordData.current"
|
|
type="password"
|
|
prepend-inner-icon="mdi-lock"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
hide-details="auto"
|
|
class="mb-4"
|
|
></v-text-field>
|
|
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">PASSWORD BARU</label>
|
|
<v-text-field
|
|
v-model="passwordData.new"
|
|
type="password"
|
|
prepend-inner-icon="mdi-lock-plus"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
hide-details="auto"
|
|
class="mb-4"
|
|
></v-text-field>
|
|
|
|
<label class="text-caption text-grey-darken-1 font-weight-bold mb-1 d-block">KONFIRMASI PASSWORD</label>
|
|
<v-text-field
|
|
v-model="passwordData.confirm"
|
|
type="password"
|
|
prepend-inner-icon="mdi-lock-check"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
color="blue-darken-2"
|
|
hide-details="auto"
|
|
></v-text-field>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-divider></v-divider>
|
|
<v-card-actions class="pa-6 pt-4">
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
variant="outlined"
|
|
color="grey-darken-1"
|
|
class="rounded-lg"
|
|
@click="passwordDialog = false"
|
|
>
|
|
Batal
|
|
</v-btn>
|
|
<v-btn
|
|
color="orange-darken-2"
|
|
variant="flat"
|
|
class="rounded-lg px-6"
|
|
@click="changePassword"
|
|
:loading="isChangingPassword"
|
|
>
|
|
Ubah Password
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Active Devices Dialog -->
|
|
<v-dialog v-model="devicesDialog" max-width="700" scrollable>
|
|
<v-card class="rounded-xl">
|
|
<v-card-title class="pa-6 pb-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon color="blue-darken-2" class="mr-2">mdi-devices</v-icon>
|
|
<span class="font-weight-bold">Perangkat Aktif</span>
|
|
</div>
|
|
</v-card-title>
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-text class="pa-0" style="max-height: 500px;">
|
|
<v-list class="py-0">
|
|
<v-list-item
|
|
v-for="(session, index) in activeSessions"
|
|
:key="index"
|
|
class="py-4 px-6"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-avatar :color="session.current ? 'success' : 'blue-grey-lighten-4'" size="48">
|
|
<v-icon :color="session.current ? 'white' : 'blue-grey-darken-2'">
|
|
{{ session.icon }}
|
|
</v-icon>
|
|
</v-avatar>
|
|
</template>
|
|
|
|
<v-list-item-title class="font-weight-bold mb-1">
|
|
{{ session.device }}
|
|
<v-chip v-if="session.current" size="x-small" color="success" class="ml-2">
|
|
Sesi Ini
|
|
</v-chip>
|
|
</v-list-item-title>
|
|
<v-list-item-subtitle class="text-caption">
|
|
<div>{{ session.location }}</div>
|
|
<div class="text-grey-darken-1 mt-1">
|
|
<v-icon size="12">mdi-clock-outline</v-icon>
|
|
{{ session.lastActive }}
|
|
</div>
|
|
</v-list-item-subtitle>
|
|
|
|
<template v-slot:append v-if="!session.current">
|
|
<v-btn
|
|
icon="mdi-close-circle"
|
|
size="small"
|
|
variant="text"
|
|
color="error"
|
|
@click="removeSession(index)"
|
|
></v-btn>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
|
|
<v-divider></v-divider>
|
|
<v-card-actions class="pa-6 pt-4">
|
|
<v-btn
|
|
color="error"
|
|
variant="outlined"
|
|
class="rounded-lg"
|
|
prepend-icon="mdi-logout-variant"
|
|
@click="logoutAllDevices"
|
|
>
|
|
Keluar dari Semua Perangkat
|
|
</v-btn>
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
variant="text"
|
|
class="rounded-lg"
|
|
@click="devicesDialog = false"
|
|
>
|
|
Tutup
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Photo Upload Dialog -->
|
|
<v-dialog v-model="photoDialog" max-width="400">
|
|
<v-card class="rounded-xl">
|
|
<v-card-title class="pa-6 pb-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon color="blue-darken-2" class="mr-2">mdi-camera</v-icon>
|
|
<span class="font-weight-bold">Ubah Foto Profil</span>
|
|
</div>
|
|
</v-card-title>
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-text class="pa-6 text-center">
|
|
<v-avatar size="150" class="mb-4">
|
|
<v-img :src="user?.picture || 'https://i.pravatar.cc/300?img=68'"></v-img>
|
|
</v-avatar>
|
|
<v-file-input
|
|
label="Pilih foto baru"
|
|
variant="outlined"
|
|
prepend-icon=""
|
|
prepend-inner-icon="mdi-image"
|
|
accept="image/*"
|
|
hide-details
|
|
></v-file-input>
|
|
</v-card-text>
|
|
|
|
<v-divider></v-divider>
|
|
<v-card-actions class="pa-6 pt-4">
|
|
<v-btn
|
|
color="error"
|
|
variant="text"
|
|
class="rounded-lg"
|
|
>
|
|
Hapus Foto
|
|
</v-btn>
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
variant="outlined"
|
|
color="grey-darken-1"
|
|
class="rounded-lg"
|
|
@click="photoDialog = false"
|
|
>
|
|
Batal
|
|
</v-btn>
|
|
<v-btn
|
|
color="blue-darken-2"
|
|
variant="flat"
|
|
class="rounded-lg px-6"
|
|
>
|
|
Simpan
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Success Snackbar -->
|
|
<v-snackbar
|
|
v-model="snackbar"
|
|
:color="snackbarColor"
|
|
location="top"
|
|
timeout="3000"
|
|
class="snackbar-custom"
|
|
>
|
|
<div class="d-flex align-center">
|
|
<v-icon class="mr-2">{{ snackbarIcon }}</v-icon>
|
|
<span>{{ snackbarMessage }}</span>
|
|
</div>
|
|
<template v-slot:actions>
|
|
<v-btn
|
|
variant="text"
|
|
size="small"
|
|
icon="mdi-close"
|
|
@click="snackbar = false"
|
|
></v-btn>
|
|
</template>
|
|
</v-snackbar>
|
|
</v-container>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
|
import { navigateTo } from '#app';
|
|
// Explicitly import useAuth composable
|
|
import { useAuth } from '~/composables/useAuth';
|
|
|
|
definePageMeta({
|
|
middleware: 'auth'
|
|
});
|
|
|
|
// Get user data from your custom useAuth composable
|
|
const { user, isLoading: authLoading, checkAuth } = useAuth();
|
|
|
|
const isEditing = ref(false);
|
|
const isSaving = ref(false);
|
|
const isChangingPassword = ref(false);
|
|
const snackbar = ref(false);
|
|
const snackbarMessage = ref('');
|
|
const snackbarColor = ref('success');
|
|
|
|
const passwordDialog = ref(false);
|
|
const devicesDialog = ref(false);
|
|
const photoDialog = ref(false);
|
|
|
|
const twoFactorEnabled = ref(false);
|
|
const emailNotifications = ref(true);
|
|
const darkMode = ref(false);
|
|
|
|
const snackbarIcon = computed(() => {
|
|
return snackbarColor.value === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle';
|
|
});
|
|
|
|
// Initialize profile data with user info
|
|
const profileData = reactive({
|
|
id: '',
|
|
name: '',
|
|
username: '',
|
|
email: '',
|
|
phone: '',
|
|
bio: '',
|
|
picture: '',
|
|
joinDate: '15 Januari 2024',
|
|
lastLogin: 'Memuat...' // Will be updated from database
|
|
});
|
|
|
|
const passwordData = reactive({
|
|
current: '',
|
|
new: '',
|
|
confirm: ''
|
|
});
|
|
|
|
const activeSessions = ref([
|
|
{
|
|
device: 'Chrome on Windows 11',
|
|
location: 'Sidoarjo, Indonesia',
|
|
lastActive: 'Sekarang',
|
|
icon: 'mdi-laptop',
|
|
current: true
|
|
},
|
|
{
|
|
device: 'Mobile App on Android',
|
|
location: 'Surabaya, Indonesia',
|
|
lastActive: '2 jam yang lalu',
|
|
icon: 'mdi-cellphone',
|
|
current: false
|
|
},
|
|
{
|
|
device: 'Safari on macOS',
|
|
location: 'Jakarta, Indonesia',
|
|
lastActive: '1 hari yang lalu',
|
|
icon: 'mdi-laptop',
|
|
current: false
|
|
}
|
|
]);
|
|
|
|
const originalData = ref({});
|
|
|
|
// Format last login time
|
|
const formatLastLogin = (timestamp) => {
|
|
if (!timestamp) return 'Belum pernah login';
|
|
|
|
// Convert Unix timestamp (seconds) to Date object
|
|
const date = new Date(timestamp * 1000);
|
|
|
|
// Format like: "10 Desember 2025 pukul 13.00"
|
|
return date.toLocaleString('id-ID', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false
|
|
}).replace(',', ' pukul');
|
|
};
|
|
|
|
// Fetch user data from database
|
|
const fetchUserData = async () => {
|
|
try {
|
|
const userData = await $fetch('/api/users/current');
|
|
if (userData) {
|
|
// Get lastLogin from database
|
|
const users = await $fetch('/api/users/list');
|
|
const dbUser = users.find(u => u.id === userData.id);
|
|
|
|
if (dbUser && dbUser.lastLogin) {
|
|
profileData.lastLogin = formatLastLogin(dbUser.lastLogin);
|
|
} else {
|
|
// Fallback to current time if not found
|
|
profileData.lastLogin = formatLastLogin(Math.floor(Date.now() / 1000));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch user data:', error);
|
|
// Fallback to current time on error
|
|
profileData.lastLogin = formatLastLogin(Math.floor(Date.now() / 1000));
|
|
}
|
|
};
|
|
|
|
// Sync profile data with user data
|
|
const syncUserData = () => {
|
|
if (user.value) {
|
|
profileData.id = user.value.id || '';
|
|
profileData.name = user.value.name || user.value.preferred_username || '';
|
|
profileData.username = user.value.preferred_username || user.value.email?.split('@')[0] || '';
|
|
profileData.email = user.value.email || '';
|
|
profileData.picture = user.value.picture || 'https://i.pravatar.cc/300?img=68';
|
|
// phone and bio can be loaded from additional user data if available
|
|
profileData.phone = user.value.phone_number || '';
|
|
profileData.bio = user.value.bio || '';
|
|
}
|
|
};
|
|
|
|
// Watch for user changes and sync
|
|
watch(user, () => {
|
|
syncUserData();
|
|
}, { immediate: true });
|
|
|
|
onMounted(async () => {
|
|
// Check authentication status on mount
|
|
await checkAuth();
|
|
syncUserData();
|
|
// Fetch lastLogin from database
|
|
await fetchUserData();
|
|
originalData.value = { ...profileData };
|
|
});
|
|
|
|
const startEdit = () => {
|
|
originalData.value = { ...profileData };
|
|
isEditing.value = true;
|
|
};
|
|
|
|
const cancelEdit = () => {
|
|
Object.assign(profileData, originalData.value);
|
|
isEditing.value = false;
|
|
};
|
|
|
|
const saveProfile = async () => {
|
|
isSaving.value = true;
|
|
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// TODO: Replace with actual API call to update user profile
|
|
// await $fetch('/api/profile/update', {
|
|
// method: 'PUT',
|
|
// body: {
|
|
// name: profileData.name,
|
|
// username: profileData.username,
|
|
// email: profileData.email,
|
|
// phone: profileData.phone,
|
|
// bio: profileData.bio
|
|
// }
|
|
// });
|
|
|
|
originalData.value = { ...profileData };
|
|
isEditing.value = false;
|
|
snackbarMessage.value = 'Profil berhasil diperbarui!';
|
|
snackbarColor.value = 'success';
|
|
snackbar.value = true;
|
|
} catch (error) {
|
|
snackbarMessage.value = 'Gagal memperbarui profil!';
|
|
snackbarColor.value = 'error';
|
|
snackbar.value = true;
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
};
|
|
|
|
const openPhotoDialog = () => {
|
|
photoDialog.value = true;
|
|
};
|
|
|
|
const openPasswordDialog = () => {
|
|
passwordDialog.value = true;
|
|
passwordData.current = '';
|
|
passwordData.new = '';
|
|
passwordData.confirm = '';
|
|
};
|
|
|
|
const changePassword = async () => {
|
|
if (passwordData.new !== passwordData.confirm) {
|
|
snackbarMessage.value = 'Password baru tidak cocok!';
|
|
snackbarColor.value = 'error';
|
|
snackbar.value = true;
|
|
return;
|
|
}
|
|
|
|
isChangingPassword.value = true;
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// TODO: Replace with actual API call
|
|
|
|
passwordDialog.value = false;
|
|
snackbarMessage.value = 'Password berhasil diubah!';
|
|
snackbarColor.value = 'success';
|
|
snackbar.value = true;
|
|
} catch (error) {
|
|
snackbarMessage.value = 'Gagal mengubah password!';
|
|
snackbarColor.value = 'error';
|
|
snackbar.value = true;
|
|
} finally {
|
|
isChangingPassword.value = false;
|
|
}
|
|
};
|
|
|
|
const openDevicesDialog = () => {
|
|
devicesDialog.value = true;
|
|
};
|
|
|
|
const removeSession = (index) => {
|
|
activeSessions.value.splice(index, 1);
|
|
snackbarMessage.value = 'Perangkat berhasil dihapus dari sesi aktif';
|
|
snackbarColor.value = 'success';
|
|
snackbar.value = true;
|
|
};
|
|
|
|
const logoutAllDevices = () => {
|
|
// Keep only current session
|
|
activeSessions.value = activeSessions.value.filter(s => s.current);
|
|
devicesDialog.value = false;
|
|
snackbarMessage.value = 'Berhasil keluar dari semua perangkat lain';
|
|
snackbarColor.value = 'success';
|
|
snackbar.value = true;
|
|
};
|
|
|
|
const toggleTwoFactor = () => {
|
|
const status = twoFactorEnabled.value ? 'diaktifkan' : 'dinonaktifkan';
|
|
snackbarMessage.value = `Autentikasi dua faktor ${status}`;
|
|
snackbarColor.value = 'info';
|
|
snackbar.value = true;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.hero-header {
|
|
background: linear-gradient(135deg, #1976d2 0%, #1565c0 50%, #f57c00 100%);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.content-container {
|
|
margin-top: 40px;
|
|
}
|
|
|
|
.sticky-card {
|
|
position: sticky;
|
|
top: 80px;
|
|
}
|
|
|
|
.profile-card {
|
|
overflow: visible;
|
|
}
|
|
|
|
.profile-content {
|
|
padding-top: 0;
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.profile-avatar-container {
|
|
position: relative;
|
|
display: inline-block;
|
|
margin-top: -70px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.profile-avatar {
|
|
border: 6px solid white;
|
|
background: white;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.avatar-edit-btn {
|
|
position: absolute;
|
|
bottom: 8px;
|
|
right: 8px;
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
label {
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.v-list-item {
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.v-list-item:hover {
|
|
background-color: rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
.snackbar-custom {
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (max-width: 1280px) {
|
|
.sticky-card {
|
|
position: relative;
|
|
top: 0;
|
|
}
|
|
}
|
|
</style> |