@@ -95,7 +96,7 @@ defineShortcuts({
-
+
{{ item.label }}
@@ -123,13 +124,23 @@ defineShortcuts({
-
+
- Delete account
- Change user details
+
+
+
+
+ Delete account
+
+
diff --git a/app/components/App/Tab.vue b/app/components/App/Tab.vue
index 77f72c3..d8ad2e3 100644
--- a/app/components/App/Tab.vue
+++ b/app/components/App/Tab.vue
@@ -1,8 +1,9 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- {{ item.label }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/App/UserSettingsForm.vue b/app/components/App/UserSettingsForm.vue
new file mode 100644
index 0000000..76abe43
--- /dev/null
+++ b/app/components/App/UserSettingsForm.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/Modal/CreateCategory.vue b/app/components/Modal/CreateCategory.vue
index ce72ba0..77543ca 100644
--- a/app/components/Modal/CreateCategory.vue
+++ b/app/components/Modal/CreateCategory.vue
@@ -5,6 +5,7 @@ import { COLORS, CreateCategorySchema } from '~~/types/types'
const emit = defineEmits(['closeModal'])
const { createCategory } = await useCategories()
+const { refreshUserLimits, canCreateCategory } = await useUserLimits()
const state = reactive({
name: undefined,
icon: undefined,
@@ -14,6 +15,12 @@ const state = reactive({
async function handleCreate(event: FormSubmitEvent
) {
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')
state.color = COLORS[0]
state.nameVisible = true
@@ -34,7 +41,7 @@ const { loading, search } = useIcons()
{
})
async function handleCreate(event: FormSubmitEvent) {
- 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')
state.name = undefined
state.icon = undefined
@@ -47,7 +57,7 @@ const { loading, search } = useIcons()
+
+
+
+
+
+ avatar
+ Warning: This will permanently delete your account, all your categories, and all your tabs.
+ ---
+ To verify, type confirm delete account below
+ input
+ confirm
+
+
+
+
diff --git a/app/components/Modal/DeleteCategory.vue b/app/components/Modal/DeleteCategory.vue
index fbeb764..7df38ff 100644
--- a/app/components/Modal/DeleteCategory.vue
+++ b/app/components/Modal/DeleteCategory.vue
@@ -7,9 +7,11 @@ const props = defineProps<{
const emit = defineEmits(['closeModal'])
const { deleteCategory } = await useCategories()
+const { refreshUserLimits } = await useUserLimits()
async function handleDelete() {
await deleteCategory(props.category.id)
+ await refreshUserLimits()
emit('closeModal')
}
@@ -28,7 +30,7 @@ defineShortcuts({
-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'
const props = defineProps<{
@@ -24,7 +25,7 @@ watchEffect(() => {
state.nameVisible = props.category?.nameVisible
})
-async function handleUpdate(event: FormSubmitEvent) {
+async function handleUpdate(event: FormSubmitEvent) {
await updateCategory({
id: props.category!.id,
...event.data,
@@ -43,7 +44,7 @@ async function handleUpdate(event: FormSubmitEvent) {
{
@@ -26,6 +27,7 @@ watchEffect(() => {
state.color = props.tab?.color
state.primary = props.tab?.primary
state.categoryId = props.tab?.categoryId
+ state.link = props.tab?.link
})
async function handleUpdate(event: FormSubmitEvent) {
@@ -44,11 +46,11 @@ async function handleUpdate(event: FormSubmitEvent) {
- Update category '{{ tab.name }}'
+ Update tab '{{ tab.name }}'
) {
+
+
+
+
diff --git a/app/composables/categories.ts b/app/composables/categories.ts
index 706c124..38a6383 100644
--- a/app/composables/categories.ts
+++ b/app/composables/categories.ts
@@ -4,42 +4,49 @@ export async function useCategories() {
const { data: categories, refresh }
= await useAsyncData(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) {
- await $fetch('/api/categories', {
- method: 'POST',
- body: JSON.stringify(category),
- })
- .catch(error => useErrorToast('Category creation failed!', `Error: ${error}`))
- await refresh()
- await useSuccessToast('Category successfully created!')
+ try {
+ await useRequestFetch()('/api/categories', {
+ method: 'POST',
+ body: JSON.stringify(category),
+ })
+ await refresh()
+ await useSuccessToast('Category successfully created!', category.color)
+ }
+ catch (error) {
+ useErrorToast('Category creation failed!', error as string)
+ }
}
async function updateCategory(category: UpdateCategorySchema & { id: number }) {
- await $fetch(`/api/categories/${category.id}`, {
- method: 'PUT',
- body: JSON.stringify(category),
- })
- .catch(error => useErrorToast('Category update failed!', `Error: ${error}`))
- await refresh()
- await useSuccessToast('Category successfully updated!')
+ try {
+ await $fetch(`/api/categories/${category.id}`, {
+ method: 'PUT',
+ body: JSON.stringify(category),
+ })
+ await refresh()
+ await useSuccessToast('Category successfully updated!')
+ }
+ catch (error) {
+ useErrorToast('Category update failed!', error as string)
+ }
}
async function deleteCategory(id: number) {
- await $fetch(`/api/categories/${id}`, {
- method: 'DELETE',
- })
- .catch(error => useErrorToast('Category deletion failed!', `Error: ${error}`))
- await refresh()
- await useSuccessToast('Category successfully deleted!')
+ try {
+ await $fetch(`/api/categories/${id}`, {
+ method: 'DELETE',
+ })
+ await refresh()
+ await useSuccessToast('Category successfully deleted!')
+ }
+ catch (error) {
+ useErrorToast('Category deletion failed!', error as string)
+ }
}
return {
categories,
- getCategory,
createCategory,
updateCategory,
deleteCategory,
diff --git a/app/composables/tabs.ts b/app/composables/tabs.ts
index 354dce9..dba8531 100644
--- a/app/composables/tabs.ts
+++ b/app/composables/tabs.ts
@@ -9,51 +9,61 @@ export async function useTabs() {
}
async function createTab(tab: CreateTabSchema) {
- await $fetch('/api/tabs', {
- method: 'POST',
- body: JSON.stringify(tab),
- })
- .then(async () => {
- await refresh()
- useSuccessToast('Tab successfully created!')
+ try {
+ await $fetch('/api/tabs', {
+ method: 'POST',
+ body: JSON.stringify(tab),
})
- .catch(error => useErrorToast('Tab creation failed!', `Error: ${error}`))
+ await refresh()
+ useSuccessToast('Tab successfully created!', tab.color)
+ }
+ catch (error) {
+ useErrorToast('Tab creation failed!', error as string)
+ }
}
async function updateTab(tab: UpdateTabSchema) {
- await $fetch(`/api/tabs/${tab.id}`, {
- method: 'PUT',
- body: JSON.stringify(tab),
- })
- .then(async () => {
- await refresh()
- useSuccessToast('Tab successfully updated!')
+ try {
+ await $fetch(`/api/tabs/${tab.id}`, {
+ method: 'PUT',
+ body: JSON.stringify(tab),
})
- .catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
+ await refresh()
+ useSuccessToast('Tab successfully updated!')
+ }
+ catch (error) {
+ useErrorToast('Tab update failed!', error as string)
+ }
}
async function setTabPrimary(tab, primary: boolean) {
- await $fetch(`/api/tabs/${tab.id}`, {
- method: 'PUT',
- body: JSON.stringify({
- primary,
- categoryId: tab.categoryId,
- }),
- })
- .then(async () => {
- await refresh()
- useSuccessToast('Tab favorite toggled with success!')
+ try {
+ await $fetch(`/api/tabs/${tab.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ primary,
+ categoryId: tab.categoryId,
+ }),
})
- .catch(error => useErrorToast('Cannot toggle Tab favorite!', `Error: ${error}`))
+ await refresh()
+ useSuccessToast(`Tab ${tab.name} ${primary ? 'set as favorite' : 'unset as favorite'}!`, 'yellow')
+ }
+ catch (error) {
+ useErrorToast('Cannot toggle favorite state for tab!', error as string)
+ }
}
async function deleteTab(id: number) {
- await $fetch(`/api/tabs/${id}`, {
- method: 'DELETE',
- })
- .catch(error => useErrorToast('Tab deletion failed!', `Error: ${error}`))
- await refresh()
- useSuccessToast('Tab successfully deleted!')
+ try {
+ await $fetch(`/api/tabs/${id}`, {
+ method: 'DELETE',
+ })
+ await refresh()
+ useSuccessToast('Tab successfully deleted!')
+ }
+ catch (error) {
+ useErrorToast('Tab deletion failed!', error as string)
+ }
}
return {
diff --git a/app/composables/toasts.ts b/app/composables/toasts.ts
index 64a9006..b82c4d1 100644
--- a/app/composables/toasts.ts
+++ b/app/composables/toasts.ts
@@ -1,10 +1,10 @@
-export function useSuccessToast(title: string, description?: string) {
+export function useSuccessToast(title: string, color?: string, description?: string) {
const toast = useToast()
toast.add({
title,
description,
- color: 'green',
+ color: color || 'green',
icon: 'i-ph:check-circle-duotone',
})
}
diff --git a/app/composables/user-limit.ts b/app/composables/user-limit.ts
deleted file mode 100644
index 1c1e102..0000000
--- a/app/composables/user-limit.ts
+++ /dev/null
@@ -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,
- }
-}
diff --git a/app/composables/user-limits.ts b/app/composables/user-limits.ts
new file mode 100644
index 0000000..8d330e6
--- /dev/null
+++ b/app/composables/user-limits.ts
@@ -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,
+ }
+}
diff --git a/app/composables/users.ts b/app/composables/users.ts
new file mode 100644
index 0000000..90b07e2
--- /dev/null
+++ b/app/composables/users.ts
@@ -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,
+ }
+}
diff --git a/app/middleware/ghost.ts b/app/middleware/ghost.ts
index 4f93b4c..8cd688b 100644
--- a/app/middleware/ghost.ts
+++ b/app/middleware/ghost.ts
@@ -1,7 +1,7 @@
export default defineNuxtRouteMiddleware(async () => {
- const { loggedIn } = useUserSession()
+ const { loggedIn, user } = await useUserSession()
if (loggedIn.value) {
- return navigateTo('/')
+ return navigateTo(`/${user.value.username.toLowerCase()}`)
}
})
diff --git a/app/pages/[user].vue b/app/pages/[user].vue
index 3e4f198..cee0e73 100644
--- a/app/pages/[user].vue
+++ b/app/pages/[user].vue
@@ -1,13 +1,101 @@
-
- {{ router.currentRoute.value.params.user }}
-
+
+
+
+ {{ useDateFormat(date, 'HH') }}
+ :
+ {{ useDateFormat(date, 'mm') }}
+
+
+ {{ useDateFormat(date, 'dddd D MMMM YYYY', { locales: userDetails.locale ? userDetails.language : user.locale }) }}
+
+
+
+
+
+
+ {{ userDetails.message ? userDetails.message : 'This user\'s profile is private.' }}
+
+
+
+ Please look for another user.
+
+
+
+
+
+
+
+
+
+
+ {{ category.name }}
+
+
+
+
+ {{ userDetails.categories.filter(tab => tab.categoryId === category.id) }}
+
+
+
+
+ The category is empty.
+
+
+
+
+
+
+
+ This user doesn't have any categories.
+
+
+
+
-
-
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 355077c..ecd8efc 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,4 +1,4 @@
-
@@ -116,11 +142,15 @@ defineShortcuts({
:category="category"
@create-tab="openCreateTab(category)"
/>
-
+
diff --git a/app/pages/login.vue b/app/pages/login.vue
index 1729238..3f96858 100644
--- a/app/pages/login.vue
+++ b/app/pages/login.vue
@@ -1,29 +1,13 @@