first commit
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled

This commit is contained in:
AlucardDev
2026-08-28 23:32:07 +03:00
commit 7d4ff5962c
35 changed files with 15426 additions and 0 deletions

43
app/pages/index.vue Normal file
View File

@@ -0,0 +1,43 @@
<template>
<div class="flex flex-col gap-4">
<p class="font-medium">
Welcome to Atidone.
</p>
<p>
A <a
href="https://nuxt.com"
target="_blank"
class="text-primary"
rel="noopener"
>Nuxt</a> demo hosted on <a
href="https://vercel.com"
target="_blank"
rel="noopener"
class="text-primary"
>Vercel</a> with server-side rendering on the edge and using <NuxtLink
href="https://turso.tech"
target="_blank"
rel="noopener"
class="text-primary"
>
Turso database
</NuxtLink>.
</p>
<p>
It is made using <a
href="https://hub.nuxt.com/docs/features/database"
class="text-primary"
>NuxtHub Database</a> and <a
href="https://github.com/atinux/nuxt-auth-utils"
target="_blank"
rel="noopener"
class="text-primary"
>nuxt-auth-utils</a> for an almost zero-config development & deployment experience.
</p>
<USeparator />
<p class="text-sm text-neutral-500 italic">
No personal information regarding your GitHub account are stored in database.<br>
We store only the todos created linked with your GitHub ID.
</p>
</div>
</template>

View File

@@ -0,0 +1,228 @@
<script setup lang="ts">
import { todosQuery } from '~/queries/todos'
definePageMeta({
middleware: 'auth'
})
const newTodo = ref('')
const toast = useToast()
const { user } = useUserSession()
const queryCache = useQueryCache()
const { data: todos } = useQuery(todosQuery)
const { mutate: addTodo } = useMutation({
mutation: (title: string) => {
if (!title.trim()) throw new Error('Title is required')
return $fetch('/api/todos', {
method: 'POST',
body: {
title,
completed: 0
}
}) as Promise<Todo>
},
onMutate(title) {
// let the user enter new todos right away!
newTodo.value = ''
const oldTodos = queryCache.getQueryData(todosQuery.key) || []
const newTodoItem = {
title,
completed: 0,
// a negative id to differentiate them from the server ones
id: -Date.now(),
createdAt: new Date(),
userId: user.value!.id
} satisfies Todo
// we use newTodos to check for the cache consistency
// a better way would be to save the entry time
// const when = queryCache.getEntries({ key: ['todos'], exact: true }).at(0)?.when
const newTodos = [...oldTodos, newTodoItem]
queryCache.setQueryData(todosQuery.key, newTodos)
queryCache.cancelQueries({ key: todosQuery.key, exact: true })
return { oldTodos, newTodos, newTodoItem }
},
onSuccess(todo, _, { newTodoItem }) {
// update the todo with the information from the server
// since we are invalidating queries, this allows us to progressively
// update the todo list even if the user is adding a lot very quickly
const todoList = queryCache.getQueryData(todosQuery.key) || []
const todoIndex = todoList.findIndex(t => t.id === newTodoItem.id)
if (todoIndex >= 0) {
queryCache.setQueryData(
todosQuery.key,
todoList.toSpliced(todoIndex, 1, todo)
)
}
},
onSettled() {
// always refetch the todos after a mutation
queryCache.invalidateQueries({ key: todosQuery.key })
},
onError(err, _title, { oldTodos, newTodos }) {
// oldTodos can be undefined if onMutate errors
// we also want to check if the oldTodos are still in the cache
// because the cache could have been updated by another query
if (
newTodos != null
&& newTodos === queryCache.getQueryData(todosQuery.key)
) {
queryCache.setQueryData(todosQuery.key, oldTodos)
}
if (isNuxtZodError(err)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const title = (err as any).data.data.issues
.map((issue: { message: string }) => issue.message)
.join('\n')
toast.add({ title, color: 'error' })
}
else {
console.error(err)
toast.add({ title: 'Unexpected Error', color: 'error' })
}
}
})
const { mutate: toggleTodo } = useMutation({
mutation: (todo: Todo) =>
$fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: {
completed: !todo.completed
}
}),
onMutate(todo) {
const oldTodos = queryCache.getQueryData(todosQuery.key) || []
const todoIndex = oldTodos.findIndex(t => t.id === todo.id)
let newTodos = oldTodos
if (todoIndex >= 0) {
newTodos = oldTodos.toSpliced(todoIndex, 1, {
...todo,
completed: todo.completed ? 0 : 1
})
queryCache.setQueryData(todosQuery.key, newTodos)
}
queryCache.cancelQueries({ key: todosQuery.key, exact: true })
return { oldTodos, newTodos }
},
onSettled() {
// always refetch the todos after a mutation
queryCache.invalidateQueries({ key: todosQuery.key, exact: true })
},
onError(err, todo, { oldTodos, newTodos }) {
// oldTodos can be undefined if onMutate errors
if (
newTodos != null
&& newTodos === queryCache.getQueryData(todosQuery.key)
) {
queryCache.setQueryData(todosQuery.key, oldTodos)
}
console.error(err)
toast.add({ title: 'Unexpected Error', color: 'error' })
}
})
const { mutate: deleteTodo } = useMutation({
mutation: (todo: Todo) => $fetch(`/api/todos/${todo.id}`, { method: 'DELETE' }),
onMutate(todo) {
const oldTodos = queryCache.getQueryData(todosQuery.key) || []
const todoIndex = oldTodos.findIndex(t => t.id === todo.id)
let newTodos = oldTodos
if (todoIndex >= 0) {
newTodos = oldTodos.toSpliced(todoIndex, 1)
queryCache.setQueryData(todosQuery.key, newTodos)
}
queryCache.cancelQueries({ key: todosQuery.key, exact: true })
return { oldTodos, newTodos }
},
onSettled() {
// always refetch the todos after a mutation
queryCache.invalidateQueries({ key: todosQuery.key, exact: true })
},
onError(err, todo, { oldTodos, newTodos }) {
// oldTodos can be undefined if onMutate errors
if (newTodos != null && newTodos === queryCache.getQueryData(todosQuery.key)) {
queryCache.setQueryData(todosQuery.key, oldTodos)
}
console.error(err)
toast.add({ title: 'Unexpected Error', color: 'error' })
}
})
</script>
<template>
<form
class="flex flex-col gap-4"
@submit.prevent="addTodo(newTodo)"
>
<div class="flex items-center gap-2">
<UInput
v-model="newTodo"
name="todo"
class="flex-1"
placeholder="Make a Nuxt demo"
autocomplete="off"
autofocus
:ui="{ base: 'flex-1' }"
/>
<UButton
type="submit"
icon="i-lucide-plus"
:disabled="newTodo.trim().length === 0"
/>
</div>
<ul class="divide-y divide-gray-200 dark:divide-gray-800">
<li
v-for="todo of todos"
:key="todo.id"
class="flex items-center gap-4 py-2"
>
<span
class="flex-1 font-medium"
:class="{
'text-gray-500': todo.completed || todo.id < 0,
'line-through': todo.completed
}"
>{{ todo.title }}</span>
<USwitch
:model-value="Boolean(todo.completed)"
:disabled="todo.id < 0"
@update:model-value="toggleTodo(todo)"
/>
<UButton
color="error"
variant="soft"
size="xs"
icon="i-lucide-x"
:disabled="todo.id < 0"
@click="deleteTodo(todo)"
/>
</li>
</ul>
</form>
</template>

138
app/pages/todos.vue Normal file
View File

@@ -0,0 +1,138 @@
<script setup lang="ts">
import { todosQuery } from '~/queries/todos'
definePageMeta({
middleware: 'auth'
})
const newTodo = ref('')
const newTodoInput = useTemplateRef('new-todo')
const toast = useToast()
const queryCache = useQueryCache()
const { data: todos } = useQuery(todosQuery)
const { mutate: addTodo, isLoading: loading } = useMutation({
mutation: (title: string) => {
if (!title.trim()) throw new Error('Title is required')
return $fetch('/api/todos', {
method: 'POST',
body: {
title,
completed: 0
}
})
},
async onSuccess(todo) {
await queryCache.invalidateQueries(todosQuery)
toast.add({ title: `Todo "${todo!.title}" created.` })
},
onSettled() {
newTodo.value = ''
// the first nextTick allows loading to become false and re enable the input
// the second nextTick allows the input to be rendered again so it can be focused
// a better solution would be to use a custom `v-focus` directive or a more elaborated focus management solution
nextTick()
.then(() => nextTick())
.then(() => {
newTodoInput.value?.inputRef?.focus()
})
},
onError(err) {
if (isNuxtZodError(err)) {
const title = err.data?.data.issues
.map(issue => issue.message)
.join('\n')
if (title) {
toast.add({ title, color: 'error' })
}
}
else {
console.error(err)
toast.add({ title: 'Unexpected Error', color: 'error' })
}
}
})
const { mutate: toggleTodo } = useMutation({
mutation: (todo: Todo) =>
$fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: {
completed: !todo.completed
}
}),
async onSuccess() {
await queryCache.invalidateQueries(todosQuery)
}
})
const { mutate: deleteTodo } = useMutation({
mutation: (todo: Todo) =>
$fetch(`/api/todos/${todo.id}`, { method: 'DELETE' }),
async onSuccess(_result, todo) {
await queryCache.invalidateQueries(todosQuery)
toast.add({ title: `Todo "${todo.title}" deleted.` })
}
})
</script>
<template>
<form
class="flex flex-col gap-4"
@submit.prevent="addTodo(newTodo)"
>
<div class="flex items-center gap-2">
<UInput
ref="new-todo"
v-model="newTodo"
name="todo"
:disabled="loading"
class="flex-1"
placeholder="Make a Nuxt demo"
autocomplete="off"
autofocus
:ui="{ base: 'flex-1' }"
/>
<UButton
type="submit"
icon="i-lucide-plus"
:loading="loading"
:disabled="newTodo.trim().length === 0"
/>
</div>
<ul class="divide-y divide-gray-200 dark:divide-gray-800">
<li
v-for="todo of todos"
:key="todo.id"
class="flex items-center gap-4 py-2"
>
<span
class="flex-1 font-medium"
:class="[todo.completed ? 'line-through text-gray-500' : '']"
>{{ todo.title }}</span>
<USwitch
:model-value="Boolean(todo.completed)"
@update:model-value="toggleTodo(todo)"
/>
<UButton
color="error"
variant="soft"
size="xs"
icon="i-lucide-x"
@click="deleteTodo(todo)"
/>
</li>
</ul>
</form>
</template>