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>

View File

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

View File

@@ -15,9 +15,9 @@
},
"dependencies": {
"@nuxt/content": "^2.13.2",
"@nuxt/image": "^1.7.0",
"@nuxt/image": "^1.7.1",
"@nuxthq/studio": "^2.0.3",
"@nuxthub/core": "^0.7.3",
"@nuxthub/core": "^0.7.7",
"@nuxtjs/google-fonts": "^3.2.0",
"drizzle-orm": "^0.33.0",
"h3-zod": "^0.5.3",
@@ -28,21 +28,20 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@antfu/eslint-config": "^2.26.1",
"@nuxt/devtools": "^1.3.14",
"@antfu/eslint-config": "^2.27.3",
"@nuxt/devtools": "^1.4.1",
"@nuxt/ui": "^2.18.4",
"@types/node": "^22.4.2",
"@types/pg": "^8.11.6",
"@vueuse/core": "^11.0.1",
"@vueuse/nuxt": "^11.0.1",
"@types/node": "^22.5.1",
"@vueuse/core": "^11.0.3",
"@vueuse/nuxt": "^11.0.3",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.24.1",
"eslint": "^9.9.0",
"drizzle-kit": "^0.24.2",
"eslint": "^9.9.1",
"mapbox-gl": "^3.6.0",
"nuxt": "^3.13.0",
"nuxt-mapbox": "^1.6.0",
"typescript": "^5.5.4",
"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) => {
const user = await getUserSession(event)
console.log('session', user)
return useDrizzle().query.categories.findMany({
where: eq(tables.users.id, user.id),
})
const user = await requireUserSession(event)
return useDrizzle()
.select()
.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
CREATE TABLE IF NOT EXISTS "categories" (
"id" serial PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL,
"name_visible" boolean DEFAULT true NOT NULL,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone' NOT NULL,
"color" text DEFAULT 'gray' NOT NULL,
"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,
"name" text DEFAULT '',
"name_visible" boolean DEFAULT true,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
"color" text DEFAULT 'gray',
"user_id" integer NOT NULL,
"created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3)
@@ -24,10 +17,10 @@ CREATE TABLE IF NOT EXISTS "pages" (
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "tabs" (
"id" serial PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL,
"name_visible" boolean DEFAULT true NOT NULL,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone' NOT NULL,
"color" text DEFAULT 'gray' NOT NULL,
"name" text DEFAULT '',
"name_visible" boolean DEFAULT true,
"icon" text DEFAULT 'i-ph:circle-wavy-question-duotone',
"color" text DEFAULT 'gray',
"category_id" integer NOT NULL,
"created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3)
@@ -43,10 +36,11 @@ CREATE TABLE IF NOT EXISTS "users" (
"google_id" text,
"google_token" text,
"description" text DEFAULT '',
"private" boolean DEFAULT false NOT NULL,
"timezone" text DEFAULT 'undefined' NOT NULL,
"location" text DEFAULT 'undefined' NOT NULL,
"subscription" "subscription" DEFAULT 'free' NOT NULL,
"avatar" text DEFAULT '',
"private" boolean DEFAULT false,
"language" text DEFAULT 'en-EN',
"location" text DEFAULT 'unknown',
"subscription" "subscription" DEFAULT 'free',
"created_at" timestamp (3) DEFAULT now(),
"updated_at" timestamp (3),
CONSTRAINT "users_email_unique" UNIQUE("email"),
@@ -55,13 +49,7 @@ CREATE TABLE IF NOT EXISTS "users" (
);
--> statement-breakpoint
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;
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;
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 $$;

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",
"version": "7",
"dialect": "postgresql",
@@ -18,78 +18,29 @@
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "''"
},
"name_visible": {
"name": "name_visible",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": true
},
"icon": {
"name": "icon",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'"
},
"color": {
"name": "color",
"type": "text",
"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,
"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": "serial",
"primaryKey": true,
"notNull": true
"default": "'gray'"
},
"user_id": {
"name": "user_id",
@@ -113,9 +64,9 @@
},
"indexes": {},
"foreignKeys": {
"pages_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk",
"tableFrom": "pages",
"categories_user_id_users_id_fk": {
"name": "categories_user_id_users_id_fk",
"tableFrom": "categories",
"tableTo": "users",
"columnsFrom": [
"user_id"
@@ -144,28 +95,28 @@
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "''"
},
"name_visible": {
"name": "name_visible",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": true
},
"icon": {
"name": "icon",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "'i-ph:circle-wavy-question-duotone'"
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "'gray'"
},
"category_id": {
@@ -266,33 +217,40 @@
"notNull": false,
"default": "''"
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"private": {
"name": "private",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": false
},
"timezone": {
"name": "timezone",
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'undefined'"
"notNull": false,
"default": "'en-EN'"
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'undefined'"
"notNull": false,
"default": "'unknown'"
},
"subscription": {
"name": "subscription",
"type": "subscription",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"notNull": false,
"default": "'free'"
},
"created_at": {
@@ -354,4 +312,4 @@
"schemas": {},
"tables": {}
}
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"id": "7d4e591a-f6c7-48eb-b9e8-e0e200bfea26",
"prevId": "0550ff2a-d819-4a38-a515-915d5ef620a6",
"id": "b9aba4fe-7f04-4acc-b47f-2d29d739df98",
"prevId": "1a96f2ca-db04-445d-b671-d61aaeef8882",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -10,7 +10,7 @@
"columns": {
"id": {
"name": "id",
"type": "integer",
"type": "serial",
"primaryKey": true,
"notNull": true
},
@@ -42,55 +42,6 @@
"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",
@@ -113,9 +64,9 @@
},
"indexes": {},
"foreignKeys": {
"pages_user_id_users_id_fk": {
"name": "pages_user_id_users_id_fk",
"tableFrom": "pages",
"categories_user_id_users_id_fk": {
"name": "categories_user_id_users_id_fk",
"tableFrom": "categories",
"tableTo": "users",
"columnsFrom": [
"user_id"
@@ -136,7 +87,7 @@
"columns": {
"id": {
"name": "id",
"type": "integer",
"type": "serial",
"primaryKey": true,
"notNull": true
},
@@ -147,12 +98,12 @@
"notNull": false,
"default": "''"
},
"name_visible": {
"name": "name_visible",
"primary": {
"name": "primary",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
"default": false
},
"icon": {
"name": "icon",
@@ -168,6 +119,13 @@
"notNull": false,
"default": "'gray'"
},
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"category_id": {
"name": "category_id",
"type": "integer",
@@ -213,7 +171,7 @@
"columns": {
"id": {
"name": "id",
"type": "integer",
"type": "serial",
"primaryKey": true,
"notNull": true
},
@@ -266,6 +224,13 @@
"notNull": false,
"default": "''"
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"private": {
"name": "private",
"type": "boolean",
@@ -273,12 +238,12 @@
"notNull": false,
"default": false
},
"timezone": {
"name": "timezone",
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'undefined'"
"default": "'en-EN'"
},
"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,
"version": "7",
"when": 1724455773734,
"tag": "0000_wild_luke_cage",
"when": 1724865045534,
"tag": "0000_giant_stranger",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1724455851539,
"tag": "0001_goofy_dormammu",
"when": 1724884620789,
"tag": "0001_fancy_tyger_tiger",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1724456130150,
"tag": "0002_slim_whistler",
"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",
"when": 1725015619221,
"tag": "0002_cool_dexter_bennett",
"breakpoints": true
}
]

View File

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

View File

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

View File

@@ -10,9 +10,3 @@ export function useDrizzle() {
const config = useRuntimeConfig()
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
*/
export const createdAt = pg
.timestamp('created_at', { mode: 'date', precision: 3 })
.defaultNow()
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 createdAt = timestamp('created_at', { mode: 'date', precision: 3 }).defaultNow()
export const updatedAt = timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date())
export const id = serial('id').primaryKey()
export const timestamps = {
createdAt,

View File

@@ -1,7 +1,66 @@
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
// 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
export interface AppType extends ParsedContent {
primary?: boolean