This commit is contained in:
2024-09-02 16:58:23 +02:00
parent c77503ed45
commit 1b0dc0f27d
52 changed files with 817 additions and 1379 deletions

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
const props = defineProps<{
src: string | null
}>()
const src = computed(() => {
if (!props.src) {
return
}
if (props.src.startsWith('http') || props.src.startsWith('data')) {
return props.src
}
// Prefix the image path with the images folder
return `images/${props.src}`
})
</script>
<template>
<UAvatar :src />
</template>

View File

@@ -7,7 +7,7 @@ defineProps<{
}>()
defineEmits(['createTab'])
const { canCreateTabInCategory } = await useUserLimit()
const { canCreateTabInCategory } = await useUserLimits()
</script>
<template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// todo: implement the footer
</script>
<template>
</template>
<style scoped>
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
const colorMode = useColorMode()
const { user, loggedIn, clear } = useUserSession()
const { user, loggedIn, clear } = await useUserSession()
const isSettingsOpen = ref(false)
const isDark = computed(() => colorMode.preference === 'dark')
@@ -33,20 +33,21 @@ const items = [
}],
]
function toggleColorMode() {
colorMode.preference = isDark.value ? 'light' : 'dark'
}
async function logout() {
await clear()
navigateTo('/login')
await navigateTo('/')
window.location.reload()
}
function toggleColorMode() {
colorMode.preference = isDark.value ? 'light' : 'dark'
}
defineShortcuts({
t: () => toggleColorMode(),
s: () => isSettingsOpen.value = !isSettingsOpen.value,
l: async () => await logout(),
h: () => navigateTo('/'),
})
</script>
@@ -69,7 +70,7 @@ defineShortcuts({
:ui="{ item: { disabled: 'cursor-text select-text' } }"
:popper="{ placement: 'bottom-end' }"
>
<UAvatar :src="user.avatar" />
<AppAvatar :src="user.avatar" />
<template #account>
<div class="text-left">
@@ -95,7 +96,7 @@ defineShortcuts({
</template>
<template #logout="{ item }">
<div class="w-full flex justify-between items-center" @click="logout()">
<div class="w-full flex justify-between items-center" @click="logout">
<div class="flex gap-2 items-center">
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500" />
<span class="truncate">{{ item.label }}</span>
@@ -123,13 +124,23 @@ defineShortcuts({
</template>
<template #default>
<div class="space-y-12">
<div class="space-y-12 overflow-auto">
<div>
Delete account
Change user details
<AppUserSettingsForm :user="user" />
</div>
</div>
</template>
<template #footer>
<UButton
color="red"
variant="solid"
icon="i-ph:trash-duotone"
block
>
Delete account
</UButton>
</template>
</UCard>
</USlideover>
</div>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { TabType } from '~~/types/types'
defineProps<{
const props = defineProps<{
tab: TabType
editMode: boolean
}>()
const { setTabPrimary } = await useTabs()
@@ -46,74 +47,106 @@ function openDeleteTabModal(tab: TabType) {
currentDeleteTab.value = tab
deleteTabModal.value = true
}
function visitLink() {
if (!props.editMode) {
window.open(props.tab.link, '_blank')
// add view count
}
}
</script>
<template>
<ULink
:to="tab.link"
class="relative"
target="_blank"
<UCard
:ui="{
body: { base: 'h-full relative z-20' },
background: `h-full duration-300 bg-white dark:bg-gray-900 ${editMode ? '' : 'hover:bg-gray-100 dark:hover:bg-gray-800'}`,
}"
:class="editMode ? 'animate-wiggle' : 'cursor-pointer'"
@click.prevent="visitLink"
>
<div v-show="tab.primary" class="absolute flex h-3 w-3 -left-1 -top-1">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="`bg-${tab.color}-400`" />
<span class="relative inline-flex rounded-full h-3 w-3" :class="`bg-${tab.color}-400`" />
</div>
<UCard
:ui="{ body: { base: 'h-full' }, background: 'h-full duration-300 bg-white hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800' }"
>
<div class="flex items-center justify-between h-full">
<div class="flex gap-4 items-center h-full">
<UBadge :color="tab.color" class="p-2" variant="soft">
<UIcon :name="tab.icon" size="32" />
</UBadge>
<div class="flex flex-col gap-1">
<p :class="`text-${tab.color}-400`" class="text-xl font-medium truncate">
{{ tab.name }}
</p>
</div>
<div class="flex items-center justify-between h-full">
<div class="flex gap-4 items-center h-full">
<UBadge :color="tab.color" class="p-2" variant="soft">
<UIcon :name="tab.icon" size="32" />
</UBadge>
<div class="flex flex-col gap-1">
<p :class="`text-${tab.color}-400`" class="text-xl font-medium truncate">
{{ tab.name }}
</p>
</div>
<UDropdown
:items="items"
:popper="{ placement: 'bottom-end', arrow: true }"
:ui="{ container: 'z-50 group', width: 'w-40', shadow: 'shadow-2xl' }"
>
<UButton
color="gray"
variant="ghost"
:padded="false"
size="sm"
icon="i-ph:dots-three-outline-duotone"
/>
<template #item="{ item }">
<div class="w-full flex items-center justify-between" @click.prevent="item.click(tab)">
<span
class="truncate"
:class="`text-${item.color}-500`"
>
{{ item.label }}
</span>
<UIcon
:name="item.icon"
class="flex-shrink-0 h-4 w-4 ms-auto"
:class="`text-${item.color}-500`"
/>
</div>
</template>
</UDropdown>
</div>
</UCard>
<ModalUpdateTab
v-if="currentUpdateTab"
v-model="updateTabModal"
:tab="currentUpdateTab"
@close-modal="updateTabModal = false"
/>
<ModalDeleteTab
v-if="currentDeleteTab"
v-model="deleteTabModal"
:tab="currentDeleteTab"
@close-modal="deleteTabModal = false"
/>
</ULink>
<UDropdown
:items="items"
:popper="{ placement: 'bottom-end', arrow: true }"
:ui="{ container: 'z-50 group', width: 'w-40', shadow: 'shadow-2xl', wrapper: 'absolute inline-flex -top-3 -right-3' }"
>
<UButton
v-show="editMode"
color="gray"
variant="solid"
size="md"
:padded="false"
:ui="{ rounded: 'rounded-full p-1' }"
icon="i-ph:dots-three-outline-duotone"
/>
<template #item="{ item }">
<div class="w-full flex items-center justify-between" @click.prevent="item.click(tab)">
<span
class="truncate"
:class="`text-${item.color}-500`"
>
{{ item.label }}
</span>
<UIcon
:name="item.icon"
class="flex-shrink-0 h-4 w-4 ms-auto"
:class="`text-${item.color}-500`"
/>
</div>
</template>
</UDropdown>
</div>
</UCard>
<ModalUpdateTab
v-if="currentUpdateTab"
v-model="updateTabModal"
:tab="currentUpdateTab"
@close-modal="updateTabModal = false"
/>
<ModalDeleteTab
v-if="currentDeleteTab"
v-model="deleteTabModal"
:tab="currentDeleteTab"
@close-modal="deleteTabModal = false"
/>
</template>
<style scoped>
@keyframes wiggle {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(1deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-1deg);
}
100% {
transform: rotate(0deg);
}
}
.animate-wiggle {
animation: wiggle .4s ease-in-out infinite;
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import type { UpdateUserSchemaType } from '~~/types/types'
import { UpdateUserSchema, locales } from '~~/types/types'
import type { FormSubmitEvent } from '#ui/types'
import type { UserSession } from '#auth-utils'
const props = defineProps<{
user: UserSession
}>()
const state = reactive({
name: undefined,
username: undefined,
email: undefined,
private: undefined,
description: undefined,
language: locales[0],
location: undefined,
})
watchEffect(() => {
state.name = props.user.name
state.username = props.user.username
state.private = props.user.private
state.description = props.user.description
state.language = locales.find(locale => locale.locale === props.user.language).locale
state.location = props.user.location
state.email = props.user.email
})
async function handleUpdate(event: FormSubmitEvent<UpdateUserSchemaType>) {
try {
await useRequestFetch()(`/api/users/me`, {
method: 'PUT',
body: JSON.stringify({
username: event.data.username,
name: event.data.name,
description: event.data.description,
location: event.data.location,
language: event.data.language,
private: event.data.private,
}),
})
useSuccessToast('Profile successfully updated!')
}
catch (error) {
useErrorToast('Profile update failed!', error as string)
}
}
const { deleteAvatar, uploadAvatar } = await useUser()
</script>
<template>
<UForm :schema="UpdateUserSchema" :state="state" class="space-y-4 p-1" @submit="handleUpdate">
<UFormGroup label="Username" name="username">
<UInput v-model="state.username" type="text" />
</UFormGroup>
<UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" />
</UFormGroup>
<UFormGroup label="Avatar" name="avatar">
<UInput type="file" size="sm" accept="image/*" hidden @change="uploadAvatar" />
</UFormGroup>
<UButton
v-if="user?.avatar"
variant="outline"
color="red"
label="Delete avatar"
size="xs"
@click.prevent="deleteAvatar"
/>
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" type="text" disabled />
</UFormGroup>
<UFormGroup label="Description" name="description">
<UTextarea v-model="state.description" autoresize :rows="2" />
</UFormGroup>
<UFormGroup label="Language" name="language">
<USelect v-model="state.language" :options="locales" option-attribute="label" value-attribute="locale" />
</UFormGroup>
<UFormGroup label="Location" name="location">
<UInput v-model="state.location" type="text" />
</UFormGroup>
<UFormGroup label="Page private" name="private" :description="state.private ? 'Your page is private' : 'Your page is public'">
<UToggle
v-model="state.private"
on-icon="i-ph:lock-key-duotone"
off-icon="i-ph:users-four-duotone"
:model-value="state.private"
size="lg"
color="red"
/>
</UFormGroup>
<UButton
type="submit"
block
color="gray"
label="Update Profile"
/>
</UForm>
</template>
<style scoped>
</style>