Working on arthome

This commit is contained in:
2024-08-30 14:22:29 +02:00
parent a1e31a89a7
commit 396e8a6850
51 changed files with 2019 additions and 2290 deletions

View File

@@ -1,6 +1,23 @@
export default defineAppConfig({
ui: {
gray: 'neutral',
gray: 'zinc',
primary: 'gray',
notifications: {
position: 'bottom-0 right-0',
},
input: {
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',
},
},
},
select: {
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',
},
},
},
},
})

View File

@@ -4,58 +4,22 @@ useHead({
title: 'ArtHome by Arthur Danjou',
})
const { loggedIn, clear, user } = useUserSession()
const colorMode = useColorMode()
const { loggedIn } = useUserSession()
watch(loggedIn, async () => {
if (!loggedIn.value) {
navigateTo('/login')
}
})
function toggleColorMode() {
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
}
async function logout() {
await clear()
navigateTo('/login')
window.location.reload()
}
defineShortcuts({
t: () => toggleColorMode(),
c: () => toggleColorMode(),
})
</script>
<template>
<div>
<NuxtLoadingIndicator color="#808080" />
<UContainer>
<ClientOnly>
<div class="absolute top-2 right-2 flex gap-2">
<UTooltip v-if="loggedIn" text="Déconnexion">
<UButton
:label="user.name"
color="gray"
square
trailing-icon="i-ph:person-arms-spread-duotone"
variant="ghost"
@click="logout"
/>
</UTooltip>
<UButton
:icon="$colorMode.preference === 'dark' ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone'"
color="gray"
square
variant="ghost"
@click="toggleColorMode"
/>
</div>
</ClientOnly>
<NuxtLayout>
<NuxtPage />
</UContainer>
</NuxtLayout>
<UNotifications />
</div>
</template>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
const colorMode = useColorMode()
const { user, loggedIn, clear } = useUserSession()
const isSettingsOpen = ref(false)
const items = [
[{
slot: 'account',
disabled: true,
}],
[{
label: 'Home',
icon: 'i-ph:house-line-duotone',
action: () => navigateTo('/'),
}, {
label: 'Settings',
icon: 'i-ph:gear-six-duotone',
action: () => {
console.log('Settings')
isSettingsOpen.value = true
},
}, {
label: 'Profile',
icon: 'i-ph:person-arms-spread-duotone',
action: () => navigateTo('/profile'),
}],
[{
slot: 'logout',
label: 'Sign out',
icon: 'i-ph:sign-out-bold',
}],
]
function toggleColorMode() {
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
}
async function logout() {
await clear()
navigateTo('/login')
window.location.reload()
}
defineShortcuts({
t: () => toggleColorMode(),
})
</script>
<template>
<div>
<header
class="fixed top-0 w-full py-4 z-50 bg-white/30 dark:bg-zinc-900/30 duration-300 backdrop-blur"
>
<UContainer>
<div class="flex justify-between w-full items-center">
<h1 class="tracking-wide text-lg font-bold text-black dark:text-white">
ArtHome
</h1>
<div class="flex items-center gap-2">
<ClientOnly>
<UDropdown v-if="loggedIn" :items="items" mode="hover" :ui="{ item: { disabled: 'cursor-text select-text' } }" :popper="{ placement: 'bottom-start' }">
<UAvatar :src="user.avatar" />
<template #account>
<div class="text-left">
<p>
Signed in as
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{ user.name }}
</p>
</div>
</template>
<template #item="{ item }">
<div class="w-full flex justify-between items-center" @click.prevent="item.action()">
<span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
</div>
</template>
<template #logout="{ item }">
<div class="w-full flex justify-between items-center" @click="logout()">
<span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto" />
</div>
</template>
</UDropdown>
<UButton
:icon="$colorMode.preference === 'dark' ? 'i-ph:moon-duotone' : 'i-ph:sun-duotone'"
color="gray"
square
size="md"
variant="ghost"
@click="toggleColorMode"
/>
</clientonly>
</div>
</div>
</UContainer>
</header>
<USlideover v-model="isSettingsOpen">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Settings
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isSettingsOpen = false" />
</div>
</template>
<template #default>
Hey
Delete account
Change user details
{{ user }}
</template>
</UCard>
</USlideover>
</div>
</template>

109
app/components/App/Tab.vue Normal file
View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import type { TabType } from '~~/types/types'
defineProps<{
tab: TabType
}>()
// DropDown Items
const items = [[
{
label: 'Edit',
icon: 'i-ph:pencil-duotone',
color: 'green',
click: tab => openUpdateTabModal(tab),
},
{
label: 'Delete',
icon: 'i-ph:trash-duotone',
color: 'red',
click: tab => openDeleteTabModal(tab),
},
]]
// Modals
const updateTabModal = ref(false)
const deleteTabModal = ref(false)
// Update Category
const currentUpdateTab = ref<TabType | null>(null)
function openUpdateTabModal(tab: TabType) {
currentUpdateTab.value = tab
updateTabModal.value = true
}
// Delete Category
const currentDeleteTab = ref<TabType | null>(null)
function openDeleteTabModal(tab: TabType) {
currentDeleteTab.value = tab
deleteTabModal.value = true
}
</script>
<template>
<ULink
:to="tab.link"
class="relative"
target="_blank"
>
<div v-show="tab.primary" class="absolute flex h-3 w-3 -left-1 -top-1">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="`bg-${tab.color}-400`" />
<span class="relative inline-flex rounded-full h-3 w-3" :class="`bg-${tab.color}-400`" />
</div>
<UCard
:ui="{ body: { base: 'h-full' }, background: 'h-full duration-300 bg-white hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800' }"
>
<div class="flex items-center justify-between h-full">
<div class="flex gap-4 items-center h-full">
<UBadge :color="tab.color" class="p-2" variant="soft">
<UIcon :name="`i-ph:${tab.icon}`" size="32" />
</UBadge>
<div class="flex flex-col gap-1">
<div :class="`text-${tab.color}-400`" class="text-xl font-medium">
<p>{{ tab.name }}</p>
</div>
</div>
</div>
<UDropdown
:items="items"
:popper="{ placement: 'bottom-end', arrow: true }"
:ui="{ container: 'z-50 group', width: 'w-40', shadow: 'shadow-2xl' }"
>
<UButton
color="gray"
variant="soft"
icon="i-ph:dots-three-outline-vertical-duotone"
/>
<template #item="{ item }">
<div class="w-full flex items-center justify-between" @click.prevent="item.click(tab)">
<span
class="truncate"
:class="`text-${item.color}-500`"
>
{{ item.label }}
</span>
<UIcon
:name="item.icon"
class="flex-shrink-0 h-4 w-4 ms-auto"
:class="`text-${item.color}-500`"
/>
</div>
</template>
</UDropdown>
</div>
</UCard>
<ModalUpdateTab
v-if="currentUpdateTab"
v-model="updateTabModal"
:tab="currentUpdateTab"
@close-modal="updateTabModal = false"
/>
<ModalDeleteTab
v-if="currentDeleteTab"
v-model="deleteTabModal"
:tab="currentDeleteTab"
@close-modal="deleteTabModal = false"
/>
</ULink>
</template>

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div>
<slot />
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { CategoryType } from '~~/types/types'
defineProps<{
category: CategoryType
dropdownItems: { label: string, icon: string, color: string, click: (category: CategoryType) => void }[]
}>()
defineEmits(['createTab'])
</script>
<template>
<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="`i-ph:${category.icon}`" size="28" />
<h1 class="font-bold text-2xl">
{{ category.name }}
</h1>
</div>
<div class="flex gap-4">
<UButton
color="gray"
variant="solid"
label="New tab"
icon="i-ph:plus-circle-duotone"
@click.prevent="$emit('createTab')"
/>
<UDropdown
:items="dropdownItems"
:popper="{ placement: 'bottom-end', arrow: true }"
:ui="{ width: 'w-40', shadow: 'shadow-xl' }"
>
<UButton
color="white"
variant="solid"
icon="i-ph:dots-three-outline-vertical-duotone"
/>
<template #item="{ item }">
<div class="w-full flex items-center justify-between" @click.prevent="item.click(category)">
<span
class="truncate"
:class="`text-${item.color}-500`"
>
{{ item.label }}
</span>
<UIcon
:name="item.icon"
class="flex-shrink-0 h-4 w-4 ms-auto"
:class="`text-${item.color}-500`"
/>
</div>
</template>
</UDropdown>
</div>
</div>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types'
import type { CreateCategorySchemaType } from '~~/types/types'
import { COLORS, CreateCategorySchema } from '~~/types/types'
const emit = defineEmits(['closeModal'])
const { createCategory } = await useCategories()
const state = reactive({
name: undefined,
icon: undefined,
color: COLORS[0],
nameVisible: true,
})
async function handleCreate(event: FormSubmitEvent<CreateCategorySchemaType>) {
await createCategory(event.data)
emit('closeModal')
state.color = COLORS[0]
state.nameVisible = true
state.icon = undefined
state.name = undefined
}
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Create a new category
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<UForm :schema="CreateCategorySchema" :state="state" class="space-y-4" @submit="handleCreate">
<UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" variant="outline" />
</UFormGroup>
<UFormGroup label="Icon " name="icon" help="Get icon from the Phosphor Collection">
<UInput v-model="state.icon" type="text" variant="outline" />
</UFormGroup>
<UFormGroup label="Color " name="color">
<USelect v-model="state.color" :options="COLORS" variant="outline" />
</UFormGroup>
<UFormGroup>
<UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" />
</UFormGroup>
<UButton
type="submit"
:color="state.color"
block
label="Create category"
/>
</UForm>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types'
import { COLORS, type CategoryType, CreateTabSchema, type CreateTabSchemaType } from '~~/types/types'
const props = defineProps<{
category: CategoryType | undefined
}>()
const emit = defineEmits(['closeModal'])
const { createTab } = await useTabs()
const { categories } = await useCategories()
const state = reactive({
name: undefined,
icon: undefined,
link: undefined,
color: COLORS[0],
primary: false,
categoryId: props.category?.id,
})
watchEffect(() => {
state.categoryId = props.category?.id
})
async function handleCreate(event: FormSubmitEvent<CreateTabSchemaType>) {
await createTab(event.data)
emit('closeModal')
state.name = undefined
state.icon = undefined
state.link = undefined
state.color = COLORS[0]
state.primary = false
state.categoryId = props.category?.id
}
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Create a new tab
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<UForm :schema="CreateTabSchema" :state="state" class="space-y-4" @submit="handleCreate">
<UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" />
</UFormGroup>
<UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" />
</UFormGroup>
<UFormGroup label="Color " name="color">
<USelect v-model="state.color" :options="COLORS" />
</UFormGroup>
<UFormGroup label="Category " name="category">
<USelect v-model="state.categoryId" :options="categories" option-attribute="name" value-attribute="id" />
</UFormGroup>
<UFormGroup label="Link " name="link">
<UInput v-model="state.link" type="text" />
</UFormGroup>
<UFormGroup>
<UCheckbox v-model="state.primary" :color="state.color" label="Is the tab primary?" />
</UFormGroup>
<UButton
type="submit"
:color="state.color"
block
label="Create tab"
/>
</UForm>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { CategoryType } from '~~/types/types'
const props = defineProps<{
category: CategoryType | null
}>()
const emit = defineEmits(['closeModal'])
const { deleteCategory } = await useCategories()
async function handleDelete() {
await deleteCategory(props.category.id)
emit('closeModal')
}
defineShortcuts({
enter: async () => await handleDelete(),
})
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Confirm deletion of '{{ category.name }}'
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<div class="space-y-4">
<UButton
color="red"
variant="solid"
label="Delete"
block
@click.prevent="handleDelete"
/>
<UButton
color="gray"
variant="solid"
label="Cancel"
block
@click.prevent="$emit('closeModal')"
/>
</div>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { TabType } from '~~/types/types'
const props = defineProps<{
tab: TabType | null
}>()
const emit = defineEmits(['closeModal'])
const { deleteTab } = await useTabs()
async function handleDelete() {
await deleteTab(props.tab.id)
emit('closeModal')
}
defineShortcuts({
enter: async () => await handleDelete(),
})
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Confirm deletion of '{{ tab.name }}'
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<div class="space-y-4">
<UButton
color="red"
variant="solid"
label="Delete"
block
@click.prevent="handleDelete"
/>
<UButton
color="gray"
variant="solid"
label="Cancel"
block
@click.prevent="$emit('closeModal')"
/>
</div>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { COLORS, type CategoryType, UpdateCategorySchema } from '~~/types/types'
import type { FormSubmitEvent } from '#ui/types'
const props = defineProps<{
category: CategoryType | null
}>()
const emit = defineEmits(['closeModal'])
const { updateCategory } = await useCategories()
const state = reactive({
name: props.category?.name,
icon: props.category?.icon,
color: props.category?.color,
nameVisible: props.category?.nameVisible,
})
watchEffect(() => {
state.name = props.category?.name
state.icon = props.category?.icon
state.color = props.category?.color
state.nameVisible = props.category?.nameVisible
})
async function handleUpdate(event: FormSubmitEvent<UpdateCategorySchema>) {
await updateCategory({
id: props.category!.id,
...event.data,
})
emit('closeModal')
}
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Update category '{{ category.name }}'
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<UForm :schema="UpdateCategorySchema" :state="state" class="space-y-4" @submit="handleUpdate">
<UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" />
</UFormGroup>
<UFormGroup label="Color " name="color">
<USelect v-model="state.color" :options="COLORS" />
</UFormGroup>
<UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" />
</UFormGroup>
<UFormGroup>
<UCheckbox v-model="state.nameVisible" :color="state.color" label="Is the category name visible?" />
</UFormGroup>
<UButton
type="submit"
:color="state.color"
block
label="Update category"
/>
</UForm>
</template>
</UCard>
</UModal>
</template>

View File

@@ -0,0 +1,89 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '#ui/types'
import type { COLORS, TabType, UpdateTabSchemaType } from '~~/types/types'
import { UpdateTabSchema } from '~~/types/types'
const props = defineProps<{
tab: TabType | null
}>()
const emit = defineEmits(['closeModal'])
const { categories } = await useCategories()
const { updateTab } = await useTabs()
const state = reactive({
name: props.tab?.name,
icon: props.tab?.icon,
color: props.tab?.color,
primary: props.tab?.primary,
categoryId: props.tab?.categoryId,
})
watchEffect(() => {
state.name = props.tab?.name
state.icon = props.tab?.icon
state.color = props.tab?.color
state.primary = props.tab?.primary
state.categoryId = props.tab?.categoryId
})
async function handleUpdate(event: FormSubmitEvent<UpdateTabSchemaType>) {
await updateTab({
id: props.tab!.id,
...event.data,
categoryId: Number(event.data.categoryId),
})
emit('closeModal')
}
</script>
<template>
<UModal :ui="{ width: 'w-full sm:max-w-md' }">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Update category '{{ tab.name }}'
</h3>
<UButton
color="gray"
variant="soft"
icon="i-heroicons-x-mark-20-solid"
class="p-1"
@click="$emit('closeModal')"
/>
</div>
</template>
<template #default>
<UForm :schema="UpdateTabSchema" :state="state" class="space-y-4" @submit="handleUpdate">
<UFormGroup label="Name" name="name">
<UInput v-model="state.name" type="text" />
</UFormGroup>
<UFormGroup label="Color " name="color">
<USelect v-model="state.color" :options="COLORS" />
</UFormGroup>
<UFormGroup label="Icon " name="icon">
<UInput v-model="state.icon" type="text" />
</UFormGroup>
<UFormGroup label="Category " name="category">
<USelect v-model="state.categoryId" :options="categories" option-attribute="name" value-attribute="id" />
</UFormGroup>
<UFormGroup>
<UCheckbox v-model="state.primary" :color="state.color" label="Is the category primary?" />
</UFormGroup>
<UButton
type="submit"
:color="state.color"
block
label="Update tab"
/>
</UForm>
</template>
</UCard>
</UModal>
</template>

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
import type { Tab } from '~~/server/utils/db'
defineProps<{
tab: PropType<Tab>
}>()
</script>
<template>
<div>
Tab
{{ tab }}
</div>
</template>
<style scoped>
</style>

View File

@@ -1,13 +1,47 @@
export function useCategories() {
async function getCategories() {
return useAsyncData<CategoryType[]>(async () => {
const res = await $fetch('/api/categories')
console.log('res', res)
return res
import type { type CategoryType, CreateCategorySchema, UpdateCategorySchema } from '~~/types/types'
export async function useCategories() {
const { data: categories, refresh }
= await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories'))
async function getCategory(id: number) {
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!')
}
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!')
}
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!')
}
return {
getCategories,
categories,
getCategory,
createCategory,
updateCategory,
deleteCategory,
}
}

View File

@@ -1,21 +1,52 @@
export function useTabs() {
async function createTab(tab: TabType) {
console.log('createTab', tab)
return tab
import type { CreateTabSchema, TabType, UpdateTabSchema } from '~~/types/types'
export async function useTabs() {
const { data: tabs, refresh }
= await useAsyncData<TabType[]>(async () => await useRequestFetch()('/api/tabs'))
function getTabsForCategory(categoryId: number): TabType[] {
return tabs.value.filter(tab => tab.categoryId === categoryId)
}
async function deleteTab(tab: TabType) {
console.log('deleteTab', tab)
return tab
async function createTab(tab: CreateTabSchema) {
await $fetch('/api/tabs', {
method: 'POST',
body: JSON.stringify(tab),
})
.then(async () => {
await refresh()
useSuccessToast('Tab successfully created!')
})
.catch(error => useErrorToast('Tab creation failed!', `Error: ${error}`))
}
async function updateTab(tab: TabType) {
console.log('updateTab', tab)
return tab
async function updateTab(tab: UpdateTabSchema) {
console.log(tab)
await $fetch(`/api/tabs/${tab.id}`, {
method: 'PUT',
body: JSON.stringify(tab),
})
.then(async () => {
await refresh()
useSuccessToast('Tab successfully updated!')
})
.catch(error => useErrorToast('Tab update failed!', `Error: ${error}`))
}
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!')
}
return {
tabs,
createTab,
deleteTab,
getTabsForCategory,
updateTab,
}
}

8
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<div>
<AppHeader />
<UContainer class="mt-20">
<NuxtPage />
</UContainer>
</div>
</template>

7
app/layouts/login.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div>
<UContainer>
<NuxtPage />
</UContainer>
</div>
</template>

View File

@@ -1,4 +1,7 @@
<script lang="ts" setup>
import type { CategoryType } from '~~/types/types'
import CategoryHeader from '~/components/CategoryHeader.vue'
definePageMeta({
middleware: 'auth',
})
@@ -8,15 +11,61 @@ onMounted(() => {
setInterval(() => date.value = new Date(), 1000)
})
const { user, session } = useUserSession()
const { user } = useUserSession()
const { categories } = await useCategories()
const { getTabsForCategory } = await useTabs()
const { getCategories } = useCategories()
const categories = await getCategories()
// Modals
const createCategoryModal = ref(false)
const updateCategoryModal = ref(false)
const deleteCategoryModal = ref(false)
const createTabModal = ref(false)
// Update Category
const currentUpdateCategory = ref<CategoryType | null>(null)
function openUpdateCategoryModal(category: CategoryType) {
currentUpdateCategory.value = category
updateCategoryModal.value = true
}
// Delete Category
const currentDeleteCategory = ref<CategoryType | null>(null)
function openDeleteCategoryModal(category: CategoryType) {
currentDeleteCategory.value = category
deleteCategoryModal.value = true
}
// Create Tab
const currentCategory = ref<CategoryType | null>(null)
function openCreateTab(category: CategoryType) {
currentCategory.value = category
createTabModal.value = true
}
// DropDown Items
const items = [[
{
label: 'Edit',
icon: 'i-ph:pencil-duotone',
color: 'green',
click: category => openUpdateCategoryModal(category),
},
{
label: 'Delete',
icon: 'i-ph:trash-duotone',
color: 'red',
click: category => openDeleteCategoryModal(category),
},
]]
defineShortcuts({
c: () => createCategoryModal.value = true,
})
</script>
<template>
<main v-if="user" class="my-12">
<div v-if="date" class="flex flex-col items-center">
<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>
@@ -26,29 +75,72 @@ const categories = await getCategories()
{{ useDateFormat(date, 'dddd D MMMM YYYY', { locales: user.language }) }}
</h1>
</div>
<div>
{{ user }}
</div>
<div>
{{ session }}
</div>
<div>
{{ user === session.user }}
</div>
<div v-if="categories">
{{ categories }}
</div>
<div>
<Category>
<Tab
:tab="{
name: 'Test',
nameVisible: true,
icon: 'i-ph:cloud-duotone',
color: 'blue',
}"
/>
</Category>
<div class="flex justify-end mb-8 gap-4">
<UButton
icon="i-ph:folder-simple-plus-duotone"
color="black"
variant="solid"
size="lg"
@click.prevent="createCategoryModal = true"
>
Create Category
<UKbd>C</UKbd>
</UButton>
</div>
<section v-if="categories">
<div v-if="categories.length > 0" class="space-y-12">
<div
v-for="category in categories"
:key="category.id"
>
<CategoryHeader
:dropdown-items="items"
:category="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">
<AppTab
v-for="tab in getTabsForCategory(category.id)"
:key="tab.id"
:tab="tab"
/>
</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">
You don't have any categories.
</h1>
</div>
</section>
<ModalCreateCategory
v-model="createCategoryModal"
@close-modal="createCategoryModal = false"
/>
<ModalUpdateCategory
v-if="currentUpdateCategory"
v-model="updateCategoryModal"
:category="currentUpdateCategory"
@close-modal="updateCategoryModal = false"
/>
<ModalDeleteCategory
v-if="currentDeleteCategory"
v-model="deleteCategoryModal"
:category="currentDeleteCategory"
@close-modal="deleteCategoryModal = false"
/>
<ModalCreateTab
v-if="currentCategory"
v-model="createTabModal"
:category="currentCategory"
@close-modal="createTabModal = false"
/>
</main>
</template>

View File

@@ -7,13 +7,13 @@ const { loggedIn } = useUserSession()
definePageMeta({
middleware: 'ghost',
layout: 'login',
})
const schema = z.object({
email: z.string().email('Invalid email'),
})
const form = ref()
type Schema = z.output<typeof schema>
const state = reactive({ email: undefined })
@@ -62,7 +62,7 @@ if (import.meta.server) {
</template>
<template #default>
<div v-if="!loggedIn" class="flex flex-col gap-4 p-4">
<UForm ref="form" :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<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>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { user, loggedIn, session, clear } = useUserSession()
const { user, loggedIn, clear } = useUserSession()
</script>
<template>
@@ -10,9 +10,6 @@ const { user, loggedIn, session, clear } = useUserSession()
<div>
LoggedIn: {{ loggedIn }}
</div>
<div>
Session: {{ session }}
</div>
<div @click="clear">
clear
</div>