first init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { profileLayoutRoutes } from './routes'
|
||||
export { profileMenu } from './menu'
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Menu } from '@/core/types/menu'
|
||||
|
||||
export const profileMenu: Menu[] = [
|
||||
{
|
||||
icon: 'User',
|
||||
route_name: 'profile-overview-2',
|
||||
title: 'Profil',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { PasswordInput } from '@/components/ui/password-input'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { updatePassword } from '@/modules/profile/services/profile.service'
|
||||
|
||||
defineProps<{
|
||||
embedded?: boolean
|
||||
}>()
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
form.current_password = ''
|
||||
form.password = ''
|
||||
form.password_confirmation = ''
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (form.password !== form.password_confirmation) {
|
||||
await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Kata laluan tidak sepadan',
|
||||
text: 'Kata laluan baharu dan pengesahan mesti sama.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const res = await updatePassword({
|
||||
current_password: form.current_password,
|
||||
password: form.password,
|
||||
password_confirmation: form.password_confirmation,
|
||||
})
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.message ?? 'Gagal menukar kata laluan.')
|
||||
}
|
||||
|
||||
resetForm()
|
||||
|
||||
await Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: res.message || 'Kata laluan berjaya dikemas kini.',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Ralat',
|
||||
text: getApiErrorMessage(error, 'Gagal menukar kata laluan.'),
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!embedded" class="mb-6 flex items-center">
|
||||
<h2 class="mr-auto text-lg font-medium">Tukar Kata Laluan</h2>
|
||||
</div>
|
||||
<Box raised="single" :class="embedded ? 'p-0' : 'p-0 lg:mt-5'">
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="flex items-center border-b border-foreground/15 p-5">
|
||||
<h3 class="mr-auto text-base font-medium">Tukar Kata Laluan</h3>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<FieldGroup>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel for="password-current">Kata Laluan Semasa</FieldLabel>
|
||||
<PasswordInput
|
||||
id="password-current"
|
||||
v-model="form.current_password"
|
||||
placeholder="Kata laluan semasa"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="saving"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="password-new">Kata Laluan Baharu</FieldLabel>
|
||||
<PasswordInput
|
||||
id="password-new"
|
||||
v-model="form.password"
|
||||
placeholder="Kata laluan baharu"
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="saving"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="password-confirm">Sahkan Kata Laluan Baharu</FieldLabel>
|
||||
<PasswordInput
|
||||
id="password-confirm"
|
||||
v-model="form.password_confirmation"
|
||||
placeholder="Sahkan kata laluan baharu"
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="saving"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit" variant="primary" :disabled="saving">
|
||||
{{ saving ? 'Menyimpan...' : 'Tukar Kata Laluan' }}
|
||||
</Button>
|
||||
<Button look="outline" type="button" :disabled="saving" @click="resetForm">
|
||||
Set Semula
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,650 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { SwitchRoot, SwitchControl } from '@/components/ui/switch'
|
||||
import { ProgressRoot, ProgressTrack, ProgressRange } from '@/components/ui/progress-linear'
|
||||
import {
|
||||
CarouselRoot,
|
||||
CarouselPrevTrigger,
|
||||
CarouselNextTrigger,
|
||||
CarouselItemGroup,
|
||||
CarouselItem,
|
||||
} from '@/components/ui/carousel'
|
||||
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { FileIcon } from '@/components/ui/file-icon'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { updateProfile, uploadProfileImage } from '@/modules/profile/services/profile.service'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ChangePassword from './ChangePassword.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const saving = ref(false)
|
||||
const uploadingImage = ref(false)
|
||||
const imageInputRef = ref<HTMLInputElement | null>(null)
|
||||
const imagePreviewUrl = ref<string | null>(null)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
ic_number: '',
|
||||
position: '',
|
||||
phone_number: '',
|
||||
})
|
||||
|
||||
const displayValue = (value: string | null | undefined) => value?.trim() || '-'
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const status = authStore.user?.status
|
||||
if (!status) return '-'
|
||||
return status.charAt(0).toUpperCase() + status.slice(1)
|
||||
})
|
||||
|
||||
const avatarSrc = computed(
|
||||
() => imagePreviewUrl.value ?? authStore.userImageUrl ?? undefined,
|
||||
)
|
||||
|
||||
function syncFormFromUser() {
|
||||
const user = authStore.user
|
||||
if (!user) return
|
||||
|
||||
form.name = user.name ?? ''
|
||||
form.ic_number = user.ic_number ?? ''
|
||||
form.position = user.position ?? ''
|
||||
form.phone_number = user.phone_number ?? ''
|
||||
}
|
||||
|
||||
function clearImagePreview() {
|
||||
if (imagePreviewUrl.value) {
|
||||
URL.revokeObjectURL(imagePreviewUrl.value)
|
||||
imagePreviewUrl.value = null
|
||||
}
|
||||
if (imageInputRef.value) {
|
||||
imageInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openImagePicker() {
|
||||
if (uploadingImage.value) return
|
||||
imageInputRef.value?.click()
|
||||
}
|
||||
|
||||
async function onImageSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
clearImagePreview()
|
||||
imagePreviewUrl.value = URL.createObjectURL(file)
|
||||
uploadingImage.value = true
|
||||
|
||||
try {
|
||||
const res = await uploadProfileImage(file)
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.message ?? 'Gagal mengemas kini gambar profil.')
|
||||
}
|
||||
|
||||
authStore.setUserProfile(res.data)
|
||||
clearImagePreview()
|
||||
|
||||
await Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: res.message || 'Gambar profil berjaya dikemas kini.',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
clearImagePreview()
|
||||
await Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Ralat',
|
||||
text: getApiErrorMessage(error, 'Gagal mengemas kini gambar profil.'),
|
||||
})
|
||||
} finally {
|
||||
uploadingImage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveProfile() {
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const res = await updateProfile({
|
||||
name: form.name.trim(),
|
||||
ic_number: form.ic_number.trim(),
|
||||
position: form.position.trim(),
|
||||
phone_number: form.phone_number.trim(),
|
||||
})
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.message ?? 'Gagal mengemas kini profil.')
|
||||
}
|
||||
|
||||
authStore.setUserProfile(res.data)
|
||||
syncFormFromUser()
|
||||
|
||||
await Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: res.message || 'Profil berjaya dikemas kini.',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Ralat',
|
||||
text: getApiErrorMessage(error, 'Gagal mengemas kini profil.'),
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => authStore.user, syncFormFromUser, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
if (!authStore.user) {
|
||||
await authStore.fetchSession()
|
||||
}
|
||||
syncFormFromUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="mr-auto text-lg font-medium">Profil</h2>
|
||||
</div>
|
||||
<TabsRoot defaultValue="1">
|
||||
<!-- BEGIN: Profile Info -->
|
||||
<Box raised="single" class="mt-5 p-0">
|
||||
<div class="flex flex-col border-b border-foreground/15 p-5 lg:flex-row">
|
||||
<div class="flex flex-1 items-center justify-center px-5 lg:justify-start">
|
||||
<div class="relative" :class="{ 'opacity-60': uploadingImage }">
|
||||
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
|
||||
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
|
||||
<AvatarImage v-if="avatarSrc" :src="avatarSrc" :alt="authStore.userName" />
|
||||
</AvatarRoot>
|
||||
<button type="button"
|
||||
class="bg-(--color)/70 border-3 border-background absolute bottom-0 right-0 mb-1 mr-1 flex cursor-pointer items-center justify-center rounded-full p-2 text-white [--color:var(--color-primary)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
:disabled="uploadingImage" aria-label="Tukar gambar profil" @click="openImagePicker">
|
||||
<Lucide class="size-4" :icon="uploadingImage ? 'LoaderCircle' : 'Camera'"
|
||||
:class="{ 'animate-spin': uploadingImage }" />
|
||||
</button>
|
||||
<input ref="imageInputRef" type="file" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden"
|
||||
@change="onImageSelected" />
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
|
||||
{{ authStore.userName || '-' }}
|
||||
</div>
|
||||
<div class="opacity-70">{{ authStore.userActiveRole }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 flex-1 border-t border-l border-r border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-t-0 lg:pt-0">
|
||||
<div class="text-center font-medium lg:mt-3 lg:text-left">Maklumat Hubungan</div>
|
||||
<div class="mt-4 flex flex-col items-center justify-center lg:items-start">
|
||||
<div class="flex items-center truncate sm:whitespace-normal">
|
||||
<Lucide class="mr-2 size-4" icon="Mail" />
|
||||
{{ displayValue(authStore.user?.email) }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
||||
<Lucide class="mr-2 size-4" icon="Phone" />
|
||||
{{ displayValue(authStore.user?.phone_number) }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
|
||||
<Lucide class="mr-2 size-4" icon="IdCard" />
|
||||
{{ displayValue(authStore.user?.ic_number) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 flex flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-0 lg:pt-0">
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium">{{ displayValue(authStore.activeRole?.name) }}</div>
|
||||
<div class="opacity-70">Peranan</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium">{{ statusLabel }}</div>
|
||||
<div class="opacity-70">Status</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-medium capitalize">{{ displayValue(authStore.activeRole?.context) }}</div>
|
||||
<div class="opacity-70">Konteks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabs title -->
|
||||
<div class="px-5 py-4">
|
||||
<TabsList class="w-full mb-0">
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="1">
|
||||
<Lucide class="mr-2 size-4" icon="User" /> Profil
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="3">
|
||||
<Lucide class="mr-2 size-4" icon="Banknote" /> Maklumat Bank
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="2">
|
||||
<Lucide class="mr-2 size-4" icon="Lock" /> Kata Laluan
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="4">
|
||||
<Lucide class="mr-2 size-4" icon="Server" /> Perkhidmatan
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- Profile Info -->
|
||||
<TabsContent value="1" class="mt-8">
|
||||
<Box raised="single" class="p-6">
|
||||
<form class="space-y-6" @submit.prevent="onSaveProfile">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Maklumat Peribadi</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
Kemas kini maklumat peribadi anda.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" :disabled="saving">
|
||||
{{ saving ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="profile-name">Nama</FieldLabel>
|
||||
<Input id="profile-name" v-model="form.name" type="text" placeholder="Nama penuh" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-email">E-mel</FieldLabel>
|
||||
<Input id="profile-email" :model-value="authStore.user?.email ?? ''" type="email" disabled />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-ic">No. Kad Pengenalan</FieldLabel>
|
||||
<Input id="profile-ic" v-model="form.ic_number" type="text" placeholder="No. kad pengenalan" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="profile-phone">No. Telefon</FieldLabel>
|
||||
<Input id="profile-phone" v-model="form.phone_number" type="text" placeholder="No. telefon" />
|
||||
</Field>
|
||||
<Field class="md:col-span-2">
|
||||
<FieldLabel for="profile-position">Jawatan</FieldLabel>
|
||||
<Input id="profile-position" v-model="form.position" type="text" placeholder="Jawatan" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</Box>
|
||||
</TabsContent>
|
||||
<TabsContent value="2" class="mt-8">
|
||||
<ChangePassword embedded />
|
||||
</TabsContent>
|
||||
<!-- Maklumat Bank -->
|
||||
<TabsContent value="4" class="mt-8">
|
||||
<div class="grid grid-cols-12 gap-x-6 gap-y-8">
|
||||
<!-- BEGIN: Latest Uploads -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-5 sm:py-3">
|
||||
<h2 class="mr-auto text-base font-medium">Latest Uploads</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="all">All Files</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 hidden sm:flex">All Files</Button>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<FileIcon class="w-12" variant="directory" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Documentation</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">40 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<FileIcon class="w-12 text-xs" variant="file" type="MP3" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Celine Dion - Ashes</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">40 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<FileIcon class="w-12" variant="empty-directory" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Resources</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">0 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Latest Uploads -->
|
||||
<!-- BEGIN: Work In Progress -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<TabsRoot defaultValue="wip-0">
|
||||
<div class="relative flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Work In Progress</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="new">New</MenuItem>
|
||||
<MenuItem value="last-week">Last Week</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<TabsList class="absolute inset-y-0 h-[2.8rem] right-5 my-auto hidden w-auto sm:flex">
|
||||
<TabsTrigger value="wip-0">New</TabsTrigger>
|
||||
<TabsTrigger value="wip-1">Last Week</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="wip-0" class="p-5">
|
||||
<div>
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Pending Tasks</div>
|
||||
<div>20%</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="50" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Completed Tasks</div>
|
||||
<div>2 / 20</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="25" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Tasks In Progress</div>
|
||||
<div>42</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="75" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<Button variant="ghost" class="border border-foreground/15 shadow-none mx-auto mt-5 inline-block"
|
||||
as="a" href="">
|
||||
View More Details
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</Box>
|
||||
<!-- END: Work In Progress -->
|
||||
<!-- BEGIN: Daily Sales -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-5 sm:py-3">
|
||||
<h2 class="mr-auto text-base font-medium">Daily Sales</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="download">
|
||||
<Lucide class="mr-2 size-4" icon="File" /> Download Excel
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 hidden sm:flex">
|
||||
<Lucide class="mr-2 size-4" icon="File" /> Download Excel
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div v-for="(faker, index) in fakers.slice(0, 3)" :key="index" :class="{ 'mt-5': index > 0 }"
|
||||
class="relative flex items-center">
|
||||
<AvatarRoot class="size-12 bg-background rounded-full">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][0]" />
|
||||
</AvatarRoot>
|
||||
<div class="ml-4 mr-auto">
|
||||
<a class="font-medium" href="">{{ faker['users'][0]!['name'] }}</a>
|
||||
<div class="mr-5 opacity-70 sm:mr-5">
|
||||
{{
|
||||
index === 0
|
||||
? 'Bootstrap 4 HTML Admin Template'
|
||||
: index === 1
|
||||
? 'Tailwind Admin Dashboard Template'
|
||||
: 'Vuejs HTML Admin Template'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-medium">
|
||||
{{ index === 0 ? '+$19' : index === 1 ? '+$25' : '+$21' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Daily Sales -->
|
||||
<!-- BEGIN: Latest Tasks -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<TabsRoot defaultValue="lt-0">
|
||||
<div class="relative flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Latest Tasks</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="new">New</MenuItem>
|
||||
<MenuItem value="last-week">Last Week</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<TabsList class="absolute inset-y-0 h-[2.8rem] right-5 my-auto hidden w-auto sm:flex">
|
||||
<TabsTrigger value="lt-0">New</TabsTrigger>
|
||||
<TabsTrigger value="lt-1">Last Week</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<TabsContent value="lt-0">
|
||||
<div class="flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Create New Campaign</a>
|
||||
<div class="opacity-70">10:00 AM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Meeting With Client</a>
|
||||
<div class="opacity-70">02:00 PM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Create New Repository</a>
|
||||
<div class="opacity-70">04:00 PM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</Box>
|
||||
<!-- END: Latest Tasks -->
|
||||
<Box raised="single" class="col-span-12 p-0">
|
||||
<CarouselRoot :default-page="0" :slide-count="fakers.slice(0, 5).length">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-3">
|
||||
<h2 class="mr-auto text-base font-medium">New Products</h2>
|
||||
<CarouselPrevTrigger as-child>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 mr-2">
|
||||
<Lucide class="size-4" icon="ChevronLeft" />
|
||||
</Button>
|
||||
</CarouselPrevTrigger>
|
||||
<CarouselNextTrigger as-child>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15">
|
||||
<Lucide class="size-4" icon="ChevronRight" />
|
||||
</Button>
|
||||
</CarouselNextTrigger>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<CarouselItemGroup class="py-5">
|
||||
<CarouselItem v-for="(faker, index) in fakers.slice(0, 5)" :key="index" :index="index" class="px-5"
|
||||
as-child>
|
||||
<div>
|
||||
<div class="flex flex-col items-center pb-5 lg:flex-row">
|
||||
<div class="flex flex-col items-center pr-5 sm:flex-row lg:border-r border-foreground/15">
|
||||
<div class="sm:mr-5">
|
||||
<AvatarRoot class="size-20 bg-background rounded-full">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['images'][0]" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<div class="mr-auto mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<a class="text-lg font-medium" href="">
|
||||
{{ faker['products'][0]!['name'] }}
|
||||
</a>
|
||||
<div class="mt-1 opacity-70 sm:mt-0">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 flex w-full flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-4 lg:mt-0 lg:w-auto lg:border-t-0 lg:pt-0">
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][0] }}</div>
|
||||
<div class="opacity-70">Orders</div>
|
||||
</div>
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][1] }}k</div>
|
||||
<div class="opacity-70">Purchases</div>
|
||||
</div>
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][0] }}</div>
|
||||
<div class="opacity-70">Reviews</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center border-t border-foreground/15 pt-5 sm:flex-row">
|
||||
<div
|
||||
class="flex w-full items-center justify-center border-b border-foreground/15 pb-5 sm:w-auto sm:justify-start sm:border-b-0 sm:pb-0">
|
||||
<Badge look="outline" class="mr-3 px-3 py-2">{{
|
||||
faker['dates'][0]
|
||||
}}</Badge>
|
||||
<div class="opacity-70">Date of Release</div>
|
||||
</div>
|
||||
<div class="mt-5 flex sm:ml-auto sm:mt-0">
|
||||
<Button variant="ghost"
|
||||
class="border border-foreground/15 shadow-none ml-auto">Preview</Button>
|
||||
<Button variant="ghost" class="border border-foreground/15 shadow-none ml-2">Details</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselItemGroup>
|
||||
</div>
|
||||
</CarouselRoot>
|
||||
</Box>
|
||||
<!-- END: New Products -->
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,353 @@
|
||||
<script lang="ts" setup>
|
||||
import fakers from '@/utils/faker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import {
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
MenuPositioner,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
} from '@/components/ui/menu'
|
||||
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
TooltipRoot,
|
||||
TooltipTrigger,
|
||||
TooltipPositioner,
|
||||
TooltipContent,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<h2 class="mr-auto text-lg font-medium">Update Profile</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- BEGIN: Profile Menu -->
|
||||
<div class="col-span-12 flex flex-col-reverse lg:col-span-4 lg:block 2xl:col-span-3">
|
||||
<Box class="mt-5 p-0">
|
||||
<div class="relative flex items-center p-5">
|
||||
<AvatarRoot class="size-12 rounded-full">
|
||||
<AvatarFallback>PA</AvatarFallback>
|
||||
<AvatarImage :src="fakers[0]!['photos'][0]" alt="avatar" />
|
||||
</AvatarRoot>
|
||||
<div class="ml-4 mr-auto">
|
||||
<div class="text-base font-medium">
|
||||
{{ fakers[0]!['users'][0]!['name'] }}
|
||||
</div>
|
||||
<div class="opacity-70">{{ fakers[0]!['jobs'][0] }}</div>
|
||||
</div>
|
||||
<MenuRoot
|
||||
class="w-auto"
|
||||
:positioning="{
|
||||
placement: 'bottom',
|
||||
}"
|
||||
>
|
||||
<MenuTrigger as-child>
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-64">
|
||||
<div class="font-medium">Export Options</div>
|
||||
<MenuSeparator />
|
||||
<MenuItem value="0">
|
||||
<Lucide icon="Activity" />
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem value="1">
|
||||
<Lucide icon="Box" />
|
||||
Indonesia
|
||||
<Badge variant="danger" look="outline" class="ml-auto">10</Badge>
|
||||
</MenuItem>
|
||||
<MenuItem value="2">
|
||||
<Lucide icon="Layout" />
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem value="3">
|
||||
<Lucide icon="Sidebar" />
|
||||
Indonesia
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
<div class="flex">
|
||||
<Button class="text-xs" type="button" variant="primary" look="outline" size="sm">
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
class="ml-auto text-xs"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
look="outline"
|
||||
size="sm"
|
||||
>
|
||||
View Profile
|
||||
</Button>
|
||||
</div>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 border-t border-foreground/10 p-5">
|
||||
<a
|
||||
class="[&.active]:text-primary active flex items-center [&.active]:font-medium"
|
||||
href=""
|
||||
>
|
||||
<Lucide class="mr-2 size-4" icon="Activity" /> Personal Information
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Box" /> Account Settings
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Lock" /> Change Password
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Settings" /> User Settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 border-t border-foreground/10 p-5">
|
||||
<a class="flex items-center" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Activity" /> Email Settings
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Box" /> Saved Credit Cards
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Lock" /> Social Networks
|
||||
</a>
|
||||
<a class="[&.active]:text-primary flex items-center [&.active]:font-medium" href="">
|
||||
<Lucide class="mr-2 size-4" icon="Settings" /> Tax Information
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex border-t border-foreground/10 p-5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="shadow-none border border-foreground/15"
|
||||
type="button"
|
||||
>
|
||||
New Group
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="shadow-none border border-foreground/15 ml-auto"
|
||||
type="button"
|
||||
>
|
||||
New Quick Link
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<!-- END: Profile Menu -->
|
||||
<div class="col-span-12 lg:col-span-8 2xl:col-span-9">
|
||||
<!-- BEGIN: Display Information -->
|
||||
<Box class="p-0 lg:mt-5">
|
||||
<div class="flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Display Information</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="flex flex-col xl:flex-row">
|
||||
<div class="mt-6 flex-1 xl:mt-0">
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-12 gap-x-5">
|
||||
<div class="col-span-12 2xl:col-span-6">
|
||||
<Field>
|
||||
<FieldLabel for="update-profile-form-1">Display Name</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-1"
|
||||
type="text"
|
||||
:value="fakers[0]!['users'][0]!['name']"
|
||||
placeholder="Input text"
|
||||
disabled
|
||||
/>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-2">Nearest MRT Station</FieldLabel>
|
||||
<NativeSelect class="w-full" id="update-profile-form-2">
|
||||
<NativeSelectOption value="1">Admiralty</NativeSelectOption>
|
||||
<NativeSelectOption value="2">Aljunied</NativeSelectOption>
|
||||
<NativeSelectOption value="3">Ang Mo Kio</NativeSelectOption>
|
||||
<NativeSelectOption value="4">Bartley</NativeSelectOption>
|
||||
<NativeSelectOption value="5">Beauty World</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-span-12 2xl:col-span-6">
|
||||
<Field class="mt-3 2xl:mt-0">
|
||||
<FieldLabel for="update-profile-form-3">Postal Code</FieldLabel>
|
||||
<NativeSelect class="w-full" id="update-profile-form-3">
|
||||
<NativeSelectOption value="1"
|
||||
>018906 - 1 STRAITS BOULEVARD SINGA...</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="2"
|
||||
>018910 - 5A MARINA GARDENS DRIVE...</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="3"
|
||||
>018915 - 100A CENTRAL BOULEVARD...</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="4"
|
||||
>018925 - 21 PARK STREET MARINA...</NativeSelectOption
|
||||
>
|
||||
<NativeSelectOption value="5"
|
||||
>018926 - 23 PARK STREET MARINA...</NativeSelectOption
|
||||
>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-4">Phone Number</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-4"
|
||||
type="text"
|
||||
value="65570828"
|
||||
placeholder="Input text"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-span-12">
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-5">Address</FieldLabel>
|
||||
<Textarea
|
||||
id="update-profile-form-5"
|
||||
value="10 Anson Road, International Plaza, #10-11, 079903 Singapore, Singapore"
|
||||
placeholder="Adress"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="w-28" type="button" variant="primary"> Save </Button>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<div class="mx-auto w-52 xl:ml-6 xl:mr-0">
|
||||
<div class="rounded-xl border-2 border-dashed border-foreground/15 p-5">
|
||||
<div class="image-fit relative mx-auto h-40 cursor-pointer">
|
||||
<img
|
||||
class="rounded-xl"
|
||||
:src="fakers[0]!['photos'][0]"
|
||||
alt="Midone - Tailwind Admin Dashboard Template"
|
||||
/>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="bg-(--color)/80 border-(--color) text-medium absolute right-0 top-0 -mr-2 -mt-2 flex size-5 items-center justify-center rounded-full text-white [--color:var(--color-danger)]"
|
||||
>
|
||||
<Lucide class="h-4 w-4" icon="X" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPositioner>
|
||||
<TooltipContent>Remove this profile photo?</TooltipContent>
|
||||
</TooltipPositioner>
|
||||
</TooltipRoot>
|
||||
</div>
|
||||
<div class="relative mx-auto mt-3 cursor-pointer">
|
||||
<Button class="w-full" type="button" variant="primary"> Change Photo </Button>
|
||||
<Input class="absolute left-0 top-0 h-full w-full opacity-0" type="file" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Display Information -->
|
||||
<!-- BEGIN: Personal Information -->
|
||||
<Box class="mt-8 p-0">
|
||||
<div class="flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Personal Information</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-12 gap-x-5">
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<Field>
|
||||
<FieldLabel for="update-profile-form-6">Email</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-6"
|
||||
type="text"
|
||||
:value="fakers[0]!['users'][0]!['email']"
|
||||
placeholder="Input text"
|
||||
disabled
|
||||
/>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-7">Name</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-7"
|
||||
type="text"
|
||||
:value="fakers[0]!['users'][0]!['name']"
|
||||
placeholder="Input text"
|
||||
disabled
|
||||
/>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-8">ID Type</FieldLabel>
|
||||
<NativeSelect id="update-profile-form-8">
|
||||
<NativeSelectOption>IC</NativeSelectOption>
|
||||
<NativeSelectOption>FIN</NativeSelectOption>
|
||||
<NativeSelectOption>Passport</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-9">ID Number</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-9"
|
||||
type="text"
|
||||
value="357821204950001"
|
||||
placeholder="Input text"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<Field class="mt-3 xl:mt-0">
|
||||
<FieldLabel for="update-profile-form-10">Phone Number</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-10"
|
||||
type="text"
|
||||
value="65570828"
|
||||
placeholder="Input text"
|
||||
/>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-11">Address</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-11"
|
||||
type="text"
|
||||
value="10 Anson Road, International Plaza, #10-11, 079903 Singapore, Singapore"
|
||||
placeholder="Input text"
|
||||
/>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-12">Bank Name</FieldLabel>
|
||||
<NativeSelect class="w-full" id="update-profile-form-12">
|
||||
<NativeSelectOption value="1">SBI - STATE BANK OF INDIA</NativeSelectOption>
|
||||
<NativeSelectOption value="2">CITI BANK - CITI BANK</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field class="mt-3">
|
||||
<FieldLabel for="update-profile-form-13">Bank Account</FieldLabel>
|
||||
<Input
|
||||
id="update-profile-form-13"
|
||||
type="text"
|
||||
value="DBS Current 011-903573-0"
|
||||
placeholder="Input text"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button class="mr-auto w-28" type="button" variant="primary"> Save </Button>
|
||||
<Button type="button" variant="danger" look="outline">
|
||||
<Lucide class="mr-1 size-4" icon="Trash" /> Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Personal Information -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const profileLayoutRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: 'profile-overview-2',
|
||||
name: 'profile-overview-2',
|
||||
component: () => import('./pages/ProfileOverview2.vue'),
|
||||
meta: { title: 'Profil', module: 'profile' },
|
||||
},
|
||||
{
|
||||
path: 'update-profile',
|
||||
name: 'update-profile',
|
||||
component: () => import('./pages/UpdateProfile.vue'),
|
||||
meta: { title: 'Update Profile', module: 'profile' },
|
||||
},
|
||||
{
|
||||
path: 'change-password',
|
||||
name: 'change-password',
|
||||
component: () => import('./pages/ChangePassword.vue'),
|
||||
meta: { title: 'Change Password', module: 'profile' },
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
import { api } from '@/core/services/api'
|
||||
import type {
|
||||
UpdatePasswordPayload,
|
||||
UpdatePasswordResponse,
|
||||
UpdateProfilePayload,
|
||||
UpdateProfileResponse,
|
||||
} from '../types/profile.types'
|
||||
|
||||
function appendIfDefined(formData: FormData, key: string, value: string | undefined) {
|
||||
if (value !== undefined && value !== '') {
|
||||
formData.append(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadProfileImage(image: File): Promise<UpdateProfileResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('image', image)
|
||||
|
||||
const { data } = await api.post<UpdateProfileResponse>('/v1/profile', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updateProfile(payload: UpdateProfilePayload): Promise<UpdateProfileResponse> {
|
||||
const hasImage = payload.image instanceof File
|
||||
|
||||
if (hasImage) {
|
||||
const formData = new FormData()
|
||||
formData.append('name', payload.name!.trim())
|
||||
appendIfDefined(formData, 'ic_number', payload.ic_number)
|
||||
appendIfDefined(formData, 'position', payload.position)
|
||||
appendIfDefined(formData, 'phone_number', payload.phone_number)
|
||||
formData.append('image', payload.image!)
|
||||
|
||||
const { data } = await api.post<UpdateProfileResponse>('/v1/profile', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
const body: Record<string, string> = {
|
||||
name: payload.name!.trim(),
|
||||
}
|
||||
|
||||
if (payload.ic_number?.trim()) body.ic_number = payload.ic_number.trim()
|
||||
if (payload.position?.trim()) body.position = payload.position.trim()
|
||||
if (payload.phone_number?.trim()) body.phone_number = payload.phone_number.trim()
|
||||
|
||||
const { data } = await api.post<UpdateProfileResponse>('/v1/profile', body)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updatePassword(payload: UpdatePasswordPayload): Promise<UpdatePasswordResponse> {
|
||||
const { data } = await api.put<UpdatePasswordResponse>('/v1/profile/password', payload)
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { AuthUser } from '@/modules/auth/types/auth.types'
|
||||
|
||||
export interface UpdateProfilePayload {
|
||||
name?: string
|
||||
ic_number?: string
|
||||
position?: string
|
||||
phone_number?: string
|
||||
image?: File
|
||||
}
|
||||
|
||||
export interface UpdateProfileResponse {
|
||||
success: boolean
|
||||
data: AuthUser
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface UpdatePasswordPayload {
|
||||
current_password: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
}
|
||||
|
||||
export interface UpdatePasswordResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
Reference in New Issue
Block a user