mirror of
https://github.com/ArthurDanjou/arthome.git
synced 2026-01-14 12:14:33 +01:00
Working
This commit is contained in:
20
.env.example
20
.env.example
@@ -1,20 +0,0 @@
|
|||||||
NUXT_SESSION_PASSWORD=
|
|
||||||
|
|
||||||
NUXT_OAUTH_GITHUB_CLIENT_ID=
|
|
||||||
NUXT_OAUTH_GITHUB_CLIENT_SECRET=
|
|
||||||
NUXT_OAUTH_GITHUB_REDIRECT_URL=
|
|
||||||
|
|
||||||
NUXT_OAUTH_GOOGLE_CLIENT_ID=
|
|
||||||
NUXT_OAUTH_GOOGLE_CLIENT_SECRET=
|
|
||||||
NUXT_OAUTH_GOOGLE_REDIRECT_URL=
|
|
||||||
|
|
||||||
NUXT_OPEN_WEATHER_API_KEY=
|
|
||||||
NUXT_OPEN_WEATHER_LAT=
|
|
||||||
NUXT_OPEN_WEATHER_LON=
|
|
||||||
NUXT_OPEN_WEATHER_LANG=
|
|
||||||
NUXT_OPEN_WEATHER_UNITS=
|
|
||||||
|
|
||||||
NUXT_HUB_PROJECT_KEY=
|
|
||||||
|
|
||||||
NUXT_PUBLIC_MAPBOX_STYLE=
|
|
||||||
NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN=
|
|
||||||
@@ -19,5 +19,12 @@ export default defineAppConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
textarea: {
|
||||||
|
color: {
|
||||||
|
white: {
|
||||||
|
outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-zinc-500 dark:focus:ring-zinc-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ useHead({
|
|||||||
title: 'ArtHome by Arthur Danjou',
|
title: 'ArtHome by Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { loggedIn } = useUserSession()
|
const { loggedIn } = await useUserSession()
|
||||||
|
|
||||||
watch(loggedIn, async () => {
|
watch(loggedIn, async () => {
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
|
|||||||
22
app/components/App/Avatar.vue
Normal file
22
app/components/App/Avatar.vue
Normal 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>
|
||||||
@@ -7,7 +7,7 @@ defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
defineEmits(['createTab'])
|
defineEmits(['createTab'])
|
||||||
|
|
||||||
const { canCreateTabInCategory } = await useUserLimit()
|
const { canCreateTabInCategory } = await useUserLimits()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
10
app/components/App/Footer.vue
Normal file
10
app/components/App/Footer.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// todo: implement the footer
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const { user, loggedIn, clear } = useUserSession()
|
const { user, loggedIn, clear } = await useUserSession()
|
||||||
const isSettingsOpen = ref(false)
|
const isSettingsOpen = ref(false)
|
||||||
|
|
||||||
const isDark = computed(() => colorMode.preference === 'dark')
|
const isDark = computed(() => colorMode.preference === 'dark')
|
||||||
@@ -33,20 +33,21 @@ const items = [
|
|||||||
}],
|
}],
|
||||||
]
|
]
|
||||||
|
|
||||||
function toggleColorMode() {
|
|
||||||
colorMode.preference = isDark.value ? 'light' : 'dark'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await clear()
|
await clear()
|
||||||
navigateTo('/login')
|
await navigateTo('/')
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleColorMode() {
|
||||||
|
colorMode.preference = isDark.value ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
t: () => toggleColorMode(),
|
t: () => toggleColorMode(),
|
||||||
s: () => isSettingsOpen.value = !isSettingsOpen.value,
|
s: () => isSettingsOpen.value = !isSettingsOpen.value,
|
||||||
l: async () => await logout(),
|
l: async () => await logout(),
|
||||||
|
h: () => navigateTo('/'),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ defineShortcuts({
|
|||||||
:ui="{ item: { disabled: 'cursor-text select-text' } }"
|
:ui="{ item: { disabled: 'cursor-text select-text' } }"
|
||||||
:popper="{ placement: 'bottom-end' }"
|
:popper="{ placement: 'bottom-end' }"
|
||||||
>
|
>
|
||||||
<UAvatar :src="user.avatar" />
|
<AppAvatar :src="user.avatar" />
|
||||||
|
|
||||||
<template #account>
|
<template #account>
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
@@ -95,7 +96,7 @@ defineShortcuts({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #logout="{ item }">
|
<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">
|
<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" />
|
<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>
|
<span class="truncate">{{ item.label }}</span>
|
||||||
@@ -123,12 +124,22 @@ defineShortcuts({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="space-y-12">
|
<div class="space-y-12 overflow-auto">
|
||||||
<div>
|
<div>
|
||||||
|
<AppUserSettingsForm :user="user" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton
|
||||||
|
color="red"
|
||||||
|
variant="solid"
|
||||||
|
icon="i-ph:trash-duotone"
|
||||||
|
block
|
||||||
|
>
|
||||||
Delete account
|
Delete account
|
||||||
Change user details
|
</UButton>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TabType } from '~~/types/types'
|
import type { TabType } from '~~/types/types'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
tab: TabType
|
tab: TabType
|
||||||
|
editMode: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { setTabPrimary } = await useTabs()
|
const { setTabPrimary } = await useTabs()
|
||||||
@@ -46,21 +47,28 @@ function openDeleteTabModal(tab: TabType) {
|
|||||||
currentDeleteTab.value = tab
|
currentDeleteTab.value = tab
|
||||||
deleteTabModal.value = true
|
deleteTabModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visitLink() {
|
||||||
|
if (!props.editMode) {
|
||||||
|
window.open(props.tab.link, '_blank')
|
||||||
|
// add view count
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ULink
|
<UCard
|
||||||
:to="tab.link"
|
:ui="{
|
||||||
class="relative"
|
body: { base: 'h-full relative z-20' },
|
||||||
target="_blank"
|
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">
|
<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="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`" />
|
<span class="relative inline-flex rounded-full h-3 w-3" :class="`bg-${tab.color}-400`" />
|
||||||
</div>
|
</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 items-center justify-between h-full">
|
||||||
<div class="flex gap-4 items-center h-full">
|
<div class="flex gap-4 items-center h-full">
|
||||||
<UBadge :color="tab.color" class="p-2" variant="soft">
|
<UBadge :color="tab.color" class="p-2" variant="soft">
|
||||||
@@ -75,13 +83,15 @@ function openDeleteTabModal(tab: TabType) {
|
|||||||
<UDropdown
|
<UDropdown
|
||||||
:items="items"
|
:items="items"
|
||||||
:popper="{ placement: 'bottom-end', arrow: true }"
|
:popper="{ placement: 'bottom-end', arrow: true }"
|
||||||
:ui="{ container: 'z-50 group', width: 'w-40', shadow: 'shadow-2xl' }"
|
:ui="{ container: 'z-50 group', width: 'w-40', shadow: 'shadow-2xl', wrapper: 'absolute inline-flex -top-3 -right-3' }"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
|
v-show="editMode"
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="ghost"
|
variant="solid"
|
||||||
|
size="md"
|
||||||
:padded="false"
|
:padded="false"
|
||||||
size="sm"
|
:ui="{ rounded: 'rounded-full p-1' }"
|
||||||
icon="i-ph:dots-three-outline-duotone"
|
icon="i-ph:dots-three-outline-duotone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -115,5 +125,28 @@ function openDeleteTabModal(tab: TabType) {
|
|||||||
:tab="currentDeleteTab"
|
:tab="currentDeleteTab"
|
||||||
@close-modal="deleteTabModal = false"
|
@close-modal="deleteTabModal = false"
|
||||||
/>
|
/>
|
||||||
</ULink>
|
|
||||||
</template>
|
</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>
|
||||||
|
|||||||
115
app/components/App/UserSettingsForm.vue
Normal file
115
app/components/App/UserSettingsForm.vue
Normal 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>
|
||||||
@@ -5,6 +5,7 @@ import { COLORS, CreateCategorySchema } from '~~/types/types'
|
|||||||
|
|
||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { createCategory } = await useCategories()
|
const { createCategory } = await useCategories()
|
||||||
|
const { refreshUserLimits, canCreateCategory } = await useUserLimits()
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
@@ -14,6 +15,12 @@ const state = reactive({
|
|||||||
|
|
||||||
async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
|
async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
|
||||||
await createCategory(event.data)
|
await createCategory(event.data)
|
||||||
|
await refreshUserLimits()
|
||||||
|
|
||||||
|
if (!canCreateCategory()) {
|
||||||
|
useErrorToast('You have reach the limit of categories', 'Subscribe to a paid plan to create more categories')
|
||||||
|
}
|
||||||
|
|
||||||
emit('closeModal')
|
emit('closeModal')
|
||||||
state.color = COLORS[0]
|
state.color = COLORS[0]
|
||||||
state.nameVisible = true
|
state.nameVisible = true
|
||||||
@@ -34,7 +41,7 @@ const { loading, search } = useIcons()
|
|||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { createTab } = await useTabs()
|
const { createTab } = await useTabs()
|
||||||
|
const { refreshUserLimits, canCreateTabInCategory } = await useUserLimits()
|
||||||
const { categories } = await useCategories()
|
const { categories } = await useCategories()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -24,7 +25,16 @@ watchEffect(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
|
async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
|
||||||
await createTab(event.data)
|
await createTab({
|
||||||
|
primary: Boolean(event.data.primary),
|
||||||
|
...event.data,
|
||||||
|
})
|
||||||
|
await refreshUserLimits()
|
||||||
|
|
||||||
|
if (!canCreateTabInCategory(state.categoryId)) {
|
||||||
|
useErrorToast('You have reach the limit of tabs in this category', 'Subscribe to a paid plan to create more tabs')
|
||||||
|
}
|
||||||
|
|
||||||
emit('closeModal')
|
emit('closeModal')
|
||||||
state.name = undefined
|
state.name = undefined
|
||||||
state.icon = undefined
|
state.icon = undefined
|
||||||
@@ -47,7 +57,7 @@ const { loading, search } = useIcons()
|
|||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
|
|||||||
18
app/components/Modal/DeleteAccount.vue
Normal file
18
app/components/Modal/DeleteAccount.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal>
|
||||||
|
avatar
|
||||||
|
Warning: This will permanently delete your account, all your categories, and all your tabs.
|
||||||
|
---
|
||||||
|
To verify, type confirm delete account below
|
||||||
|
input
|
||||||
|
confirm
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -7,9 +7,11 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { deleteCategory } = await useCategories()
|
const { deleteCategory } = await useCategories()
|
||||||
|
const { refreshUserLimits } = await useUserLimits()
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
await deleteCategory(props.category.id)
|
await deleteCategory(props.category.id)
|
||||||
|
await refreshUserLimits()
|
||||||
emit('closeModal')
|
emit('closeModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ defineShortcuts({
|
|||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { deleteTab } = await useTabs()
|
const { deleteTab } = await useTabs()
|
||||||
|
const { refreshUserLimits } = await useUserLimits()
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
await deleteTab(props.tab.id)
|
await deleteTab(props.tab.id)
|
||||||
|
await refreshUserLimits()
|
||||||
emit('closeModal')
|
emit('closeModal')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ defineShortcuts({
|
|||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { COLORS, type CategoryType, UpdateCategorySchema } from '~~/types/types'
|
import type { CategoryType, UpdateCategorySchemaType } from '~~/types/types'
|
||||||
|
import { COLORS, UpdateCategorySchema } from '~~/types/types'
|
||||||
import type { FormSubmitEvent } from '#ui/types'
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -24,7 +25,7 @@ watchEffect(() => {
|
|||||||
state.nameVisible = props.category?.nameVisible
|
state.nameVisible = props.category?.nameVisible
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchema>) {
|
async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchemaType>) {
|
||||||
await updateCategory({
|
await updateCategory({
|
||||||
id: props.category!.id,
|
id: props.category!.id,
|
||||||
...event.data,
|
...event.data,
|
||||||
@@ -43,7 +44,7 @@ async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchema>) {
|
|||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const state = reactive({
|
|||||||
color: COLORS[0],
|
color: COLORS[0],
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
categoryId: undefined,
|
categoryId: undefined,
|
||||||
|
link: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -26,6 +27,7 @@ watchEffect(() => {
|
|||||||
state.color = props.tab?.color
|
state.color = props.tab?.color
|
||||||
state.primary = props.tab?.primary
|
state.primary = props.tab?.primary
|
||||||
state.categoryId = props.tab?.categoryId
|
state.categoryId = props.tab?.categoryId
|
||||||
|
state.link = props.tab?.link
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
|
async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
|
||||||
@@ -44,11 +46,11 @@ async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
Update category '{{ tab.name }}'
|
Update tab '{{ tab.name }}'
|
||||||
</h3>
|
</h3>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
@click="$emit('closeModal')"
|
@click="$emit('closeModal')"
|
||||||
@@ -97,6 +99,10 @@ async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
|
|||||||
<USelect v-model="state.categoryId" :options="categories" option-attribute="name" value-attribute="id" />
|
<USelect v-model="state.categoryId" :options="categories" option-attribute="name" value-attribute="id" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Link " name="link">
|
||||||
|
<UInput v-model="state.link" type="text" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup>
|
<UFormGroup>
|
||||||
<UCheckbox v-model="state.primary" :color="state.color" label="Is the category primary?" />
|
<UCheckbox v-model="state.primary" :color="state.color" label="Is the category primary?" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|||||||
@@ -4,42 +4,49 @@ export async function useCategories() {
|
|||||||
const { data: categories, refresh }
|
const { data: categories, refresh }
|
||||||
= await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories'))
|
= await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories'))
|
||||||
|
|
||||||
async function getCategory(id: number): CategoryType {
|
|
||||||
return categories.data.value.find(category => category.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createCategory(category: CreateCategorySchema) {
|
async function createCategory(category: CreateCategorySchema) {
|
||||||
await $fetch('/api/categories', {
|
try {
|
||||||
|
await useRequestFetch()('/api/categories', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(category),
|
body: JSON.stringify(category),
|
||||||
})
|
})
|
||||||
.catch(error => useErrorToast('Category creation failed!', `Error: ${error}`))
|
|
||||||
await refresh()
|
await refresh()
|
||||||
await useSuccessToast('Category successfully created!')
|
await useSuccessToast('Category successfully created!', category.color)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('Category creation failed!', error as string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateCategory(category: UpdateCategorySchema & { id: number }) {
|
async function updateCategory(category: UpdateCategorySchema & { id: number }) {
|
||||||
|
try {
|
||||||
await $fetch(`/api/categories/${category.id}`, {
|
await $fetch(`/api/categories/${category.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(category),
|
body: JSON.stringify(category),
|
||||||
})
|
})
|
||||||
.catch(error => useErrorToast('Category update failed!', `Error: ${error}`))
|
|
||||||
await refresh()
|
await refresh()
|
||||||
await useSuccessToast('Category successfully updated!')
|
await useSuccessToast('Category successfully updated!')
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('Category update failed!', error as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteCategory(id: number) {
|
async function deleteCategory(id: number) {
|
||||||
|
try {
|
||||||
await $fetch(`/api/categories/${id}`, {
|
await $fetch(`/api/categories/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
.catch(error => useErrorToast('Category deletion failed!', `Error: ${error}`))
|
|
||||||
await refresh()
|
await refresh()
|
||||||
await useSuccessToast('Category successfully deleted!')
|
await useSuccessToast('Category successfully deleted!')
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('Category deletion failed!', error as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
getCategory,
|
|
||||||
createCategory,
|
createCategory,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
deleteCategory,
|
deleteCategory,
|
||||||
|
|||||||
@@ -9,30 +9,35 @@ export async function useTabs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createTab(tab: CreateTabSchema) {
|
async function createTab(tab: CreateTabSchema) {
|
||||||
|
try {
|
||||||
await $fetch('/api/tabs', {
|
await $fetch('/api/tabs', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(tab),
|
body: JSON.stringify(tab),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
|
||||||
await refresh()
|
await refresh()
|
||||||
useSuccessToast('Tab successfully created!')
|
useSuccessToast('Tab successfully created!', tab.color)
|
||||||
})
|
}
|
||||||
.catch(error => useErrorToast('Tab creation failed!', `Error: ${error}`))
|
catch (error) {
|
||||||
|
useErrorToast('Tab creation failed!', error as string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTab(tab: UpdateTabSchema) {
|
async function updateTab(tab: UpdateTabSchema) {
|
||||||
|
try {
|
||||||
await $fetch(`/api/tabs/${tab.id}`, {
|
await $fetch(`/api/tabs/${tab.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(tab),
|
body: JSON.stringify(tab),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
|
||||||
await refresh()
|
await refresh()
|
||||||
useSuccessToast('Tab successfully updated!')
|
useSuccessToast('Tab successfully updated!')
|
||||||
})
|
}
|
||||||
.catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
|
catch (error) {
|
||||||
|
useErrorToast('Tab update failed!', error as string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setTabPrimary(tab, primary: boolean) {
|
async function setTabPrimary(tab, primary: boolean) {
|
||||||
|
try {
|
||||||
await $fetch(`/api/tabs/${tab.id}`, {
|
await $fetch(`/api/tabs/${tab.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -40,21 +45,26 @@ export async function useTabs() {
|
|||||||
categoryId: tab.categoryId,
|
categoryId: tab.categoryId,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
|
||||||
await refresh()
|
await refresh()
|
||||||
useSuccessToast('Tab favorite toggled with success!')
|
useSuccessToast(`Tab ${tab.name} ${primary ? 'set as favorite' : 'unset as favorite'}!`, 'yellow')
|
||||||
})
|
}
|
||||||
.catch(error => useErrorToast('Cannot toggle Tab favorite!', `Error: ${error}`))
|
catch (error) {
|
||||||
|
useErrorToast('Cannot toggle favorite state for tab!', error as string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTab(id: number) {
|
async function deleteTab(id: number) {
|
||||||
|
try {
|
||||||
await $fetch(`/api/tabs/${id}`, {
|
await $fetch(`/api/tabs/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
.catch(error => useErrorToast('Tab deletion failed!', `Error: ${error}`))
|
|
||||||
await refresh()
|
await refresh()
|
||||||
useSuccessToast('Tab successfully deleted!')
|
useSuccessToast('Tab successfully deleted!')
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('Tab deletion failed!', error as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export function useSuccessToast(title: string, description?: string) {
|
export function useSuccessToast(title: string, color?: string, description?: string) {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
color: 'green',
|
color: color || 'green',
|
||||||
icon: 'i-ph:check-circle-duotone',
|
icon: 'i-ph:check-circle-duotone',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
export async function useUserLimit() {
|
|
||||||
const { user } = useUserSession()
|
|
||||||
const { categories } = await useCategories()
|
|
||||||
const { tabs } = await useTabs()
|
|
||||||
|
|
||||||
const hasPaidPlan = computed(() => user.value.subscription !== 'free')
|
|
||||||
|
|
||||||
function canCreateCategory() {
|
|
||||||
if (hasPaidPlan.value)
|
|
||||||
return true
|
|
||||||
return categories.value.length < 3
|
|
||||||
}
|
|
||||||
|
|
||||||
function canCreateTabInCategory(categoryId: number): boolean {
|
|
||||||
if (hasPaidPlan.value)
|
|
||||||
return true
|
|
||||||
return tabs.filter(tab => tab.categoryId === categoryId).length < 5
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasPaidPlan,
|
|
||||||
userLimits,
|
|
||||||
canCreateCategory,
|
|
||||||
canCreateTabInCategory,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
app/composables/user-limits.ts
Normal file
28
app/composables/user-limits.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const MAX_CATEGORIES = 3
|
||||||
|
const MAX_TABS_PER_CATEGORY = 6
|
||||||
|
|
||||||
|
export async function useUserLimits() {
|
||||||
|
const { user } = useUserSession()
|
||||||
|
const { data: userLimits, refresh: refreshUserLimits } = await useFetch('/api/users/limits')
|
||||||
|
|
||||||
|
const hasPaidPlan = computed(() => user.value.subscription !== 'free')
|
||||||
|
|
||||||
|
function canCreateCategory() {
|
||||||
|
if (hasPaidPlan.value)
|
||||||
|
return true
|
||||||
|
return userLimits.value.categories.length < MAX_CATEGORIES
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCreateTabInCategory(categoryId: number): boolean {
|
||||||
|
if (hasPaidPlan.value)
|
||||||
|
return true
|
||||||
|
return userLimits.value.categories.find(category => category.id === categoryId).tabs.length < MAX_TABS_PER_CATEGORY
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasPaidPlan,
|
||||||
|
canCreateCategory,
|
||||||
|
canCreateTabInCategory,
|
||||||
|
refreshUserLimits,
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/composables/users.ts
Normal file
43
app/composables/users.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export async function useUser() {
|
||||||
|
const { fetch } = useUserSession()
|
||||||
|
|
||||||
|
async function deleteAvatar() {
|
||||||
|
try {
|
||||||
|
await useRequestFetch()('/api/users/avatars', {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
useSuccessToast('Avatar successfully deleted!')
|
||||||
|
await fetch()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('An error occurred while deleting your avatar', error as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAvatar(event: Event) {
|
||||||
|
const file = event[0] as File
|
||||||
|
|
||||||
|
if (!file)
|
||||||
|
return
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useRequestFetch()('/api/users/avatars', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
await fetch()
|
||||||
|
useSuccessToast('Avatar successfully uploaded!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
useErrorToast('An error occurred while uploading your avatar', error as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteAvatar,
|
||||||
|
uploadAvatar,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export default defineNuxtRouteMiddleware(async () => {
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
const { loggedIn } = useUserSession()
|
const { loggedIn, user } = await useUserSession()
|
||||||
|
|
||||||
if (loggedIn.value) {
|
if (loggedIn.value) {
|
||||||
return navigateTo('/')
|
return navigateTo(`/${user.value.username.toLowerCase()}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,101 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const date = ref<Date>(new Date())
|
||||||
|
onMounted(() => {
|
||||||
|
setInterval(() => date.value = new Date(), 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = useUserSession()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (user.value.username.toLowerCase() === router.currentRoute.value.params.user.toLowerCase()) {
|
||||||
|
await navigateTo('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const { data: userDetails } = await useAsyncData(async () => await $fetch(`/api/users/${router.currentRoute.value.params.user}`))
|
||||||
|
onMounted(() => {
|
||||||
|
if (userDetails.value.message) {
|
||||||
|
useErrorToast(userDetails.value.message, 'Look for another user.')
|
||||||
|
}
|
||||||
|
if (userDetails.value.private) {
|
||||||
|
useErrorToast('This user\'s profile is private.', 'Look for another user.')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<main class="my-12">
|
||||||
{{ router.currentRoute.value.params.user }}
|
<div v-if="date" class="flex flex-col items-center mb-12">
|
||||||
|
<h1 class="text-6xl md:text-9xl font-bold">
|
||||||
|
{{ useDateFormat(date, 'HH') }}
|
||||||
|
<span class="animate-pulse">:</span>
|
||||||
|
{{ useDateFormat(date, 'mm') }}
|
||||||
|
</h1>
|
||||||
|
<h1 class="text-2xl md:text-5xl">
|
||||||
|
{{ useDateFormat(date, 'dddd D MMMM YYYY', { locales: userDetails.locale ? userDetails.language : user.locale }) }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div v-if="userDetails.message || userDetails.private" class="text-center mt-24 space-y-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-2 text-3xl"
|
||||||
|
:class="userDetails.message ? 'text-amber-500 dark:text-amber-400' : 'text-red-500 dark:text-red-400'"
|
||||||
|
>
|
||||||
|
<UIcon name="i-ph:warning-circle-duotone" />
|
||||||
|
<p>
|
||||||
|
{{ userDetails.message ? userDetails.message : 'This user\'s profile is private.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-lg italic text-neutral-600 dark:text-neutral-400">
|
||||||
|
Please look for another user.
|
||||||
|
</h1>
|
||||||
|
<UButton
|
||||||
|
label="Go to your page"
|
||||||
|
:color="userDetails.message ? 'amber' : 'red'"
|
||||||
|
size="xl"
|
||||||
|
icon="i-ph:house-line-duotone"
|
||||||
|
variant="outline"
|
||||||
|
to="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<section v-else>
|
||||||
|
<div v-if="userDetails.categories.length > 0" class="space-y-12">
|
||||||
|
<div
|
||||||
|
v-for="category in userDetails.categories"
|
||||||
|
:key="category.id"
|
||||||
|
>
|
||||||
|
<div v-if="category" class="flex items-center mb-4" :class="category.nameVisible ? 'justify-between' : 'justify-end'">
|
||||||
|
<div v-if="category.nameVisible" class="flex items-center gap-2 mb-4" :class="`text-${category.color}-500`">
|
||||||
|
<UIcon :name="category.icon" size="28" />
|
||||||
|
<h1 class="font-bold text-2xl">
|
||||||
|
{{ category.name }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="userDetails.categories.filter(tab => tab.categoryId === category.id).length > 0"
|
||||||
|
class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
{{ userDetails.categories.filter(tab => tab.categoryId === category.id) }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
<UIcon name="i-ph:empty-duotone" size="16" />
|
||||||
|
<h1 class="text-sm font-medium">
|
||||||
|
The category is empty.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
<UIcon name="i-ph:empty-duotone" size="20" />
|
||||||
|
<h1 class="text-lg font-medium">
|
||||||
|
This user doesn't have any categories.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import type { CategoryType } from '~~/types/types'
|
import type { CategoryType } from '~~/types/types'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -10,10 +10,10 @@ onMounted(() => {
|
|||||||
setInterval(() => date.value = new Date(), 1000)
|
setInterval(() => date.value = new Date(), 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { user } = useUserSession()
|
const { user } = await useUserSession()
|
||||||
const { categories } = await useCategories()
|
const { categories } = await useCategories()
|
||||||
const { getTabsForCategory } = await useTabs()
|
const { getTabsForCategory } = await useTabs()
|
||||||
const { canCreateCategory } = await useUserLimit()
|
const { canCreateCategory } = await useUserLimits()
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const createCategoryModal = ref(false)
|
const createCategoryModal = ref(false)
|
||||||
@@ -42,14 +42,23 @@ function openCreateTab(category: CategoryType) {
|
|||||||
createTabModal.value = true
|
createTabModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit Tabs
|
||||||
|
const currentEditCategory = ref<CategoryType | null>(null)
|
||||||
|
|
||||||
// DropDown Items
|
// DropDown Items
|
||||||
const items = [[
|
const items = [[
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit Category',
|
||||||
icon: 'i-ph:pencil-duotone',
|
icon: 'i-ph:pencil-duotone',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
click: category => openUpdateCategoryModal(category),
|
click: category => openUpdateCategoryModal(category),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit Tabs',
|
||||||
|
icon: 'i-ph:cards-three-duotone',
|
||||||
|
color: 'amber',
|
||||||
|
click: category => currentEditCategory.value?.id === category.id ? currentEditCategory.value = null : currentEditCategory.value = category,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
icon: 'i-ph:trash-duotone',
|
icon: 'i-ph:trash-duotone',
|
||||||
@@ -64,6 +73,23 @@ defineShortcuts({
|
|||||||
createCategoryModal.value = true
|
createCategoryModal.value = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
escape: () => {
|
||||||
|
if (createCategoryModal.value) {
|
||||||
|
createCategoryModal.value = false
|
||||||
|
}
|
||||||
|
if (updateCategoryModal.value) {
|
||||||
|
updateCategoryModal.value = false
|
||||||
|
}
|
||||||
|
if (deleteCategoryModal.value) {
|
||||||
|
deleteCategoryModal.value = false
|
||||||
|
}
|
||||||
|
if (createTabModal.value) {
|
||||||
|
createTabModal.value = false
|
||||||
|
}
|
||||||
|
if (currentEditCategory.value) {
|
||||||
|
currentEditCategory.value = null
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -116,11 +142,15 @@ defineShortcuts({
|
|||||||
:category="category"
|
:category="category"
|
||||||
@create-tab="openCreateTab(category)"
|
@create-tab="openCreateTab(category)"
|
||||||
/>
|
/>
|
||||||
<div v-if="getTabsForCategory(category.id).length > 0" class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 md:grid-cols-4 gap-4">
|
<div
|
||||||
|
v-if="getTabsForCategory(category.id).length > 0"
|
||||||
|
class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
<AppTab
|
<AppTab
|
||||||
v-for="tab in getTabsForCategory(category.id)"
|
v-for="tab in getTabsForCategory(category.id)"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:tab="tab"
|
:tab="tab"
|
||||||
|
:edit-mode="currentEditCategory?.id === category.id"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex gap-2 items-center">
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { z } from 'zod'
|
|
||||||
import { useSession } from 'h3'
|
import { useSession } from 'h3'
|
||||||
import type { FormSubmitEvent } from '#ui/types'
|
|
||||||
|
|
||||||
const { loggedIn } = useUserSession()
|
const { loggedIn } = await useUserSession()
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'ghost',
|
middleware: 'ghost',
|
||||||
layout: 'login',
|
layout: 'login',
|
||||||
})
|
})
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
email: z.string().email('Invalid email'),
|
|
||||||
})
|
|
||||||
|
|
||||||
type Schema = z.output<typeof schema>
|
|
||||||
const state = reactive({ email: undefined })
|
|
||||||
|
|
||||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
|
||||||
// Do something with data
|
|
||||||
// todo: add login logic
|
|
||||||
console.log(event.data)
|
|
||||||
state.email = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = useState<string>('message')
|
const message = useState<string>('message')
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
const session = await useSession(useRequestEvent()!, {
|
const session = await useSession(useRequestEvent()!, {
|
||||||
@@ -62,20 +46,6 @@ if (import.meta.server) {
|
|||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div v-if="!loggedIn" class="flex flex-col gap-4 p-4">
|
<div v-if="!loggedIn" class="flex flex-col gap-4 p-4">
|
||||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
|
||||||
<UFormGroup name="email">
|
|
||||||
<UInput v-model="state.email" color="gray" placeholder="arthur@arthome.com" />
|
|
||||||
</UFormGroup>
|
|
||||||
<UButton
|
|
||||||
:external="true"
|
|
||||||
color="gray"
|
|
||||||
icon="i-ph:envelope-duotone"
|
|
||||||
label="Continue with Email"
|
|
||||||
block
|
|
||||||
type="submit"
|
|
||||||
/>
|
|
||||||
</UForm>
|
|
||||||
<UDivider label="or" />
|
|
||||||
<UButton
|
<UButton
|
||||||
:external="true"
|
:external="true"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Config } from 'tailwindcss'
|
|
||||||
import typography from '@tailwindcss/typography'
|
|
||||||
|
|
||||||
export default <Partial<Config>>{
|
|
||||||
content: [
|
|
||||||
'./components/**/*.{vue,js,ts}',
|
|
||||||
'./layouts/**/*.vue',
|
|
||||||
'./pages/**/*.vue',
|
|
||||||
'./composables/**/*.{js,ts}',
|
|
||||||
'./plugins/**/*.{js,ts}',
|
|
||||||
'./utils/**/*.{js,ts}',
|
|
||||||
'./App.{js,ts,vue}',
|
|
||||||
'./app.{js,ts,vue}',
|
|
||||||
'./Error.{js,ts,vue}',
|
|
||||||
'./error.{js,ts,vue}',
|
|
||||||
'./app.config.{js,ts}',
|
|
||||||
'content/**/*.md',
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [typography],
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,10 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
compatibilityDate: '2024-08-21',
|
compatibilityDate: '2024-08-21',
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
},
|
||||||
|
|
||||||
// Nuxt App
|
// Nuxt App
|
||||||
app: {
|
app: {
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
@@ -41,7 +45,7 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
// Nuxt Icon
|
// Nuxt Icon
|
||||||
icon: {
|
icon: {
|
||||||
serverBundle: 'remote',
|
serverBundle: ['ph', 'heroicons'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Nuxt Color Mode
|
// Nuxt Color Mode
|
||||||
|
|||||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/content':
|
'@nuxt/content':
|
||||||
specifier: ^2.13.2
|
specifier: ^2.13.2
|
||||||
version: 2.13.2(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
version: 2.13.2(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
||||||
'@nuxt/image':
|
'@nuxt/image':
|
||||||
specifier: ^1.7.1
|
specifier: ^1.7.1
|
||||||
version: 1.7.1(ioredis@5.4.1)(magicast@0.3.5)(rollup@4.21.1)
|
version: 1.7.1(ioredis@5.4.1)(magicast@0.3.5)(rollup@4.21.1)
|
||||||
@@ -25,7 +25,7 @@ importers:
|
|||||||
version: 3.2.0(magicast@0.3.5)(rollup@4.21.1)
|
version: 3.2.0(magicast@0.3.5)(rollup@4.21.1)
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.33.0
|
specifier: ^0.33.0
|
||||||
version: 0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)
|
version: 0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1)
|
||||||
h3-zod:
|
h3-zod:
|
||||||
specifier: ^0.5.3
|
specifier: ^0.5.3
|
||||||
version: 0.5.3(h3@1.12.0)(zod@3.23.8)
|
version: 0.5.3(h3@1.12.0)(zod@3.23.8)
|
||||||
@@ -62,7 +62,7 @@ importers:
|
|||||||
version: 11.0.3(vue@3.4.38(typescript@5.5.4))
|
version: 11.0.3(vue@3.4.38(typescript@5.5.4))
|
||||||
'@vueuse/nuxt':
|
'@vueuse/nuxt':
|
||||||
specifier: ^11.0.3
|
specifier: ^11.0.3
|
||||||
version: 11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
version: 11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.5
|
specifier: ^16.4.5
|
||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
@@ -77,7 +77,7 @@ importers:
|
|||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^3.13.0
|
specifier: ^3.13.0
|
||||||
version: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
version: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
||||||
nuxt-mapbox:
|
nuxt-mapbox:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.0(magicast@0.3.5)(rollup@4.21.1)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))
|
version: 1.6.0(magicast@0.3.5)(rollup@4.21.1)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))
|
||||||
@@ -4120,6 +4120,10 @@ packages:
|
|||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lowercase-keys@2.0.0:
|
lowercase-keys@2.0.0:
|
||||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -5178,6 +5182,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
react@18.3.1:
|
||||||
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
read-cache@1.0.0:
|
read-cache@1.0.0:
|
||||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||||
|
|
||||||
@@ -7320,13 +7328,13 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.17.1
|
fastq: 1.17.1
|
||||||
|
|
||||||
'@nuxt/content@2.13.2(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
'@nuxt/content@2.13.2(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
||||||
'@nuxtjs/mdc': 0.8.3(magicast@0.3.5)(rollup@4.21.1)
|
'@nuxtjs/mdc': 0.8.3(magicast@0.3.5)(rollup@4.21.1)
|
||||||
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4))
|
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4))
|
||||||
'@vueuse/head': 2.0.0(vue@3.4.38(typescript@5.5.4))
|
'@vueuse/head': 2.0.0(vue@3.4.38(typescript@5.5.4))
|
||||||
'@vueuse/nuxt': 10.11.1(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
'@vueuse/nuxt': 10.11.1(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))
|
||||||
consola: 3.2.3
|
consola: 3.2.3
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.3
|
destr: 2.0.3
|
||||||
@@ -8581,13 +8589,13 @@ snapshots:
|
|||||||
|
|
||||||
'@vueuse/metadata@11.0.3': {}
|
'@vueuse/metadata@11.0.3': {}
|
||||||
|
|
||||||
'@vueuse/nuxt@10.11.1(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
'@vueuse/nuxt@10.11.1(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
||||||
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4))
|
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4))
|
||||||
'@vueuse/metadata': 10.11.1
|
'@vueuse/metadata': 10.11.1
|
||||||
local-pkg: 0.5.0
|
local-pkg: 0.5.0
|
||||||
nuxt: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
nuxt: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
||||||
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
|
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
@@ -8596,13 +8604,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@vueuse/nuxt@11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
'@vueuse/nuxt@11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)))(rollup@4.21.1)(vue@3.4.38(typescript@5.5.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.1)
|
||||||
'@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4))
|
'@vueuse/core': 11.0.3(vue@3.4.38(typescript@5.5.4))
|
||||||
'@vueuse/metadata': 11.0.3
|
'@vueuse/metadata': 11.0.3
|
||||||
local-pkg: 0.5.0
|
local-pkg: 0.5.0
|
||||||
nuxt: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
nuxt: 3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4))
|
||||||
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
|
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
@@ -9203,9 +9211,9 @@ snapshots:
|
|||||||
|
|
||||||
date-fns@3.6.0: {}
|
date-fns@3.6.0: {}
|
||||||
|
|
||||||
db0@0.1.4(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)):
|
db0@0.1.4(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-orm: 0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)
|
drizzle-orm: 0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1)
|
||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
|
|
||||||
@@ -9331,12 +9339,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4):
|
drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@cloudflare/workers-types': 4.20240821.1
|
'@cloudflare/workers-types': 4.20240821.1
|
||||||
'@types/pg': 8.11.7
|
'@types/pg': 8.11.7
|
||||||
pg: 8.12.0
|
pg: 8.12.0
|
||||||
postgres: 3.4.4
|
postgres: 3.4.4
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
duplexer@0.1.2: {}
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
@@ -10707,6 +10716,11 @@ snapshots:
|
|||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
lowercase-keys@2.0.0: {}
|
lowercase-keys@2.0.0: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
@@ -11275,7 +11289,7 @@ snapshots:
|
|||||||
mlly: 1.7.1
|
mlly: 1.7.1
|
||||||
pkg-types: 1.2.0
|
pkg-types: 1.2.0
|
||||||
|
|
||||||
nitropack@2.9.7(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(magicast@0.3.5):
|
nitropack@2.9.7(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(magicast@0.3.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.3.4
|
'@cloudflare/kv-asset-handler': 0.3.4
|
||||||
'@netlify/functions': 2.8.1
|
'@netlify/functions': 2.8.1
|
||||||
@@ -11298,7 +11312,7 @@ snapshots:
|
|||||||
cookie-es: 1.2.2
|
cookie-es: 1.2.2
|
||||||
croner: 8.1.1
|
croner: 8.1.1
|
||||||
crossws: 0.2.4
|
crossws: 0.2.4
|
||||||
db0: 0.1.4(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))
|
db0: 0.1.4(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.3
|
destr: 2.0.3
|
||||||
dot-prop: 8.0.2
|
dot-prop: 8.0.2
|
||||||
@@ -11486,7 +11500,7 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)):
|
nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@22.5.1)(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(meow@9.0.0)(optionator@0.9.4)(rollup@4.21.1)(terser@5.31.6)(typescript@5.5.4)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(vue-tsc@2.0.29(typescript@5.5.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devalue': 2.0.2
|
'@nuxt/devalue': 2.0.2
|
||||||
'@nuxt/devtools': 1.4.1(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))
|
'@nuxt/devtools': 1.4.1(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))
|
||||||
@@ -11520,7 +11534,7 @@ snapshots:
|
|||||||
knitwork: 1.1.0
|
knitwork: 1.1.0
|
||||||
magic-string: 0.30.11
|
magic-string: 0.30.11
|
||||||
mlly: 1.7.1
|
mlly: 1.7.1
|
||||||
nitropack: 2.9.7(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4))(magicast@0.3.5)
|
nitropack: 2.9.7(drizzle-orm@0.33.0(@cloudflare/workers-types@4.20240821.1)(@types/pg@8.11.7)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1))(magicast@0.3.5)
|
||||||
nuxi: 3.13.1
|
nuxi: 3.13.1
|
||||||
nypm: 0.3.11
|
nypm: 0.3.11
|
||||||
ofetch: 1.3.4
|
ofetch: 1.3.4
|
||||||
@@ -12168,6 +12182,11 @@ snapshots:
|
|||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react@18.3.1:
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
read-cache@1.0.0:
|
read-cache@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pify: 2.3.0
|
pify: 2.3.0
|
||||||
|
|||||||
@@ -8,12 +8,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await useValidatedBody(event, UpdateCategorySchema)
|
const body = await useValidatedBody(event, UpdateCategorySchema)
|
||||||
await useDrizzle()
|
await useDrizzle()
|
||||||
.update(tables.categories)
|
.update(tables.categories)
|
||||||
.set({
|
.set(body)
|
||||||
name: body.name,
|
|
||||||
icon: body.icon,
|
|
||||||
color: body.color,
|
|
||||||
nameVisible: body.nameVisible,
|
|
||||||
})
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tables.categories.id, id),
|
eq(tables.categories.id, id),
|
||||||
|
|||||||
@@ -6,11 +6,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
const user = await getUserSession(event)
|
const user = await getUserSession(event)
|
||||||
const body = await useValidatedBody(event, CreateCategorySchema)
|
const body = await useValidatedBody(event, CreateCategorySchema)
|
||||||
await useDrizzle().insert(tables.categories).values({
|
await useDrizzle().insert(tables.categories).values({
|
||||||
name: body.name,
|
|
||||||
icon: body.icon,
|
|
||||||
color: body.color,
|
|
||||||
nameVisible: body.nameVisible,
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
...body,
|
||||||
})
|
})
|
||||||
return { statusCode: 200 }
|
return { statusCode: 200 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await useValidatedBody(event, UpdateTabSchema)
|
const body = await useValidatedBody(event, UpdateTabSchema)
|
||||||
await useDrizzle()
|
await useDrizzle()
|
||||||
.update(tables.tabs)
|
.update(tables.tabs)
|
||||||
.set({
|
.set(body)
|
||||||
name: body.name,
|
|
||||||
icon: body.icon,
|
|
||||||
color: body.color,
|
|
||||||
primary: body.primary,
|
|
||||||
link: body.link,
|
|
||||||
})
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tables.tabs.id, id),
|
eq(tables.tabs.id, id),
|
||||||
|
|||||||
@@ -4,14 +4,7 @@ import { CreateTabSchema } from '~~/types/types'
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await useValidatedBody(event, CreateTabSchema)
|
const body = await useValidatedBody(event, CreateTabSchema)
|
||||||
await useDrizzle().insert(tables.tabs).values({
|
await useDrizzle().insert(tables.tabs).values(body)
|
||||||
name: body.name,
|
|
||||||
icon: body.icon,
|
|
||||||
color: body.color,
|
|
||||||
nameVisible: body.nameVisible,
|
|
||||||
categoryId: body.categoryId,
|
|
||||||
link: body.link,
|
|
||||||
})
|
|
||||||
return { statusCode: 200 }
|
return { statusCode: 200 }
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|||||||
19
server/api/users/[username].get.ts
Normal file
19
server/api/users/[username].get.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { username } = await getRouterParams(event)
|
||||||
|
const user = await useDrizzle()
|
||||||
|
.query
|
||||||
|
.users
|
||||||
|
.findFirst({
|
||||||
|
where: eq(tables.users.username, username.toLowerCase()),
|
||||||
|
with: {
|
||||||
|
categories: {
|
||||||
|
with: {
|
||||||
|
tabs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return user || {
|
||||||
|
message: 'User not found',
|
||||||
|
}
|
||||||
|
})
|
||||||
24
server/api/users/avatars/index.delete.ts
Normal file
24
server/api/users/avatars/index.delete.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { user, session } = await requireUserSession(event)
|
||||||
|
|
||||||
|
if (!user.avatar) {
|
||||||
|
return sendNoContent(event, 204)
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteProfilePicture(user.avatar)
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
avatar: null,
|
||||||
|
}
|
||||||
|
await updateUser(user.id, { avatar: updatedUser.avatar })
|
||||||
|
await replaceUserSession(event, {
|
||||||
|
id: user.id,
|
||||||
|
user: updatedUser,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 204,
|
||||||
|
message: 'Avatar deleted',
|
||||||
|
}
|
||||||
|
})
|
||||||
40
server/api/users/avatars/index.post.ts
Normal file
40
server/api/users/avatars/index.post.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { randomUUID } from 'uncrypto'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { user } = await requireUserSession(event)
|
||||||
|
|
||||||
|
const form = await readFormData(event)
|
||||||
|
const file = form.get('file') as File
|
||||||
|
|
||||||
|
if (!file || !file.size) {
|
||||||
|
throw createError({ statusCode: 400, message: 'No file provided' })
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureBlob(file, {
|
||||||
|
maxSize: '1MB',
|
||||||
|
types: ['image'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (user.avatar) {
|
||||||
|
await deleteProfilePicture(user.avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = randomUUID()
|
||||||
|
|
||||||
|
const avatar = await hubBlob().put(filename, file, {
|
||||||
|
addRandomSuffix: false,
|
||||||
|
prefix: 'avatars/',
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
avatar: avatar.pathname,
|
||||||
|
}
|
||||||
|
await updateUser(user.id, { avatar: updatedUser.avatar })
|
||||||
|
await replaceUserSession(event, {
|
||||||
|
id: user.id,
|
||||||
|
user: updatedUser,
|
||||||
|
})
|
||||||
|
|
||||||
|
return sendNoContent(event, 204)
|
||||||
|
})
|
||||||
13
server/api/users/limits.get.ts
Normal file
13
server/api/users/limits.get.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUserSession(event)
|
||||||
|
return useDrizzle().query.users.findFirst({
|
||||||
|
where: eq(tables.users.id, user.id),
|
||||||
|
with: {
|
||||||
|
categories: {
|
||||||
|
with: {
|
||||||
|
tabs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
17
server/api/users/me.put.ts
Normal file
17
server/api/users/me.put.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useValidatedBody } from 'h3-zod'
|
||||||
|
import { UpdateUserSchema } from '~~/types/types'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { user } = await requireUserSession(event)
|
||||||
|
const body = await useValidatedBody(event, UpdateUserSchema)
|
||||||
|
|
||||||
|
const updatedUser = {
|
||||||
|
...user,
|
||||||
|
...body,
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(user.id, updatedUser)
|
||||||
|
await replaceUserSession(event, updatedUser)
|
||||||
|
|
||||||
|
return sendNoContent(event, 204)
|
||||||
|
})
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE "public"."subscription" AS ENUM('free', 'paid');
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE IF NOT EXISTS "categories" (
|
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
|
||||||
"name" text DEFAULT '',
|
|
||||||
"name_visible" boolean DEFAULT true,
|
|
||||||
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
|
|
||||||
"color" text DEFAULT 'gray',
|
|
||||||
"user_id" integer NOT NULL,
|
|
||||||
"created_at" timestamp (3) DEFAULT now(),
|
|
||||||
"updated_at" timestamp (3)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE IF NOT EXISTS "tabs" (
|
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
|
||||||
"name" text DEFAULT '',
|
|
||||||
"name_visible" boolean DEFAULT true,
|
|
||||||
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
|
|
||||||
"color" text DEFAULT 'gray',
|
|
||||||
"category_id" integer NOT NULL,
|
|
||||||
"created_at" timestamp (3) DEFAULT now(),
|
|
||||||
"updated_at" timestamp (3)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE IF NOT EXISTS "users" (
|
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
|
||||||
"username" text NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
"github_id" text,
|
|
||||||
"github_token" text,
|
|
||||||
"google_id" text,
|
|
||||||
"google_token" text,
|
|
||||||
"description" text DEFAULT '',
|
|
||||||
"avatar" text DEFAULT '',
|
|
||||||
"private" boolean DEFAULT false,
|
|
||||||
"language" text DEFAULT 'en-EN',
|
|
||||||
"location" text DEFAULT 'unknown',
|
|
||||||
"subscription" "subscription" DEFAULT 'free',
|
|
||||||
"created_at" timestamp (3) DEFAULT now(),
|
|
||||||
"updated_at" timestamp (3),
|
|
||||||
CONSTRAINT "users_email_unique" UNIQUE("email"),
|
|
||||||
CONSTRAINT "users_github_id_unique" UNIQUE("github_id"),
|
|
||||||
CONSTRAINT "users_google_id_unique" UNIQUE("google_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
DO $$ BEGIN
|
|
||||||
ALTER TABLE "categories" ADD CONSTRAINT "categories_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
--> statement-breakpoint
|
|
||||||
DO $$ BEGIN
|
|
||||||
ALTER TABLE "tabs" ADD CONSTRAINT "tabs_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "tabs" ADD COLUMN "link" text DEFAULT '';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "tabs" ADD COLUMN "primary" boolean DEFAULT false;--> statement-breakpoint
|
|
||||||
ALTER TABLE "tabs" DROP COLUMN IF EXISTS "name_visible";
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "c52dbfc1-beae-4a41-8725-66def9fdacea",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.categories": {
|
|
||||||
"name": "categories",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"name_visible": {
|
|
||||||
"name": "name_visible",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"categories_user_id_users_id_fk": {
|
|
||||||
"name": "categories_user_id_users_id_fk",
|
|
||||||
"tableFrom": "categories",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.tabs": {
|
|
||||||
"name": "tabs",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"name_visible": {
|
|
||||||
"name": "name_visible",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"category_id": {
|
|
||||||
"name": "category_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"tabs_category_id_categories_id_fk": {
|
|
||||||
"name": "tabs_category_id_categories_id_fk",
|
|
||||||
"tableFrom": "tabs",
|
|
||||||
"tableTo": "categories",
|
|
||||||
"columnsFrom": [
|
|
||||||
"category_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"github_id": {
|
|
||||||
"name": "github_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"github_token": {
|
|
||||||
"name": "github_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_id": {
|
|
||||||
"name": "google_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_token": {
|
|
||||||
"name": "google_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"private": {
|
|
||||||
"name": "private",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"name": "language",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'en-EN'"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"name": "location",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'unknown'"
|
|
||||||
},
|
|
||||||
"subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"type": "subscription",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'free'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_github_id_unique": {
|
|
||||||
"name": "users_github_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"github_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_google_id_unique": {
|
|
||||||
"name": "users_google_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"google_id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"public.subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"free",
|
|
||||||
"paid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "1a96f2ca-db04-445d-b671-d61aaeef8882",
|
|
||||||
"prevId": "c52dbfc1-beae-4a41-8725-66def9fdacea",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.categories": {
|
|
||||||
"name": "categories",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"name_visible": {
|
|
||||||
"name": "name_visible",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"categories_user_id_users_id_fk": {
|
|
||||||
"name": "categories_user_id_users_id_fk",
|
|
||||||
"tableFrom": "categories",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.tabs": {
|
|
||||||
"name": "tabs",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"name_visible": {
|
|
||||||
"name": "name_visible",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"link": {
|
|
||||||
"name": "link",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"category_id": {
|
|
||||||
"name": "category_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"tabs_category_id_categories_id_fk": {
|
|
||||||
"name": "tabs_category_id_categories_id_fk",
|
|
||||||
"tableFrom": "tabs",
|
|
||||||
"tableTo": "categories",
|
|
||||||
"columnsFrom": [
|
|
||||||
"category_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"github_id": {
|
|
||||||
"name": "github_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"github_token": {
|
|
||||||
"name": "github_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_id": {
|
|
||||||
"name": "google_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_token": {
|
|
||||||
"name": "google_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"private": {
|
|
||||||
"name": "private",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"name": "language",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'en-EN'"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"name": "location",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'unknown'"
|
|
||||||
},
|
|
||||||
"subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"type": "subscription",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'free'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_github_id_unique": {
|
|
||||||
"name": "users_github_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"github_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_google_id_unique": {
|
|
||||||
"name": "users_google_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"google_id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"public.subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"free",
|
|
||||||
"paid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "b9aba4fe-7f04-4acc-b47f-2d29d739df98",
|
|
||||||
"prevId": "1a96f2ca-db04-445d-b671-d61aaeef8882",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.categories": {
|
|
||||||
"name": "categories",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"name_visible": {
|
|
||||||
"name": "name_visible",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"categories_user_id_users_id_fk": {
|
|
||||||
"name": "categories_user_id_users_id_fk",
|
|
||||||
"tableFrom": "categories",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.tabs": {
|
|
||||||
"name": "tabs",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"primary": {
|
|
||||||
"name": "primary",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'i-ph:circle-wavy-question-duotone'"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'gray'"
|
|
||||||
},
|
|
||||||
"link": {
|
|
||||||
"name": "link",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"category_id": {
|
|
||||||
"name": "category_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"tabs_category_id_categories_id_fk": {
|
|
||||||
"name": "tabs_category_id_categories_id_fk",
|
|
||||||
"tableFrom": "tabs",
|
|
||||||
"tableTo": "categories",
|
|
||||||
"columnsFrom": [
|
|
||||||
"category_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"github_id": {
|
|
||||||
"name": "github_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"github_token": {
|
|
||||||
"name": "github_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_id": {
|
|
||||||
"name": "google_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"google_token": {
|
|
||||||
"name": "google_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"private": {
|
|
||||||
"name": "private",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"name": "language",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'en-EN'"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"name": "location",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'unknown'"
|
|
||||||
},
|
|
||||||
"subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"type": "subscription",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "'free'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp (3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_github_id_unique": {
|
|
||||||
"name": "users_github_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"github_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"users_google_id_unique": {
|
|
||||||
"name": "users_google_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"google_id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"public.subscription": {
|
|
||||||
"name": "subscription",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"free",
|
|
||||||
"paid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1724865045534,
|
|
||||||
"tag": "0000_giant_stranger",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1724884620789,
|
|
||||||
"tag": "0001_fancy_tyger_tiger",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1725015619221,
|
|
||||||
"tag": "0002_cool_dexter_bennett",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ export const subscriptionEnum = pgEnum('subscription', Subscription)
|
|||||||
|
|
||||||
export const users = pgTable('users', {
|
export const users = pgTable('users', {
|
||||||
id,
|
id,
|
||||||
username: text('username').notNull(),
|
username: text('username').notNull().unique(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
githubId: text('github_id').unique(),
|
githubId: text('github_id').unique(),
|
||||||
@@ -59,3 +59,10 @@ export const categoriesRelations = relations(categories, ({ one, many }) => ({
|
|||||||
}),
|
}),
|
||||||
tabs: many(tabs),
|
tabs: many(tabs),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const tabsRelations = relations(tabs, ({ one }) => ({
|
||||||
|
category: one(categories, {
|
||||||
|
fields: [tabs.categoryId],
|
||||||
|
references: [categories.id],
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ export default oauthGitHubEventHandler({
|
|||||||
|
|
||||||
// If the user is not signed in and no user exists with that GitHub ID or email address, create a new user
|
// If the user is not signed in and no user exists with that GitHub ID or email address, create a new user
|
||||||
const createdUser = await createUser({
|
const createdUser = await createUser({
|
||||||
username: oauthUser.login as string,
|
username: oauthUser.login.toLowerCase() as string,
|
||||||
description: oauthUser.bio as string,
|
description: oauthUser.bio as string,
|
||||||
name: oauthUser.name as string,
|
name: oauthUser.name as string,
|
||||||
email: oauthUser.email as string,
|
email: oauthUser.email as string,
|
||||||
avatar: oauthUser.avatar_url as string,
|
avatar: oauthUser.avatar_url as string,
|
||||||
githubId: oauthUser.id as number,
|
githubId: oauthUser.id as number,
|
||||||
githubToken: tokens.access_token as string,
|
githubToken: tokens.access_token as string,
|
||||||
language: 'en-US',
|
language: 'en-EN',
|
||||||
location: 'unknown',
|
location: 'unknown',
|
||||||
private: false,
|
private: false,
|
||||||
subscription: 'free',
|
subscription: 'free',
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ export default oauthGoogleEventHandler({
|
|||||||
|
|
||||||
// If the user is not signed in and no user exists with that Google ID or email address, create a new user
|
// If the user is not signed in and no user exists with that Google ID or email address, create a new user
|
||||||
const createdUser = await createUser({
|
const createdUser = await createUser({
|
||||||
username: oauthUser.name as string,
|
username: oauthUser.name.toLowerCase() as string,
|
||||||
description: '',
|
description: '',
|
||||||
name: `${oauthUser.given_name} ${oauthUser.family_name}`,
|
name: `${oauthUser.given_name} ${oauthUser.family_name}`,
|
||||||
email: oauthUser.email as string,
|
email: oauthUser.email as string,
|
||||||
avatar: oauthUser.picture as string,
|
avatar: oauthUser.picture as string,
|
||||||
googleId: oauthUser.sub as number,
|
googleId: oauthUser.sub as number,
|
||||||
googleToken: tokens.access_token as string,
|
googleToken: tokens.access_token as string,
|
||||||
language: 'en-US',
|
language: 'en-EN',
|
||||||
location: 'unknown',
|
location: 'unknown',
|
||||||
private: false,
|
private: false,
|
||||||
subscription: 'free',
|
subscription: 'free',
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export function useDrizzle() {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
return drizzle(postgres(config.postgres.url, { prepare: false, max: 50 }), { schema })
|
return drizzle(postgres(config.postgres.url, { prepare: false, max: 50 }), { schema })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserInsert = typeof schema.users.$inferInsert
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ export async function createUser(user: UserInsert) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUser(userId: number, user: Partial<UserInsert>) {
|
export async function updateUser(userId: number, user: Partial<UserInsert>) {
|
||||||
return useDrizzle()
|
await useDrizzle()
|
||||||
.update(tables.users)
|
.update(tables.users)
|
||||||
.set(user)
|
.set(user)
|
||||||
.where(eq(tables.users.id, userId))
|
.where(eq(tables.users.id, userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProfilePicture(avatar: string) {
|
export async function deleteProfilePicture(avatar: string) {
|
||||||
if (avatar.startsWith('profile-pictures/')) {
|
if (avatar.startsWith('avatars/')) {
|
||||||
await hubBlob().delete(avatar)
|
await hubBlob().del(avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
types/auth.d.ts
vendored
13
types/auth.d.ts
vendored
@@ -18,18 +18,7 @@ declare module '#auth-utils' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UserSession {
|
interface UserSession {
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
avatar: string | null
|
|
||||||
githubId?: number | null
|
|
||||||
googleId?: string | null
|
|
||||||
description: string
|
|
||||||
private: boolean
|
|
||||||
language: string
|
|
||||||
location: string
|
|
||||||
subscription: Subscription
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const Subscription = ['free', 'paid'] as const
|
|||||||
|
|
||||||
// Category
|
// Category
|
||||||
export const CreateCategorySchema = z.object({
|
export const CreateCategorySchema = z.object({
|
||||||
name: z.string().min(4),
|
name: z.string().min(4).max(20),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
color: z.enum(COLORS).default('gray'),
|
color: z.enum(COLORS).default('gray'),
|
||||||
nameVisible: z.boolean().optional().default(false),
|
nameVisible: z.boolean().optional().default(false),
|
||||||
@@ -14,7 +14,7 @@ export const CreateCategorySchema = z.object({
|
|||||||
export const CreateCategorySchemaType = z.infer<typeof CreateCategorySchema>
|
export const CreateCategorySchemaType = z.infer<typeof CreateCategorySchema>
|
||||||
|
|
||||||
export const UpdateCategorySchema = z.object({
|
export const UpdateCategorySchema = z.object({
|
||||||
name: z.string().min(4).optional(),
|
name: z.string().min(4).max(20).optional(),
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
nameVisible: z.boolean().optional().default(false),
|
nameVisible: z.boolean().optional().default(false),
|
||||||
@@ -31,20 +31,20 @@ export interface CategoryType {
|
|||||||
|
|
||||||
// Tab
|
// Tab
|
||||||
export const CreateTabSchema = z.object({
|
export const CreateTabSchema = z.object({
|
||||||
name: z.string().min(4),
|
name: z.string().min(4).max(20),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
color: z.enum(COLORS).default('gray'),
|
color: z.enum(COLORS).default('gray'),
|
||||||
primary: z.boolean().optional().default(false),
|
primary: z.boolean().default(false),
|
||||||
link: z.string(),
|
link: z.string(),
|
||||||
categoryId: z.number(),
|
categoryId: z.number(),
|
||||||
})
|
})
|
||||||
export const CreateTabSchemaType = z.infer<typeof CreateTabSchema>
|
export const CreateTabSchemaType = z.infer<typeof CreateTabSchema>
|
||||||
|
|
||||||
export const UpdateTabSchema = z.object({
|
export const UpdateTabSchema = z.object({
|
||||||
name: z.string().min(4).optional(),
|
name: z.string().min(4).max(20).optional(),
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
color: z.enum(COLORS).default('gray').optional(),
|
color: z.enum(COLORS).default('gray').optional(),
|
||||||
primary: z.boolean().optional().default(false),
|
primary: z.boolean().optional(),
|
||||||
link: z.string().optional(),
|
link: z.string().optional(),
|
||||||
categoryId: z.number(),
|
categoryId: z.number(),
|
||||||
})
|
})
|
||||||
@@ -60,6 +60,17 @@ export interface TabType {
|
|||||||
link: string
|
link: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
export const UpdateUserSchema = z.object({
|
||||||
|
name: z.string().min(3).max(25).optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
location: z.string().optional(),
|
||||||
|
language: z.string().optional(),
|
||||||
|
private: z.boolean().optional().default(false),
|
||||||
|
})
|
||||||
|
export const UpdateUserSchemaType = z.infer<typeof UpdateUserSchema>
|
||||||
|
|
||||||
export interface OpenWeatherType {
|
export interface OpenWeatherType {
|
||||||
weather: Array<{
|
weather: Array<{
|
||||||
description: string
|
description: string
|
||||||
@@ -78,3 +89,18 @@ export interface WeatherType {
|
|||||||
}
|
}
|
||||||
temp: number
|
temp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const locales = [
|
||||||
|
{
|
||||||
|
locale: 'en-EN',
|
||||||
|
label: 'English',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locale: 'es-ES',
|
||||||
|
label: 'Spanish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locale: 'fr-FR',
|
||||||
|
label: 'French',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user