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({ export default defineAppConfig({
ui: { ui: {
gray: 'neutral', gray: 'zinc',
primary: 'gray', 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', title: 'ArtHome by Arthur Danjou',
}) })
const { loggedIn, clear, user } = useUserSession() const { loggedIn } = useUserSession()
const colorMode = useColorMode()
watch(loggedIn, async () => { watch(loggedIn, async () => {
if (!loggedIn.value) { if (!loggedIn.value) {
navigateTo('/login') 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> </script>
<template> <template>
<div> <div>
<NuxtLoadingIndicator color="#808080" /> <NuxtLoadingIndicator color="#808080" />
<UContainer> <NuxtLayout>
<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>
<NuxtPage /> <NuxtPage />
</UContainer> </NuxtLayout>
<UNotifications />
</div> </div>
</template> </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() { import type { type CategoryType, CreateCategorySchema, UpdateCategorySchema } from '~~/types/types'
async function getCategories() {
return useAsyncData<CategoryType[]>(async () => { export async function useCategories() {
const res = await $fetch('/api/categories') const { data: categories, refresh }
console.log('res', res) = await useAsyncData<CategoryType[]>(async () => await useRequestFetch()('/api/categories'))
return res
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 { return {
getCategories, categories,
getCategory,
createCategory,
updateCategory,
deleteCategory,
} }
} }

View File

@@ -1,21 +1,52 @@
export function useTabs() { import type { CreateTabSchema, TabType, UpdateTabSchema } from '~~/types/types'
async function createTab(tab: TabType) {
console.log('createTab', tab) export async function useTabs() {
return tab 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) { async function createTab(tab: CreateTabSchema) {
console.log('deleteTab', tab) await $fetch('/api/tabs', {
return tab 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) { async function updateTab(tab: UpdateTabSchema) {
console.log('updateTab', tab) console.log(tab)
return 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 { return {
tabs,
createTab, createTab,
deleteTab, 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> <script lang="ts" setup>
import type { CategoryType } from '~~/types/types'
import CategoryHeader from '~/components/CategoryHeader.vue'
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
@@ -8,15 +11,61 @@ onMounted(() => {
setInterval(() => date.value = new Date(), 1000) setInterval(() => date.value = new Date(), 1000)
}) })
const { user, session } = useUserSession() const { user } = useUserSession()
const { categories } = await useCategories()
const { getTabsForCategory } = await useTabs()
const { getCategories } = useCategories() // Modals
const categories = await getCategories() 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> </script>
<template> <template>
<main v-if="user" class="my-12"> <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"> <h1 class="text-6xl md:text-9xl font-bold">
{{ useDateFormat(date, 'HH') }} {{ useDateFormat(date, 'HH') }}
<span class="animate-pulse">:</span> <span class="animate-pulse">:</span>
@@ -26,29 +75,72 @@ const categories = await getCategories()
{{ useDateFormat(date, 'dddd D MMMM YYYY', { locales: user.language }) }} {{ useDateFormat(date, 'dddd D MMMM YYYY', { locales: user.language }) }}
</h1> </h1>
</div> </div>
<div> <div class="flex justify-end mb-8 gap-4">
{{ user }} <UButton
</div> icon="i-ph:folder-simple-plus-duotone"
<div> color="black"
{{ session }} variant="solid"
</div> size="lg"
<div> @click.prevent="createCategoryModal = true"
{{ user === session.user }} >
</div> Create Category
<div v-if="categories"> <UKbd>C</UKbd>
{{ categories }} </UButton>
</div>
<div>
<Category>
<Tab
:tab="{
name: 'Test',
nameVisible: true,
icon: 'i-ph:cloud-duotone',
color: 'blue',
}"
/>
</Category>
</div> </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> </main>
</template> </template>

View File

@@ -7,13 +7,13 @@ const { loggedIn } = useUserSession()
definePageMeta({ definePageMeta({
middleware: 'ghost', middleware: 'ghost',
layout: 'login',
}) })
const schema = z.object({ const schema = z.object({
email: z.string().email('Invalid email'), email: z.string().email('Invalid email'),
}) })
const form = ref()
type Schema = z.output<typeof schema> type Schema = z.output<typeof schema>
const state = reactive({ email: undefined }) const state = reactive({ email: undefined })
@@ -62,7 +62,7 @@ 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 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"> <UFormGroup name="email">
<UInput v-model="state.email" color="gray" placeholder="arthur@arthome.com" /> <UInput v-model="state.email" color="gray" placeholder="arthur@arthome.com" />
</UFormGroup> </UFormGroup>

View File

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

View File

@@ -14,17 +14,7 @@ export default defineNuxtConfig({
}, },
// Nuxt Modules // Nuxt Modules
modules: [ modules: ['@nuxthub/core', '@nuxt/ui', '@vueuse/nuxt', '@nuxtjs/google-fonts', 'nuxt-auth-utils', '@nuxt/content', '@nuxthq/studio', '@nuxt/image', 'nuxt-mapbox', '@pinia/nuxt'],
'@nuxthub/core',
'@nuxt/ui',
'@vueuse/nuxt',
'@nuxtjs/google-fonts',
'nuxt-auth-utils',
'@nuxt/content',
'@nuxthq/studio',
'@nuxt/image',
'nuxt-mapbox',
],
// Nuxt UI // Nuxt UI
ui: { ui: {

View File

@@ -15,9 +15,9 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/content": "^2.13.2", "@nuxt/content": "^2.13.2",
"@nuxt/image": "^1.7.0", "@nuxt/image": "^1.7.1",
"@nuxthq/studio": "^2.0.3", "@nuxthq/studio": "^2.0.3",
"@nuxthub/core": "^0.7.3", "@nuxthub/core": "^0.7.7",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/google-fonts": "^3.2.0",
"drizzle-orm": "^0.33.0", "drizzle-orm": "^0.33.0",
"h3-zod": "^0.5.3", "h3-zod": "^0.5.3",
@@ -28,21 +28,20 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.26.1", "@antfu/eslint-config": "^2.27.3",
"@nuxt/devtools": "^1.3.14", "@nuxt/devtools": "^1.4.1",
"@nuxt/ui": "^2.18.4", "@nuxt/ui": "^2.18.4",
"@types/node": "^22.4.2", "@types/node": "^22.5.1",
"@types/pg": "^8.11.6", "@vueuse/core": "^11.0.3",
"@vueuse/core": "^11.0.1", "@vueuse/nuxt": "^11.0.3",
"@vueuse/nuxt": "^11.0.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"drizzle-kit": "^0.24.1", "drizzle-kit": "^0.24.2",
"eslint": "^9.9.0", "eslint": "^9.9.1",
"mapbox-gl": "^3.6.0", "mapbox-gl": "^3.6.0",
"nuxt": "^3.13.0", "nuxt": "^3.13.0",
"nuxt-mapbox": "^1.6.0", "nuxt-mapbox": "^1.6.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vue-tsc": "^2.0.29", "vue-tsc": "^2.0.29",
"wrangler": "^3.72.1" "wrangler": "^3.72.3"
} }
} }

1419
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
export default defineEventHandler(async (event) => {
try {
const user = await getUserSession(event)
const { id } = await getRouterParams(event)
await useDrizzle()
.delete(tables.categories)
.where(
and(
eq(tables.categories.id, id),
eq(tables.categories.userId, user.id),
),
)
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -0,0 +1,28 @@
import { useValidatedBody } from 'h3-zod'
import { UpdateCategorySchema } from '~~/types/types'
export default defineEventHandler(async (event) => {
try {
const user = await getUserSession(event)
const { id } = await getRouterParams(event)
const body = await useValidatedBody(event, UpdateCategorySchema)
await useDrizzle()
.update(tables.categories)
.set({
name: body.name,
icon: body.icon,
color: body.color,
nameVisible: body.nameVisible,
})
.where(
and(
eq(tables.categories.id, id),
eq(tables.categories.userId, user.id),
),
)
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -1,7 +1,10 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const user = await getUserSession(event) const user = await requireUserSession(event)
console.log('session', user) return useDrizzle()
return useDrizzle().query.categories.findMany({ .select()
where: eq(tables.users.id, user.id), .from(tables.categories)
}) .where(
eq(tables.categories.userId, user.user.id),
)
.orderBy(tables.categories.id, 'desc')
}) })

View File

@@ -0,0 +1,20 @@
import { useValidatedBody } from 'h3-zod'
import { CreateCategorySchema } from '~~/types/types'
export default defineEventHandler(async (event) => {
try {
const user = await getUserSession(event)
const body = await useValidatedBody(event, CreateCategorySchema)
await useDrizzle().insert(tables.categories).values({
name: body.name,
icon: body.icon,
color: body.color,
nameVisible: body.nameVisible,
userId: user.id,
})
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -0,0 +1,16 @@
export default defineEventHandler(async (event) => {
try {
const { id } = await getRouterParams(event)
await useDrizzle()
.delete(tables.tabs)
.where(
and(
eq(tables.tabs.id, id),
),
)
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -0,0 +1,29 @@
import { useValidatedBody } from 'h3-zod'
import { UpdateTabSchema } from '~~/types/types'
export default defineEventHandler(async (event) => {
try {
const { id } = await getRouterParams(event)
console.log(await readBody(event))
const body = await useValidatedBody(event, UpdateTabSchema)
await useDrizzle()
.update(tables.tabs)
.set({
name: body.name,
icon: body.icon,
color: body.color,
nameVisible: body.nameVisible,
link: body.link,
})
.where(
and(
eq(tables.tabs.id, id),
eq(tables.tabs.categoryId, body.categoryId),
),
)
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -0,0 +1,6 @@
export default defineEventHandler(async () => {
return useDrizzle()
.select()
.from(tables.tabs)
.orderBy(tables.tabs.id, 'desc')
})

View File

@@ -0,0 +1,20 @@
import { useValidatedBody } from 'h3-zod'
import { CreateTabSchema } from '~~/types/types'
export default defineEventHandler(async (event) => {
try {
const body = await useValidatedBody(event, CreateTabSchema)
await useDrizzle().insert(tables.tabs).values({
name: body.name,
icon: body.icon,
color: body.color,
nameVisible: body.nameVisible,
categoryId: body.categoryId,
link: body.link,
})
return { statusCode: 200 }
}
catch (err) {
return { err }
}
})

View File

@@ -6,17 +6,10 @@ END $$;
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "categories" ( CREATE TABLE IF NOT EXISTS "categories" (
"id" serial PRIMARY KEY NOT NULL, "id" serial PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL, "name" text DEFAULT '',
"name_visible" boolean DEFAULT true NOT NULL, "name_visible" boolean DEFAULT true,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone' NOT NULL, "icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
"color" text DEFAULT 'gray' NOT NULL, "color" text DEFAULT 'gray',
"page_id" integer NOT NULL,
"created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "pages" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL, "user_id" integer NOT NULL,
"created_at" timestamp (3) DEFAULT now(), "created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3) "updated_at" timestamp (3)
@@ -24,10 +17,10 @@ CREATE TABLE IF NOT EXISTS "pages" (
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "tabs" ( CREATE TABLE IF NOT EXISTS "tabs" (
"id" serial PRIMARY KEY NOT NULL, "id" serial PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL, "name" text DEFAULT '',
"name_visible" boolean DEFAULT true NOT NULL, "name_visible" boolean DEFAULT true,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone' NOT NULL, "icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
"color" text DEFAULT 'gray' NOT NULL, "color" text DEFAULT 'gray',
"category_id" integer NOT NULL, "category_id" integer NOT NULL,
"created_at" timestamp (3) DEFAULT now(), "created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3) "updated_at" timestamp (3)
@@ -43,10 +36,11 @@ CREATE TABLE IF NOT EXISTS "users" (
"google_id" text, "google_id" text,
"google_token" text, "google_token" text,
"description" text DEFAULT '', "description" text DEFAULT '',
"private" boolean DEFAULT false NOT NULL, "avatar" text DEFAULT '',
"timezone" text DEFAULT 'undefined' NOT NULL, "private" boolean DEFAULT false,
"location" text DEFAULT 'undefined' NOT NULL, "language" text DEFAULT 'en-EN',
"subscription" "subscription" DEFAULT 'free' NOT NULL, "location" text DEFAULT 'unknown',
"subscription" "subscription" DEFAULT 'free',
"created_at" timestamp (3) DEFAULT now(), "created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3), "updated_at" timestamp (3),
CONSTRAINT "users_email_unique" UNIQUE("email"), CONSTRAINT "users_email_unique" UNIQUE("email"),
@@ -55,13 +49,7 @@ CREATE TABLE IF NOT EXISTS "users" (
); );
--> statement-breakpoint --> statement-breakpoint
DO $$ BEGIN DO $$ BEGIN
ALTER TABLE "categories" ADD CONSTRAINT "categories_page_id_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action; 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 "pages" ADD CONSTRAINT "pages_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION EXCEPTION
WHEN duplicate_object THEN null; WHEN duplicate_object THEN null;
END $$; END $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "tabs" ADD COLUMN "link" text DEFAULT '';

View File

@@ -1,4 +0,0 @@
ALTER TABLE "categories" ALTER COLUMN "id" SET DATA TYPE integer;--> statement-breakpoint
ALTER TABLE "pages" ALTER COLUMN "id" SET DATA TYPE integer;--> statement-breakpoint
ALTER TABLE "tabs" ALTER COLUMN "id" SET DATA TYPE integer;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "id" SET DATA TYPE integer;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "tabs" ADD COLUMN "primary" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "tabs" DROP COLUMN IF EXISTS "name_visible";

View File

@@ -1,13 +0,0 @@
ALTER TABLE "categories" ALTER COLUMN "name" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "name_visible" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "icon" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "categories" ALTER COLUMN "color" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "tabs" ALTER COLUMN "name" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "tabs" ALTER COLUMN "name_visible" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "tabs" ALTER COLUMN "icon" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "tabs" ALTER COLUMN "color" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "private" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "timezone" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "location" SET DEFAULT 'unknown';--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "location" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "subscription" DROP NOT NULL;

View File

@@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "avatar" text DEFAULT '';

View File

@@ -1,2 +0,0 @@
ALTER TABLE "users" RENAME COLUMN "timezone" TO "language";--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "language" SET DEFAULT 'english';

View File

@@ -1 +0,0 @@
ALTER TABLE "users" ALTER COLUMN "language" SET DEFAULT 'en-EN';

View File

@@ -1,5 +1,5 @@
{ {
"id": "a8ec7e1e-1087-4ab5-be19-459dc9b0a4e0", "id": "c52dbfc1-beae-4a41-8725-66def9fdacea",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@@ -18,78 +18,29 @@
"name": "name", "name": "name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "''" "default": "''"
}, },
"name_visible": { "name_visible": {
"name": "name_visible", "name": "name_visible",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": true "default": true
}, },
"icon": { "icon": {
"name": "icon", "name": "icon",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'" "default": "'i-ph:circle-wavy-question-duotone'"
}, },
"color": { "color": {
"name": "color", "name": "color",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true,
"default": "'gray'"
},
"page_id": {
"name": "page_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp (3)",
"primaryKey": false,
"notNull": false, "notNull": false,
"default": "now()" "default": "'gray'"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp (3)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"categories_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
}, },
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
@@ -113,9 +64,9 @@
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"pages_user_id_users_id_fk": { "categories_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk", "name": "categories_user_id_users_id_fk",
"tableFrom": "pages", "tableFrom": "categories",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": [
"user_id" "user_id"
@@ -144,28 +95,28 @@
"name": "name", "name": "name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "''" "default": "''"
}, },
"name_visible": { "name_visible": {
"name": "name_visible", "name": "name_visible",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": true "default": true
}, },
"icon": { "icon": {
"name": "icon", "name": "icon",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'" "default": "'i-ph:circle-wavy-question-duotone'"
}, },
"color": { "color": {
"name": "color", "name": "color",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'gray'" "default": "'gray'"
}, },
"category_id": { "category_id": {
@@ -266,33 +217,40 @@
"notNull": false, "notNull": false,
"default": "''" "default": "''"
}, },
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"private": { "private": {
"name": "private", "name": "private",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": false "default": false
}, },
"timezone": { "language": {
"name": "timezone", "name": "language",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'undefined'" "default": "'en-EN'"
}, },
"location": { "location": {
"name": "location", "name": "location",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'undefined'" "default": "'unknown'"
}, },
"subscription": { "subscription": {
"name": "subscription", "name": "subscription",
"type": "subscription", "type": "subscription",
"typeSchema": "public", "typeSchema": "public",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'free'" "default": "'free'"
}, },
"created_at": { "created_at": {

View File

@@ -1,6 +1,6 @@
{ {
"id": "0550ff2a-d819-4a38-a515-915d5ef620a6", "id": "1a96f2ca-db04-445d-b671-d61aaeef8882",
"prevId": "a8ec7e1e-1087-4ab5-be19-459dc9b0a4e0", "prevId": "c52dbfc1-beae-4a41-8725-66def9fdacea",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"tables": { "tables": {
@@ -10,7 +10,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -18,78 +18,29 @@
"name": "name", "name": "name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "''" "default": "''"
}, },
"name_visible": { "name_visible": {
"name": "name_visible", "name": "name_visible",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": true "default": true
}, },
"icon": { "icon": {
"name": "icon", "name": "icon",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'" "default": "'i-ph:circle-wavy-question-duotone'"
}, },
"color": { "color": {
"name": "color", "name": "color",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true,
"default": "'gray'"
},
"page_id": {
"name": "page_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp (3)",
"primaryKey": false,
"notNull": false, "notNull": false,
"default": "now()" "default": "'gray'"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp (3)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"categories_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
}, },
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
@@ -113,9 +64,9 @@
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"pages_user_id_users_id_fk": { "categories_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk", "name": "categories_user_id_users_id_fk",
"tableFrom": "pages", "tableFrom": "categories",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": [
"user_id" "user_id"
@@ -136,7 +87,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -144,30 +95,37 @@
"name": "name", "name": "name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "''" "default": "''"
}, },
"name_visible": { "name_visible": {
"name": "name_visible", "name": "name_visible",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": true "default": true
}, },
"icon": { "icon": {
"name": "icon", "name": "icon",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'" "default": "'i-ph:circle-wavy-question-duotone'"
}, },
"color": { "color": {
"name": "color", "name": "color",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'gray'" "default": "'gray'"
}, },
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"category_id": { "category_id": {
"name": "category_id", "name": "category_id",
"type": "integer", "type": "integer",
@@ -213,7 +171,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -266,33 +224,40 @@
"notNull": false, "notNull": false,
"default": "''" "default": "''"
}, },
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"private": { "private": {
"name": "private", "name": "private",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": false "default": false
}, },
"timezone": { "language": {
"name": "timezone", "name": "language",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'undefined'" "default": "'en-EN'"
}, },
"location": { "location": {
"name": "location", "name": "location",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'undefined'" "default": "'unknown'"
}, },
"subscription": { "subscription": {
"name": "subscription", "name": "subscription",
"type": "subscription", "type": "subscription",
"typeSchema": "public", "typeSchema": "public",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"default": "'free'" "default": "'free'"
}, },
"created_at": { "created_at": {

View File

@@ -1,6 +1,6 @@
{ {
"id": "7d4e591a-f6c7-48eb-b9e8-e0e200bfea26", "id": "b9aba4fe-7f04-4acc-b47f-2d29d739df98",
"prevId": "0550ff2a-d819-4a38-a515-915d5ef620a6", "prevId": "1a96f2ca-db04-445d-b671-d61aaeef8882",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"tables": { "tables": {
@@ -10,7 +10,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -42,55 +42,6 @@
"notNull": false, "notNull": false,
"default": "'gray'" "default": "'gray'"
}, },
"page_id": {
"name": "page_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_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
"type": "integer", "type": "integer",
@@ -113,9 +64,9 @@
}, },
"indexes": {}, "indexes": {},
"foreignKeys": { "foreignKeys": {
"pages_user_id_users_id_fk": { "categories_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk", "name": "categories_user_id_users_id_fk",
"tableFrom": "pages", "tableFrom": "categories",
"tableTo": "users", "tableTo": "users",
"columnsFrom": [ "columnsFrom": [
"user_id" "user_id"
@@ -136,7 +87,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -147,12 +98,12 @@
"notNull": false, "notNull": false,
"default": "''" "default": "''"
}, },
"name_visible": { "primary": {
"name": "name_visible", "name": "primary",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"default": true "default": false
}, },
"icon": { "icon": {
"name": "icon", "name": "icon",
@@ -168,6 +119,13 @@
"notNull": false, "notNull": false,
"default": "'gray'" "default": "'gray'"
}, },
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"category_id": { "category_id": {
"name": "category_id", "name": "category_id",
"type": "integer", "type": "integer",
@@ -213,7 +171,7 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
@@ -266,6 +224,13 @@
"notNull": false, "notNull": false,
"default": "''" "default": "''"
}, },
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"private": { "private": {
"name": "private", "name": "private",
"type": "boolean", "type": "boolean",
@@ -273,12 +238,12 @@
"notNull": false, "notNull": false,
"default": false "default": false
}, },
"timezone": { "language": {
"name": "timezone", "name": "language",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"default": "'undefined'" "default": "'en-EN'"
}, },
"location": { "location": {
"name": "location", "name": "location",

View File

@@ -1,364 +0,0 @@
{
"id": "d4ae60ba-5be1-4aa9-90d7-0690a599bf8e",
"prevId": "7d4e591a-f6c7-48eb-b9e8-e0e200bfea26",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.categories": {
"name": "categories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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'"
},
"page_id": {
"name": "page_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_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"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": {
"pages_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk",
"tableFrom": "pages",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.tabs": {
"name": "tabs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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": "integer",
"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
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'undefined'"
},
"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": {}
}
}

View File

@@ -1,364 +0,0 @@
{
"id": "704c03b2-8d7f-47ce-a551-95289048c5f2",
"prevId": "d4ae60ba-5be1-4aa9-90d7-0690a599bf8e",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.categories": {
"name": "categories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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'"
},
"page_id": {
"name": "page_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_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"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": {
"pages_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk",
"tableFrom": "pages",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.tabs": {
"name": "tabs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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": "integer",
"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": "'english'"
},
"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": {}
}
}

View File

@@ -1,364 +0,0 @@
{
"id": "e891a8e0-61c1-4351-90fe-caace29457a8",
"prevId": "704c03b2-8d7f-47ce-a551-95289048c5f2",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.categories": {
"name": "categories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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'"
},
"page_id": {
"name": "page_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_page_id_pages_id_fk": {
"name": "categories_page_id_pages_id_fk",
"tableFrom": "categories",
"tableTo": "pages",
"columnsFrom": [
"page_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.pages": {
"name": "pages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"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": {
"pages_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk",
"tableFrom": "pages",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.tabs": {
"name": "tabs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"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": "integer",
"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": {}
}
}

View File

@@ -5,43 +5,22 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1724455773734, "when": 1724865045534,
"tag": "0000_wild_luke_cage", "tag": "0000_giant_stranger",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "7", "version": "7",
"when": 1724455851539, "when": 1724884620789,
"tag": "0001_goofy_dormammu", "tag": "0001_fancy_tyger_tiger",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "7", "version": "7",
"when": 1724456130150, "when": 1725015619221,
"tag": "0002_slim_whistler", "tag": "0002_cool_dexter_bennett",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1724528975297,
"tag": "0003_curious_solo",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1724531645621,
"tag": "0004_sharp_shocker",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1724532003950,
"tag": "0005_tense_the_order",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -23,53 +23,39 @@ export const users = pgTable('users', {
...timestamps, ...timestamps,
}) })
export const pages = pgTable('pages', {
id,
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
...timestamps,
})
export const categories = pgTable('categories', { export const categories = pgTable('categories', {
id, id,
name: text('name').default(''), name: text('name').default(''),
nameVisible: boolean('name_visible').default(true), nameVisible: boolean('name_visible').default(true),
icon: text('icon').default('i-ph:circle-wavy-question-duotone'), icon: text('icon').default('i-ph:circle-wavy-question-duotone'),
color: text('color').default('gray'), color: text('color').default('gray'),
pageId: integer('page_id') userId: integer('user_id')
.notNull() .notNull()
.references(() => pages.id, { onDelete: 'cascade' }), .references(() => users.id, { onDelete: 'cascade' }),
...timestamps, ...timestamps,
}) })
export const tabs = pgTable('tabs', { export const tabs = pgTable('tabs', {
id, id,
name: text('name').default(''), name: text('name').default(''),
nameVisible: boolean('name_visible').default(true), primary: boolean('primary').default(false),
icon: text('icon').default('i-ph:circle-wavy-question-duotone'), icon: text('icon').default('i-ph:circle-wavy-question-duotone'),
color: text('color').default('gray'), color: text('color').default('gray'),
link: text('link').default(''),
categoryId: integer('category_id') categoryId: integer('category_id')
.notNull() .notNull()
.references(() => categories.id, { onDelete: 'cascade' }), .references(() => categories.id, { onDelete: 'cascade' }),
...timestamps, ...timestamps,
}) })
export const usersRelations = relations(users, ({ one }) => ({ export const usersRelations = relations(users, ({ many }) => ({
page: one(pages, {
fields: [users.id],
references: [pages.userId],
}),
}))
export const pagesRelations = relations(pages, ({ many }) => ({
categories: many(categories), categories: many(categories),
})) }))
export const categoriesRelations = relations(categories, ({ one, many }) => ({ export const categoriesRelations = relations(categories, ({ one, many }) => ({
page: one(pages, { user: one(users, {
fields: [categories.pageId], fields: [categories.userId],
references: [pages.id], references: [users.id],
}), }),
tabs: many(tabs), tabs: many(tabs),
})) }))

View File

@@ -1,6 +1,7 @@
export default oauthGoogleEventHandler({ export default oauthGoogleEventHandler({
config: { config: {
emailRequired: true, emailRequired: true,
scope: ['email', 'profile'],
}, },
async onSuccess(event, { user: oauthUser, tokens }) { async onSuccess(event, { user: oauthUser, tokens }) {
const userSession = await getUserSession(event) const userSession = await getUserSession(event)
@@ -15,7 +16,7 @@ export default oauthGoogleEventHandler({
googleToken: tokens.access_token, googleToken: tokens.access_token,
}) })
await replaceUserSession(event, { await setUserSession(event, {
id: userSession.id, id: userSession.id,
user: userSession, user: userSession,
googleId: oauthUser.sub, googleId: oauthUser.sub,
@@ -35,7 +36,7 @@ export default oauthGoogleEventHandler({
googleToken: tokens.access_token, googleToken: tokens.access_token,
}) })
await replaceUserSession(event, { await setUserSession(event, {
id: user.id, id: user.id,
user, user,
}) })
@@ -76,7 +77,7 @@ export default oauthGoogleEventHandler({
subscription: 'free', subscription: 'free',
}) })
await replaceUserSession(event, { await setUserSession(event, {
id: createdUser.id, id: createdUser.id,
user: createdUser, user: createdUser,
}) })

View File

@@ -10,9 +10,3 @@ 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 }), { schema })
} }
export type UserType = typeof schema.users.$inferSelect
export type UserInsert = typeof schema.users.$inferInsert
export type TabType = typeof schema.tabs.$inferSelect
export type CategoryType = typeof schema.categories.$inferSelect

View File

@@ -1,18 +1,11 @@
import * as pg from 'drizzle-orm/pg-core' import { serial, timestamp } from 'drizzle-orm/pg-core'
/** /**
* A centralized list of standardized Drizzle ORM schema field definitions to prevent duplication errors * A centralized list of standardized Drizzle ORM schema field definitions to prevent duplication errors
*/ */
export const createdAt = pg export const createdAt = timestamp('created_at', { mode: 'date', precision: 3 }).defaultNow()
.timestamp('created_at', { mode: 'date', precision: 3 }) export const updatedAt = timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date())
.defaultNow() export const id = serial('id').primaryKey()
export const updatedAt = pg
.timestamp('updated_at', { mode: 'date', precision: 3 })
.$onUpdate(() => new Date())
export const id = pg.integer('id').primaryKey({ autoIncrement: true })
export const timestamps = { export const timestamps = {
createdAt, createdAt,

View File

@@ -1,7 +1,66 @@
import type { ParsedContent } from '@nuxt/content' import type { ParsedContent } from '@nuxt/content'
import { z } from 'zod'
export const COLORS = ['gray', 'slate', 'zinc', 'neutral', 'stone', 'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose']
export const Subscription = ['free', 'paid'] as const export const Subscription = ['free', 'paid'] as const
// Category
export const CreateCategorySchema = z.object({
name: z.string().min(4),
icon: z.string(),
color: z.enum(COLORS).default('gray'),
nameVisible: z.boolean().optional().default(false),
})
export const CreateCategorySchemaType = z.infer<typeof CreateCategorySchema>
export const UpdateCategorySchema = z.object({
name: z.string().min(4).optional(),
icon: z.string().optional(),
color: z.string().optional(),
nameVisible: z.boolean().optional().default(false),
})
export const UpdateCategorySchemaType = z.infer<typeof UpdateCategorySchema>
export interface CategoryType {
id: number
name: string
icon: string
color: string
nameVisible: boolean
}
// Tab
export const CreateTabSchema = z.object({
name: z.string().min(4),
icon: z.string(),
color: z.enum(COLORS).default('gray'),
primary: z.boolean().optional().default(false),
link: z.string(),
categoryId: z.number(),
})
export const CreateTabSchemaType = z.infer<typeof CreateTabSchema>
export const UpdateTabSchema = z.object({
name: z.string().min(4).optional(),
icon: z.string().optional(),
color: z.enum(COLORS).default('gray').optional(),
primary: z.boolean().optional().default(false),
link: z.string().optional(),
categoryId: z.number(),
})
export const UpdateTabSchemaType = z.infer<typeof UpdateTabSchema>
export interface TabType {
id: number
name: string
icon: string
color: string
primary: boolean
categoryId: number
link: string
}
// todo: delete // todo: delete
export interface AppType extends ParsedContent { export interface AppType extends ParsedContent {
primary?: boolean primary?: boolean