init
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM node:25-alpine3.22 AS base
|
||||
RUN apk add --no-cache libc6-compat wget curl
|
||||
RUN npm install -g bun
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN bun run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3000 \
|
||||
HOSTNAME="0.0.0.0" \
|
||||
NODE_OPTIONS="--max-old-space-size=256"
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 bkndapp
|
||||
|
||||
COPY --from=builder --chown=bkndapp:nodejs /app/.output ./.output
|
||||
USER bkndapp
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "node",".output/server/index.mjs" ]
|
||||
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
||||
export const useUser = () => {
|
||||
const getUser = () => $fetch("/api/user");
|
||||
return { getUser };
|
||||
};
|
||||
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
@@ -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>
|
||||
55
bknd.config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { em, entity, text, boolean, libsql } from "bknd";
|
||||
import { RuntimeBkndConfig } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
|
||||
const local = registerLocalMediaAdapter();
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
// register your schema to get automatic type completion
|
||||
type Database = (typeof schema)["DB"];
|
||||
declare module "bknd" {
|
||||
interface DB extends Database {}
|
||||
}
|
||||
|
||||
export default {
|
||||
connection: libsql({
|
||||
url: process.env.DATABASE_URL || "http://localhost:8080",
|
||||
}),
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
// create some entries
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
|
||||
// and create a user
|
||||
await ctx.app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "random_gibberish_please_change_this",
|
||||
},
|
||||
},
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: local({
|
||||
path: "./public/uploads",
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies RuntimeBkndConfig;
|
||||
28
docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
DATABASE_URL: http://db:8080
|
||||
depends_on:
|
||||
- db
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:3000" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
volumes:
|
||||
- "./data:/app/data"
|
||||
db:
|
||||
image: ghcr.io/tursodatabase/libsql-server
|
||||
volumes:
|
||||
- libsql:/var/lib/sqld
|
||||
|
||||
volumes:
|
||||
libsql:
|
||||
11
nuxt.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: false },
|
||||
modules: ["@nuxtjs/tailwindcss"],
|
||||
app: {
|
||||
head: {
|
||||
title: "Nuxt 🤝 Bknd.io",
|
||||
},
|
||||
},
|
||||
});
|
||||
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "nuxbknd",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"prebuild": "bknd copy-assets --out public/admin",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/vite-dev-server": "^0.25.0",
|
||||
"@nuxtjs/google-fonts": "3.2.0",
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"bknd": "^0.20.0",
|
||||
"hono": "^4.11.9",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^4.6.4"
|
||||
}
|
||||
}
|
||||
BIN
public/bknd.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
14
public/bknd.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
width="578"
|
||||
height="188"
|
||||
viewBox="0 0 578 188"
|
||||
fill="black"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
|
||||
/>
|
||||
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
public/nuxt.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#00DC82"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
26
server/api/todo.post.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getApi } from "../utils/bknd";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const { action, data } = body;
|
||||
const api = await getApi({});
|
||||
|
||||
switch (action) {
|
||||
case 'get':
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
|
||||
return { total: todos.body.meta.total, todos, limit };
|
||||
|
||||
case 'create':
|
||||
return await api.data.createOne("todos", { title: data.title });
|
||||
|
||||
case 'delete':
|
||||
return await api.data.deleteOne("todos", data.id);
|
||||
|
||||
case 'toggle':
|
||||
return await api.data.updateOne("todos", data.id, { done: !data.done });
|
||||
|
||||
default:
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid Action" });
|
||||
}
|
||||
});
|
||||
7
server/api/user.get.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getApi } from "../utils/bknd";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const api = await getApi({ verify: true, headers: event.headers });
|
||||
const user = api.getUser();
|
||||
return { user };
|
||||
});
|
||||
44
server/middleware/bknd.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type RuntimeBkndConfig } from "bknd/adapter";
|
||||
import config from "../../bknd.config";
|
||||
import { getApp } from "../utils/bknd";
|
||||
|
||||
function serve(config: RuntimeBkndConfig) {
|
||||
return async (request: Request) => {
|
||||
const app = await getApp(config, process.env);
|
||||
return app.fetch(request);
|
||||
};
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const request = toWebRequest(event);
|
||||
const url = new URL(request.url);
|
||||
const isMethodWithBody = ["POST", "PUT", "PATCH", "DELETE"].includes(
|
||||
request.method,
|
||||
);
|
||||
|
||||
const adminBasepath = config.adminOptions.adminBasepath;
|
||||
// if (adminBasepath.endsWith("/")) adminBasepath.slice(0, -1);
|
||||
|
||||
if (
|
||||
url.pathname.startsWith("/api") ||
|
||||
url.pathname.startsWith(adminBasepath)
|
||||
) {
|
||||
if (url.pathname === adminBasepath + "/") {
|
||||
url.pathname = url.pathname.slice(0, -1);
|
||||
} else {
|
||||
url.pathname = url.pathname.replaceAll("//", "/");
|
||||
}
|
||||
|
||||
const modifiedRequest = new Request(url.toString(), {
|
||||
method: request.method,
|
||||
headers: request.headers as HeadersInit,
|
||||
// @ts-expect-error - 'duplex' is required for streaming bodies in Node.js
|
||||
duplex: isMethodWithBody ? "half" : undefined,
|
||||
body: isMethodWithBody ? request.body : undefined,
|
||||
});
|
||||
const res = await serve(config)(modifiedRequest);
|
||||
if (res && res.status !== 404) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
});
|
||||
27
server/utils/bknd.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
|
||||
import bkndConfig from "../../bknd.config";
|
||||
|
||||
export async function getApp<Env = NodeJS.ProcessEnv>(
|
||||
config: RuntimeBkndConfig<Env>,
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return await createRuntimeApp(config, args);
|
||||
}
|
||||
|
||||
export async function getApi({
|
||||
headers,
|
||||
verify,
|
||||
}: {
|
||||
verify?: boolean;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const app = await getApp(bkndConfig, process.env);
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
}
|
||||
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||