first init
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user