Refactor project structure and update dependencies
This commit is contained in:
109
nuxt/app/components/account/AccountDeviceTable.vue
Normal file
109
nuxt/app/components/account/AccountDeviceTable.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '#ui/components/Table.vue'
|
||||
import type { Device, IDevicesResponse } from '~/types/device'
|
||||
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
||||
|
||||
const dayjs = useDayjs()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const devices = ref<Device[]>([])
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
|
||||
const response = await $fetch<IDevicesResponse>('devices')
|
||||
|
||||
if (response.ok) {
|
||||
devices.value = response.devices
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const columns: TableColumn<Device>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Device',
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'font-semibold' }, [
|
||||
row.original.name,
|
||||
row.original.is_current && h(UBadge, {
|
||||
label: 'active',
|
||||
color: 'primary',
|
||||
variant: 'soft',
|
||||
size: 'sm',
|
||||
class: 'ms-1',
|
||||
}),
|
||||
h('div', { class: 'font-medium text-sm' }, `IP: ${row.original.ip}`),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_used_at',
|
||||
header: 'Last used at',
|
||||
cell: ({ row }) => {
|
||||
return dayjs(row.original.last_used_at).fromNow()
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'flex justify-end' },
|
||||
h(UDropdownMenu, {
|
||||
items: items(row.original),
|
||||
}, () => h(UButton, {
|
||||
icon: 'i-heroicons-ellipsis-vertical-20-solid',
|
||||
variant: 'ghost',
|
||||
color: 'neutral',
|
||||
})),
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const items = (row: Device) => [
|
||||
[
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid',
|
||||
onSelect: async () => {
|
||||
await $fetch<never>('devices/disconnect', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
hash: row.hash,
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
await fetchData()
|
||||
await auth.fetchUser()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
if (import.meta.client) {
|
||||
fetchData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<UTable
|
||||
:data="devices"
|
||||
:columns="columns"
|
||||
size="lg"
|
||||
:loading="loading"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
125
nuxt/app/components/account/AccountUpdatePassword.vue
Normal file
125
nuxt/app/components/account/AccountUpdatePassword.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IAccountChangePasswordResponse, IVerificationNotificationResponse } from '~/types/account'
|
||||
|
||||
const form = ref()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const state = reactive({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: accountPasswordStatus } = useFetch<IAccountChangePasswordResponse>('account/password', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'The password was successfully updated.',
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
state.current_password = ''
|
||||
state.password = ''
|
||||
state.password_confirmation = ''
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: sendResetPasswordEmail, status: resetPasswordEmailStatus } = useFetch<IVerificationNotificationResponse>('verification-notification', {
|
||||
method: 'POST',
|
||||
body: { email: auth.user.email },
|
||||
immediate: false,
|
||||
watch: false,
|
||||
onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'A link to reset your password has been sent to your email.',
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UForm
|
||||
v-if="auth.user.has_password"
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Current Password"
|
||||
name="current_password"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.current_password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="New Password"
|
||||
name="password"
|
||||
hint="min 8 characters"
|
||||
:ui="{ hint: 'text-xs text-gray-500 dark:text-gray-400' }"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Repeat Password"
|
||||
name="password_confirmation"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password_confirmation"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="pt-2">
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Save"
|
||||
:loading="accountPasswordStatus === 'pending'"
|
||||
/>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
icon="i-heroicons-information-circle-20-solid"
|
||||
title="Send a link to your email to reset your password."
|
||||
description="To create a password for your account, you must go through the password recovery process."
|
||||
:actions="[
|
||||
{
|
||||
label: 'Send link to Email',
|
||||
variant: 'solid',
|
||||
color: 'neutral',
|
||||
loading: resetPasswordEmailStatus === 'pending',
|
||||
onClick: () => sendResetPasswordEmail(),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
128
nuxt/app/components/account/AccountUpdateProfile.vue
Normal file
128
nuxt/app/components/account/AccountUpdateProfile.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IAccountUpdateResponse, IVerificationNotificationResponse } from '~/types/account'
|
||||
|
||||
const form = ref()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const state = reactive({
|
||||
...{
|
||||
email: auth.user.email,
|
||||
name: auth.user.name,
|
||||
avatar: auth.user.avatar,
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: sendEmailVerification, status: resendEmailStatus } = useFetch<IVerificationNotificationResponse>('verification-notification', {
|
||||
method: 'POST',
|
||||
body: { email: state.email },
|
||||
immediate: false,
|
||||
watch: false,
|
||||
onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: response._data.message,
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<IAccountUpdateResponse>('account/update', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Account details have been successfully updated.',
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
await auth.fetchUser()
|
||||
|
||||
state.name = auth.user.name
|
||||
state.email = auth.user.email
|
||||
state.avatar = auth.user.avatar
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label=""
|
||||
name="avatar"
|
||||
class="flex"
|
||||
>
|
||||
<InputUploadAvatar
|
||||
v-model="state.avatar"
|
||||
accept=".png, .jpg, .jpeg, .webp"
|
||||
entity="avatars"
|
||||
max-size="2"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Name"
|
||||
name="name"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.name"
|
||||
type="text"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
placeholder="you@example.com"
|
||||
icon="i-heroicons-envelope"
|
||||
trailing
|
||||
type="email"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UAlert
|
||||
v-if="auth.user.must_verify_email"
|
||||
variant="subtle"
|
||||
color="warning"
|
||||
icon="i-heroicons-information-circle-20-solid"
|
||||
title="Please confirm your email address."
|
||||
description="A confirmation email has been sent to your email address. Please click on the confirmation link in the email to verify your email address."
|
||||
:actions="[
|
||||
{
|
||||
label: 'Resend verification email',
|
||||
variant: 'solid',
|
||||
color: 'neutral',
|
||||
loading: resendEmailStatus === 'pending',
|
||||
onClick: () => sendEmailVerification(),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="pt-2">
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Save"
|
||||
:loading="accountUpdateStatus === 'pending'"
|
||||
/>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
Reference in New Issue
Block a user