mirror of
https://github.com/ArthurDanjou/arthome.git
synced 2026-01-14 12:14:33 +01:00
working
This commit is contained in:
@@ -6,18 +6,21 @@ defineProps<{
|
|||||||
dropdownItems: { label: string, icon: string, color: string, click: (category: CategoryType) => void }[]
|
dropdownItems: { label: string, icon: string, color: string, click: (category: CategoryType) => void }[]
|
||||||
}>()
|
}>()
|
||||||
defineEmits(['createTab'])
|
defineEmits(['createTab'])
|
||||||
|
|
||||||
|
const { canCreateTabInCategory } = await useUserLimit()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="category" class="flex items-center mb-4" :class="category.nameVisible ? 'justify-between' : 'justify-end'">
|
<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`">
|
<div v-if="category.nameVisible" class="flex items-center gap-2 mb-4" :class="`text-${category.color}-500`">
|
||||||
<UIcon :name="`i-ph:${category.icon}`" size="28" />
|
<UIcon :name="category.icon" size="28" />
|
||||||
<h1 class="font-bold text-2xl">
|
<h1 class="font-bold text-2xl">
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<UButton
|
<UButton
|
||||||
|
v-if="canCreateTabInCategory(category.id)"
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
label="New tab"
|
label="New tab"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const { user, loggedIn, clear } = useUserSession()
|
const { user, loggedIn, clear } = useUserSession()
|
||||||
|
|
||||||
const isSettingsOpen = ref(false)
|
const isSettingsOpen = ref(false)
|
||||||
|
|
||||||
|
const isDark = computed(() => colorMode.preference === 'dark')
|
||||||
const items = [
|
const items = [
|
||||||
[{
|
[{
|
||||||
slot: 'account',
|
slot: 'account',
|
||||||
@@ -13,26 +13,28 @@ const items = [
|
|||||||
label: 'Home',
|
label: 'Home',
|
||||||
icon: 'i-ph:house-line-duotone',
|
icon: 'i-ph:house-line-duotone',
|
||||||
action: () => navigateTo('/'),
|
action: () => navigateTo('/'),
|
||||||
|
shortcut: 'H',
|
||||||
}, {
|
}, {
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
icon: 'i-ph:gear-six-duotone',
|
icon: 'i-ph:gear-six-duotone',
|
||||||
action: () => {
|
action: () => isSettingsOpen.value = true,
|
||||||
isSettingsOpen.value = true
|
shortcut: 'S',
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
label: 'Profile',
|
label: isDark.value ? 'Light mode' : 'Dark mode',
|
||||||
icon: 'i-ph:person-arms-spread-duotone',
|
icon: isDark.value ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone',
|
||||||
action: () => navigateTo('/profile'),
|
action: () => toggleColorMode(),
|
||||||
|
shortcut: 'T',
|
||||||
}],
|
}],
|
||||||
[{
|
[{
|
||||||
slot: 'logout',
|
slot: 'logout',
|
||||||
label: 'Sign out',
|
label: 'Sign out',
|
||||||
icon: 'i-ph:sign-out-bold',
|
icon: 'i-ph:sign-out-bold',
|
||||||
|
shortcut: 'L',
|
||||||
}],
|
}],
|
||||||
]
|
]
|
||||||
|
|
||||||
function toggleColorMode() {
|
function toggleColorMode() {
|
||||||
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
|
colorMode.preference = isDark.value ? 'light' : 'dark'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
@@ -43,6 +45,8 @@ async function logout() {
|
|||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
t: () => toggleColorMode(),
|
t: () => toggleColorMode(),
|
||||||
|
s: () => isSettingsOpen.value = !isSettingsOpen.value,
|
||||||
|
l: async () => await logout(),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -58,7 +62,13 @@ defineShortcuts({
|
|||||||
</h1>
|
</h1>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<UDropdown v-if="loggedIn" :items="items" mode="hover" :ui="{ item: { disabled: 'cursor-text select-text' } }" :popper="{ placement: 'bottom-start' }">
|
<UDropdown
|
||||||
|
v-if="loggedIn"
|
||||||
|
:items="items"
|
||||||
|
mode="hover"
|
||||||
|
:ui="{ item: { disabled: 'cursor-text select-text' } }"
|
||||||
|
:popper="{ placement: 'bottom-end' }"
|
||||||
|
>
|
||||||
<UAvatar :src="user.avatar" />
|
<UAvatar :src="user.avatar" />
|
||||||
|
|
||||||
<template #account>
|
<template #account>
|
||||||
@@ -74,26 +84,28 @@ defineShortcuts({
|
|||||||
|
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div class="w-full flex justify-between items-center" @click.prevent="item.action()">
|
<div class="w-full flex justify-between items-center" @click.prevent="item.action()">
|
||||||
<span class="truncate">{{ item.label }}</span>
|
<div class="gap-2 flex items-center">
|
||||||
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
|
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
|
||||||
|
<span class="truncate">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<UKbd v-if="item.shortcut">
|
||||||
|
{{ item.shortcut }}
|
||||||
|
</UKbd>
|
||||||
</div>
|
</div>
|
||||||
</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()">
|
||||||
<span class="truncate">{{ item.label }}</span>
|
<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 ms-auto" />
|
<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>
|
||||||
|
</div>
|
||||||
|
<UKbd v-if="item.shortcut">
|
||||||
|
{{ item.shortcut }}
|
||||||
|
</UKbd>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDropdown>
|
</UDropdown>
|
||||||
<UButton
|
|
||||||
:icon="$colorMode.preference === 'dark' ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone'"
|
|
||||||
color="gray"
|
|
||||||
square
|
|
||||||
size="md"
|
|
||||||
variant="ghost"
|
|
||||||
@click="toggleColorMode"
|
|
||||||
/>
|
|
||||||
</clientonly>
|
</clientonly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,10 +123,12 @@ defineShortcuts({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
Hey
|
<div class="space-y-12">
|
||||||
Delete account
|
<div>
|
||||||
Change user details
|
Delete account
|
||||||
{{ user }}
|
Change user details
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ defineProps<{
|
|||||||
tab: TabType
|
tab: TabType
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { setTabPrimary } = await useTabs()
|
||||||
|
|
||||||
// DropDown Items
|
// DropDown Items
|
||||||
const items = [[
|
const items = [[
|
||||||
|
{
|
||||||
|
label: 'Toggle favorite',
|
||||||
|
icon: 'i-ph:star-duotone',
|
||||||
|
color: 'yellow',
|
||||||
|
click: tab => setTabPrimary(tab, !tab.primary),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: 'i-ph:pencil-duotone',
|
icon: 'i-ph:pencil-duotone',
|
||||||
@@ -56,12 +64,12 @@ function openDeleteTabModal(tab: TabType) {
|
|||||||
<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">
|
||||||
<UIcon :name="`i-ph:${tab.icon}`" size="32" />
|
<UIcon :name="tab.icon" size="32" />
|
||||||
</UBadge>
|
</UBadge>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div :class="`text-${tab.color}-400`" class="text-xl font-medium">
|
<p :class="`text-${tab.color}-400`" class="text-xl font-medium truncate">
|
||||||
<p>{{ tab.name }}</p>
|
{{ tab.name }}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UDropdown
|
<UDropdown
|
||||||
@@ -71,8 +79,10 @@ function openDeleteTabModal(tab: TabType) {
|
|||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="soft"
|
variant="ghost"
|
||||||
icon="i-ph:dots-three-outline-vertical-duotone"
|
:padded="false"
|
||||||
|
size="sm"
|
||||||
|
icon="i-ph:dots-three-outline-duotone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
|
|||||||
state.icon = undefined
|
state.icon = undefined
|
||||||
state.name = undefined
|
state.name = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { loading, search } = useIcons()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,11 +44,35 @@ async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
|
|||||||
<template #default>
|
<template #default>
|
||||||
<UForm :schema="CreateCategorySchema" :state="state" class="space-y-4" @submit="handleCreate">
|
<UForm :schema="CreateCategorySchema" :state="state" class="space-y-4" @submit="handleCreate">
|
||||||
<UFormGroup label="Name" name="name">
|
<UFormGroup label="Name" name="name">
|
||||||
<UInput v-model="state.name" type="text" variant="outline" />
|
<UInput v-model="state.name" placeholder="Enter name" type="text" variant="outline" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Icon " name="icon" help="Get icon from the Phosphor Collection">
|
<UFormGroup label="Icon " name="icon">
|
||||||
<UInput v-model="state.icon" type="text" variant="outline" />
|
<USelectMenu
|
||||||
|
v-model="state.icon"
|
||||||
|
:loading="loading"
|
||||||
|
:searchable="search"
|
||||||
|
placeholder="Select an icon"
|
||||||
|
searchable-placeholder="Search an icon"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div v-if="state.icon" class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${state.icon}`" />
|
||||||
|
<span class="truncate">{{ state.icon }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${option}`" />
|
||||||
|
<span class="truncate">{{ option }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Enter an icon name, keyword or tag
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Color " name="color">
|
<UFormGroup label="Color " name="color">
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
|
|||||||
state.primary = false
|
state.primary = false
|
||||||
state.categoryId = props.category?.id
|
state.categoryId = props.category?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { loading, search } = useIcons()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -59,7 +61,31 @@ async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
|
|||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Icon " name="icon">
|
<UFormGroup label="Icon " name="icon">
|
||||||
<UInput v-model="state.icon" type="text" />
|
<USelectMenu
|
||||||
|
v-model="state.icon"
|
||||||
|
:loading="loading"
|
||||||
|
:searchable="search"
|
||||||
|
placeholder="Select an icon"
|
||||||
|
searchable-placeholder="Search an icon"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div v-if="state.icon" class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${state.icon}`" />
|
||||||
|
<span class="truncate">{{ state.icon }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${option}`" />
|
||||||
|
<span class="truncate">{{ option }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Enter an icon name, keyword or tag
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Color " name="color">
|
<UFormGroup label="Color " name="color">
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { updateCategory } = await useCategories()
|
const { updateCategory } = await useCategories()
|
||||||
|
const { loading, search } = useIcons()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
name: props.category?.name,
|
name: undefined,
|
||||||
icon: props.category?.icon,
|
icon: undefined,
|
||||||
color: props.category?.color,
|
color: COLORS[0],
|
||||||
nameVisible: props.category?.nameVisible,
|
nameVisible: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -60,9 +61,32 @@ async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchema>) {
|
|||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Icon " name="icon">
|
<UFormGroup label="Icon " name="icon">
|
||||||
<UInput v-model="state.icon" type="text" />
|
<USelectMenu
|
||||||
</UFormGroup>
|
v-model="state.icon"
|
||||||
|
:loading="loading"
|
||||||
|
:searchable="search"
|
||||||
|
placeholder="Select an icon"
|
||||||
|
searchable-placeholder="Search an icon"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div v-if="state.icon" class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${state.icon}`" />
|
||||||
|
<span class="truncate">{{ state.icon }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${option}`" />
|
||||||
|
<span class="truncate">{{ option }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Enter an icon name, keyword or tag
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</UFormGroup>
|
||||||
<UFormGroup>
|
<UFormGroup>
|
||||||
<UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" />
|
<UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormSubmitEvent } from '#ui/types'
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
import type { COLORS, TabType, UpdateTabSchemaType } from '~~/types/types'
|
import type { TabType, UpdateTabSchemaType } from '~~/types/types'
|
||||||
import { UpdateTabSchema } from '~~/types/types'
|
import { COLORS, UpdateTabSchema } from '~~/types/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tab: TabType | null
|
tab: TabType | null
|
||||||
@@ -10,13 +10,14 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits(['closeModal'])
|
const emit = defineEmits(['closeModal'])
|
||||||
const { categories } = await useCategories()
|
const { categories } = await useCategories()
|
||||||
const { updateTab } = await useTabs()
|
const { updateTab } = await useTabs()
|
||||||
|
const { loading, search } = useIcons()
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
name: props.tab?.name,
|
name: undefined,
|
||||||
icon: props.tab?.icon,
|
icon: undefined,
|
||||||
color: props.tab?.color,
|
color: COLORS[0],
|
||||||
primary: props.tab?.primary,
|
primary: undefined,
|
||||||
categoryId: props.tab?.categoryId,
|
categoryId: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -65,7 +66,31 @@ async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
|
|||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Icon " name="icon">
|
<UFormGroup label="Icon " name="icon">
|
||||||
<UInput v-model="state.icon" type="text" />
|
<USelectMenu
|
||||||
|
v-model="state.icon"
|
||||||
|
:loading="loading"
|
||||||
|
:searchable="search"
|
||||||
|
placeholder="Select an icon"
|
||||||
|
searchable-placeholder="Search an icon"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div v-if="state.icon" class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${state.icon}`" />
|
||||||
|
<span class="truncate">{{ state.icon }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon size="20" :name="`i-${option}`" />
|
||||||
|
<span class="truncate">{{ option }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Enter an icon name, keyword or tag
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Category " name="category">
|
<UFormGroup label="Category " name="category">
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { AppType } from '~~/types/types'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
title: string
|
|
||||||
apps: AppType[]
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section>
|
|
||||||
<h1 class="font-bold text-xl mb-4">
|
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
<div v-if="apps" class="grid grid-cols-1 auto-rows-auto sm:grid-cols-3 md:grid-cols-5 gap-4">
|
|
||||||
<ULink v-for="app in apps" :key="app.name" :to="app.url" class="relative" target="_blank">
|
|
||||||
<div v-show="app.primary === true" class="absolute flex h-4 w-4 -right-2 -top-2">
|
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
||||||
<span class="relative inline-flex rounded-full h-4 w-4 bg-green-500" />
|
|
||||||
</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 gap-4 items-center h-full">
|
|
||||||
<UBadge :color="app.color" class="p-2" variant="soft">
|
|
||||||
<UIcon :name="app.icon" size="32" />
|
|
||||||
</UBadge>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div v-if="app.nuxt" class="text-xl flex gap-1 items-center">
|
|
||||||
<div class="flex">
|
|
||||||
<p>{{ app.name }}</p>
|
|
||||||
<p :class="`text-${app.color}-400 font-medium`">
|
|
||||||
{{ app.nuxt }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else :class="`text-${app.color}-400`" class="text-xl font-medium">
|
|
||||||
<p>{{ app.name }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 mt-1">
|
|
||||||
<UBadge
|
|
||||||
v-for="tag in app.tags"
|
|
||||||
:key="tag.name"
|
|
||||||
:color="tag.color"
|
|
||||||
:label="tag.name"
|
|
||||||
variant="soft"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</ULink>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
@@ -4,7 +4,7 @@ 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) {
|
async function getCategory(id: number): CategoryType {
|
||||||
return categories.data.value.find(category => category.id === id)
|
return categories.data.value.find(category => category.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
app/composables/icons.ts
Normal file
18
app/composables/icons.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export function useIcons() {
|
||||||
|
const loading = ref(false)
|
||||||
|
async function search(query: string) {
|
||||||
|
if (query) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
const response = await $fetch('/api/icons/search', {
|
||||||
|
query: { query },
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
return response.icons
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
search,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CreateTabSchema, TabType, UpdateTabSchema } from '~~/types/types'
|
import type { CreateTabSchema, type TabType, UpdateTabSchema } from '~~/types/types'
|
||||||
|
|
||||||
export async function useTabs() {
|
export async function useTabs() {
|
||||||
const { data: tabs, refresh }
|
const { data: tabs, refresh }
|
||||||
@@ -32,6 +32,21 @@ export async function useTabs() {
|
|||||||
.catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
|
.catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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!')
|
||||||
|
})
|
||||||
|
.catch(error => useErrorToast('Cannot toggle Tab favorite!', `Error: ${error}`))
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTab(id: number) {
|
async function deleteTab(id: number) {
|
||||||
await $fetch(`/api/tabs/${id}`, {
|
await $fetch(`/api/tabs/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -47,5 +62,6 @@ export async function useTabs() {
|
|||||||
deleteTab,
|
deleteTab,
|
||||||
getTabsForCategory,
|
getTabsForCategory,
|
||||||
updateTab,
|
updateTab,
|
||||||
|
setTabPrimary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
export function useUserLimit() {
|
export async function useUserLimit() {
|
||||||
function hasUserFreePlan() {
|
const { user } = useUserSession()
|
||||||
return true
|
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 getRemainingCategories() {
|
function canCreateTabInCategory(categoryId: number): boolean {
|
||||||
if (!hasUserFreePlan())
|
if (hasPaidPlan.value)
|
||||||
return -1
|
return true
|
||||||
return 3
|
return tabs.filter(tab => tab.categoryId === categoryId).length < 5
|
||||||
}
|
|
||||||
|
|
||||||
function getRemainingTabs(category_id: number) {
|
|
||||||
if (!hasUserFreePlan())
|
|
||||||
return -1
|
|
||||||
return category_id * 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getRemainingCategories,
|
hasPaidPlan,
|
||||||
getRemainingTabs,
|
userLimits,
|
||||||
|
canCreateCategory,
|
||||||
|
canCreateTabInCategory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CategoryType } from '~~/types/types'
|
import type { CategoryType } from '~~/types/types'
|
||||||
import CategoryHeader from '~/components/CategoryHeader.vue'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
@@ -14,6 +13,7 @@ onMounted(() => {
|
|||||||
const { user } = useUserSession()
|
const { user } = useUserSession()
|
||||||
const { categories } = await useCategories()
|
const { categories } = await useCategories()
|
||||||
const { getTabsForCategory } = await useTabs()
|
const { getTabsForCategory } = await useTabs()
|
||||||
|
const { canCreateCategory } = await useUserLimit()
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const createCategoryModal = ref(false)
|
const createCategoryModal = ref(false)
|
||||||
@@ -59,7 +59,11 @@ const items = [[
|
|||||||
]]
|
]]
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
c: () => createCategoryModal.value = true,
|
c: () => {
|
||||||
|
if (canCreateCategory()) {
|
||||||
|
createCategoryModal.value = true
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -77,6 +81,7 @@ defineShortcuts({
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mb-8 gap-4">
|
<div class="flex justify-end mb-8 gap-4">
|
||||||
<UButton
|
<UButton
|
||||||
|
v-if="canCreateCategory()"
|
||||||
icon="i-ph:folder-simple-plus-duotone"
|
icon="i-ph:folder-simple-plus-duotone"
|
||||||
color="black"
|
color="black"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -86,6 +91,19 @@ defineShortcuts({
|
|||||||
Create Category
|
Create Category
|
||||||
<UKbd>C</UKbd>
|
<UKbd>C</UKbd>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UTooltip v-else text="You can't create more categories on free plan. ❌">
|
||||||
|
<UButton
|
||||||
|
icon="i-ph:folder-simple-plus-duotone"
|
||||||
|
color="black"
|
||||||
|
variant="solid"
|
||||||
|
size="lg"
|
||||||
|
disabled
|
||||||
|
@click.prevent="createCategoryModal = true"
|
||||||
|
>
|
||||||
|
Create Category
|
||||||
|
<UKbd>C</UKbd>
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
<section v-if="categories">
|
<section v-if="categories">
|
||||||
<div v-if="categories.length > 0" class="space-y-12">
|
<div v-if="categories.length > 0" class="space-y-12">
|
||||||
@@ -93,12 +111,12 @@ defineShortcuts({
|
|||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
>
|
>
|
||||||
<CategoryHeader
|
<AppCategory
|
||||||
:dropdown-items="items"
|
:dropdown-items="items"
|
||||||
: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-5 gap-4">
|
<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">
|
||||||
<AppTab
|
<AppTab
|
||||||
v-for="tab in getTabsForCategory(category.id)"
|
v-for="tab in getTabsForCategory(category.id)"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const { user, loggedIn, clear } = useUserSession()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
User: {{ user }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
LoggedIn: {{ loggedIn }}
|
|
||||||
</div>
|
|
||||||
<div @click="clear">
|
|
||||||
clear
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
16
server/api/icons/search.get.ts
Normal file
16
server/api/icons/search.get.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const collections = ['ph', 'heroicons']
|
||||||
|
const { query } = getQuery(event)
|
||||||
|
|
||||||
|
const response = await $fetch('https://api.iconify.design/search', {
|
||||||
|
params: {
|
||||||
|
query,
|
||||||
|
prefixes: collections.join(','),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: response.total,
|
||||||
|
icons: response.icons && response.icons.length > 0 ? response.icons.slice(0, 25) : response.icons,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
name: body.name,
|
name: body.name,
|
||||||
icon: body.icon,
|
icon: body.icon,
|
||||||
color: body.color,
|
color: body.color,
|
||||||
nameVisible: body.nameVisible,
|
primary: body.primary,
|
||||||
link: body.link,
|
link: body.link,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ export const tables = schema
|
|||||||
|
|
||||||
export function useDrizzle() {
|
export function useDrizzle() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
return drizzle(postgres(config.postgres.url, { prepare: false }), { schema })
|
return drizzle(postgres(config.postgres.url, { prepare: false, max: 50 }), { schema })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user