mirror of
https://github.com/shishantbiswas/bknd-examples.git
synced 2026-02-27 12:01:16 +00:00
init
This commit is contained in:
27
app/assets/css/main.css
Normal file
27
app/assets/css/main.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
font-family: "Geist Mono", monospace;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
13
app/components/Buttons.vue
Normal file
13
app/components/Buttons.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground gap-2 text-white hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://bknd.io/" target="_blank" rel="noopener noreferrer">
|
||||
<img className="grayscale" src="/bknd.ico" alt="bknd logomark" width={20} height={20} />
|
||||
Go To Bknd.io
|
||||
</a>
|
||||
<a className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://docs.bknd.io/integration/nextjs" target="_blank" rel="noopener noreferrer">
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
29
app/components/Footer.vue
Normal file
29
app/components/Footer.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pathname = computed(() => route.path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<NuxtLink class="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
:to="pathname === '/' ? '/user' : '/'">
|
||||
<img aria-hidden src="/file.svg" alt="File icon" width="16" height="16" />
|
||||
{{ pathname === '/' ? 'User' : 'Home' }}
|
||||
</NuxtLink>
|
||||
|
||||
<!-- a tag is required to hit the middleware in place -->
|
||||
<a class="flex items-center gap-2 hover:underline hover:underline-offset-4" href="/admin">
|
||||
<img aria-hidden src="/window.svg" alt="Window icon" width="16" height="16" />
|
||||
Admin
|
||||
</a>
|
||||
|
||||
<a class="flex items-center gap-2 hover:underline hover:underline-offset-4" href="https://bknd.io" target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<img aria-hidden src="/globe.svg" alt="Globe icon" width="16" height="16" />
|
||||
Go to bknd.io →
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
21
app/components/List.vue
Normal file
21
app/components/List.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ items?: any[] }>(), { items: () => [] })
|
||||
|
||||
function isPrimitive(val: unknown): boolean {
|
||||
const t = typeof val
|
||||
return t === 'string' || t === 'number' || t === 'boolean'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol class="list-inside list-decimal text-sm text-center sm:text-left w-full text-center">
|
||||
<li
|
||||
v-for="(item, i) in props.items"
|
||||
:key="i"
|
||||
:class="{ 'mb-2': i < props.items.length - 1 }"
|
||||
>
|
||||
<span v-if="isPrimitive(item)">{{ item }}</span>
|
||||
<component v-else :is="item" />
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
33
app/composables/useTodoActions.ts
Normal file
33
app/composables/useTodoActions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
interface Todo {
|
||||
title: string | undefined;
|
||||
done: boolean | undefined;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const useTodoActions = () => {
|
||||
const fetchTodos = () =>
|
||||
$fetch<{ limit: number; todos: Array<Todo>; total: number }>("/api/todo", {
|
||||
method: "POST",
|
||||
body: { action: "get" },
|
||||
});
|
||||
|
||||
const createTodo = (title: string) =>
|
||||
$fetch("/api/todo", {
|
||||
method: "POST",
|
||||
body: { action: "create", data: { title } },
|
||||
});
|
||||
|
||||
const deleteTodo = (id: number) =>
|
||||
$fetch("/api/todo", {
|
||||
method: "POST",
|
||||
body: { action: "delete", data: { id } },
|
||||
});
|
||||
|
||||
const toggleTodo = (todo: any) =>
|
||||
$fetch("/api/todo", {
|
||||
method: "POST",
|
||||
body: { action: "toggle", data: todo },
|
||||
});
|
||||
|
||||
return { fetchTodos, createTodo, deleteTodo, toggleTodo };
|
||||
};
|
||||
4
app/composables/useUser.ts
Normal file
4
app/composables/useUser.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const useUser = () => {
|
||||
const getUser = () => $fetch("/api/user");
|
||||
return { getUser };
|
||||
};
|
||||
66
app/pages/index.vue
Normal file
66
app/pages/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { fetchTodos, toggleTodo, createTodo, deleteTodo } = useTodoActions();
|
||||
const { data: todos, refresh, status, pending } = await useAsyncData('todos', () => fetchTodos());
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const title = formData.get("title");
|
||||
await createTodo(title as string);
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <div v-if="pending">Loading...</div> -->
|
||||
|
||||
<div v-if="todos !== undefined"
|
||||
class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main class="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
|
||||
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
|
||||
</div>
|
||||
|
||||
<List :items="['Get started with a full backend.', 'Focus on what matters instead of repetition.']" />
|
||||
|
||||
<div class="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
|
||||
<h2 class="font-mono mb-1 opacity-70"><code>What's next?</code></h2>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div v-if="todos.total > todos.limit"
|
||||
class="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
|
||||
{{ todos.total - todos.limit }} more todo(s) hidden
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="todo in todos.todos" :key="String(todo.id)" class="flex flex-row">
|
||||
<div class="flex flex-row flex-grow items-center gap-3 ml-1">
|
||||
<input type="checkbox" class="flex-shrink-0 cursor-pointer" :checked="!!todo.done"
|
||||
@change="() => { toggleTodo(todo); refresh() }" />
|
||||
<div class="text-foreground/90 leading-none">{{ todo.title }}</div>
|
||||
</div>
|
||||
<button type="button" class="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs"
|
||||
@click="() => { deleteTodo(todo.id); refresh() }">
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-row w-full gap-3 mt-2" :key="todos.todos.map(t => t.id).join()" @submit="handleSubmit">
|
||||
<input type="text" name="title" placeholder="New todo"
|
||||
class="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none" />
|
||||
<button type="submit" class="cursor-pointer">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
46
app/pages/user.vue
Normal file
46
app/pages/user.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { getUser } = useUser();
|
||||
const { data, status: userStatus } = await useAsyncData('user', () => getUser());
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="userStatus !== 'pending'"
|
||||
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
|
||||
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
|
||||
<div class="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
|
||||
</div>
|
||||
<div v-if="data?.user">
|
||||
Logged in as {{ data.user.email }}.
|
||||
<a className="font-medium underline" href='/api/auth/logout'>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
<div v-else className="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.
|
||||
<a className="font-medium underline" href="/admin/auth/login">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
Sign in with:
|
||||
<b>
|
||||
<code>test@bknd.io</code>
|
||||
</b>
|
||||
/
|
||||
<b>
|
||||
<code>12345678</code>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user