├── public ├── robots.txt └── favicon.ico ├── .prettierrc ├── server ├── tsconfig.json ├── database │ ├── migrations │ │ ├── 0000_sad_alice.sql │ │ └── meta │ │ │ ├── _journal.json │ │ │ └── 0000_snapshot.json │ └── schema.ts ├── utils │ ├── useDatabase.ts │ └── useStreamToBlob.ts └── api │ ├── audios │ └── [id].get.ts │ └── generations │ ├── index.get.ts │ └── index.post.ts ├── app.vue ├── tsconfig.json ├── entities └── Generation.ts ├── assets └── css │ ├── tailwind.css │ └── preset.ts ├── pages ├── sign-in.vue ├── sign-up.vue ├── index.vue └── generations │ ├── index.vue │ └── new.vue ├── .gitignore ├── layouts ├── admin.vue ├── default.vue └── auth.vue ├── drizzle.config.ts ├── components ├── Onboarding.vue ├── Hero.vue ├── Header.vue └── Generation.vue ├── eslint.config.mjs ├── .env.example ├── tailwind.config.js ├── composables ├── useGenerations.ts ├── useAudioSignedUrl.ts └── useGenerationCreate.ts ├── nuxt.config.ts ├── README.md └── package.json /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckyhackersacademy/extreme-week2/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /entities/Generation.ts: -------------------------------------------------------------------------------- 1 | export interface Generation { 2 | id: string; 3 | userId: string; 4 | title: string; 5 | content: string; 6 | audioId: string; 7 | createdAt: string; 8 | } -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @layer tailwind-base, primevue, tailwind-utilities; 2 | 3 | @layer tailwind-base { 4 | @tailwind base; 5 | } 6 | 7 | @layer tailwind-utilities { 8 | @tailwind components; 9 | @tailwind utilities; 10 | } 11 | -------------------------------------------------------------------------------- /pages/sign-in.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /pages/sign-up.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /server/database/migrations/0000_sad_alice.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `generations` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `user_id` text NOT NULL, 4 | `title` text NOT NULL, 5 | `content` text NOT NULL, 6 | `audio_id` text NOT NULL, 7 | `created_at` integer DEFAULT (CURRENT_DATE) NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /server/database/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1728226081048, 9 | "tag": "0000_sad_alice", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /layouts/admin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | 3 | export default { 4 | schema: './server/database/schema.ts', 5 | out: './server/database/migrations', 6 | driver: 'turso', 7 | dialect: 'sqlite', 8 | dbCredentials: { 9 | url: process.env.TURSO_DB_URL!, 10 | authToken: process.env.TURSO_DB_TOKEN!, 11 | }, 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /components/Onboarding.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; 2 | 3 | export default createConfigForNuxt({ 4 | root: true, 5 | env: { 6 | browser: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | '@nuxtjs/eslint-config-typescript', 11 | 'eslint:recommended', 12 | 'plugin:vue/recommended', 13 | 'plugin:prettier/recommended', 14 | 'plugin:import/typescript', 15 | ], 16 | plugins: ['vue'] 17 | }); 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TURSO_DB_URL= 2 | TURSO_DB_TOKEN= 3 | 4 | PINATA_JWT= 5 | PINATA_GATEWAY_URL= 6 | 7 | ELEVENLABS_API_KEY= 8 | 9 | NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 10 | NUXT_CLERK_SECRET_KEY= 11 | 12 | CLERK_SIGN_IN_FORCE_REDIRECT_URL=http://localhost:3000/generations 13 | CLERK_SIGN_UP_FORCE_REDIRECT_URL=http://localhost:3000/generations 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./components/**/*.{js,vue,ts}", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.{js,ts}", 8 | "./app.vue", 9 | "./error.vue", 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Inter'] 15 | } 16 | }, 17 | }, 18 | plugins: [require('tailwindcss-primeui')] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /server/database/schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; 3 | 4 | export const generations = sqliteTable('generations', { 5 | id: text('id').primaryKey(), 6 | userId: text('user_id').notNull(), 7 | title: text('title').notNull(), 8 | content: text('content').notNull(), 9 | audioId: text('audio_id').notNull(), 10 | createdAt: integer('created_at', { mode: 'timestamp' }) 11 | .notNull() 12 | .default(sql`(CURRENT_DATE)`), 13 | }); 14 | -------------------------------------------------------------------------------- /layouts/auth.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /server/utils/useDatabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@libsql/client/http'; 2 | import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'; 3 | 4 | export * as tables from '~/server/database/schema'; 5 | 6 | let database: LibSQLDatabase | null = null; 7 | 8 | export const useDatabase = () => { 9 | const { tursoDBURL, tursoDBToken } = useRuntimeConfig(); 10 | 11 | if (tursoDBToken && tursoDBURL) { 12 | database = drizzle( 13 | createClient({ 14 | url: tursoDBURL, 15 | authToken: tursoDBToken, 16 | }), 17 | ); 18 | } 19 | 20 | return database; 21 | }; 22 | -------------------------------------------------------------------------------- /server/api/audios/[id].get.ts: -------------------------------------------------------------------------------- 1 | import { PinataSDK } from 'pinata'; 2 | 3 | export default eventHandler(async (event) => { 4 | const config = useRuntimeConfig(); 5 | const cid = getRouterParam(event, 'id'); 6 | 7 | if (!cid) { 8 | throw createError({ 9 | statusCode: 400, 10 | statusMessage: 'Bad Request', 11 | }); 12 | } 13 | 14 | const pinata = new PinataSDK({ 15 | pinataJwt: config.pinataJwt, 16 | pinataGateway: config.pinataGateway, 17 | }); 18 | 19 | const url = await pinata.gateways.createSignedURL({ 20 | cid, 21 | expires: 3600, 22 | }); 23 | 24 | return { 25 | url, 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /assets/css/preset.ts: -------------------------------------------------------------------------------- 1 | import { definePreset } from '@primevue/themes'; 2 | import Aura from '@primevue/themes/aura'; 3 | 4 | const preset = definePreset(Aura, { 5 | semantic: { 6 | primary: { 7 | 50: '{indigo.50}', 8 | 100: '{indigo.100}', 9 | 200: '{indigo.200}', 10 | 300: '{indigo.300}', 11 | 400: '{indigo.400}', 12 | 500: '{indigo.500}', 13 | 600: '{indigo.600}', 14 | 700: '{indigo.700}', 15 | 800: '{indigo.800}', 16 | 900: '{indigo.900}', 17 | 950: '{indigo.950}' 18 | } 19 | } 20 | }); 21 | 22 | export default { 23 | preset, 24 | options: { 25 | darkModeSelector: '.p-dark' 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/utils/useStreamToBlob.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | 3 | export const useStreamToBlob = () => { 4 | const toBlob = async (readableStream: Readable): Promise => { 5 | const chunks: Buffer[] = []; 6 | 7 | // Read the stream chunk by chunk 8 | for await (const chunk of readableStream) { 9 | chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); 10 | } 11 | 12 | // Combine all chunks into one Buffer 13 | const buffer = Buffer.concat(chunks); 14 | 15 | // Create a Blob from the Buffer 16 | const blob = new Blob([buffer], { type: 'audio/mpeg' }); 17 | 18 | return blob; 19 | }; 20 | 21 | return { 22 | toBlob, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /server/api/generations/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getAuth } from '#clerk'; 2 | import { desc, eq } from 'drizzle-orm'; 3 | 4 | export default eventHandler(async (event) => { 5 | const { userId } = getAuth(event); 6 | if (!userId) { 7 | throw createError({ 8 | statusCode: 401, 9 | statusMessage: 'Unauthorized', 10 | }); 11 | } 12 | 13 | const db = useDatabase(); 14 | if (!db) { 15 | throw createError({ 16 | statusCode: 500, 17 | statusMessage: 'Internal Server Error', 18 | }); 19 | } 20 | 21 | const generations = await db 22 | .select() 23 | .from(tables.generations) 24 | .where(eq(tables.generations.userId, userId)) 25 | .orderBy(desc(tables.generations.createdAt)) 26 | .offset(0) 27 | .limit(10000); 28 | 29 | return generations; 30 | }); 31 | -------------------------------------------------------------------------------- /composables/useGenerations.ts: -------------------------------------------------------------------------------- 1 | import type { Generation } from '@/entities/Generation'; 2 | 3 | export const useGenerations = () => { 4 | const loading = ref(true); 5 | const generations = ref([]); 6 | 7 | const fetchGenerations = async () => { 8 | try { 9 | const response = await $fetch('/api/generations'); 10 | generations.value = response; 11 | } catch (e) { 12 | console.error(e); 13 | } finally { 14 | loading.value = false; 15 | } 16 | }; 17 | 18 | const isEmpty = computed(() => { 19 | return generations.value.length === 0; 20 | }); 21 | 22 | onMounted(() => { 23 | fetchGenerations(); 24 | }); 25 | 26 | return { 27 | loading, 28 | isEmpty, 29 | generations, 30 | refetch: fetchGenerations, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /composables/useAudioSignedUrl.ts: -------------------------------------------------------------------------------- 1 | export const useAudioSignedUrl = () => { 2 | const loadings = ref>({}); 3 | const url = ref(); 4 | 5 | const fetchAudioSignedUrl = async (id: string) => { 6 | loadings.value[id] = true; 7 | 8 | try { 9 | const response = await $fetch<{ url: string }>(`/api/audios/${id}`); 10 | url.value = response.url; 11 | } catch (e) { 12 | console.error(e); 13 | } finally { 14 | loadings.value[id] = false; 15 | } 16 | }; 17 | 18 | const download = async (title: string) => { 19 | if (!url.value) { 20 | return; 21 | } 22 | 23 | const a = document.createElement('a'); 24 | a.href = url.value; 25 | a.download = `${title}.mp3`; 26 | a.click(); 27 | }; 28 | 29 | return { 30 | loadings, 31 | download, 32 | fetchAudioSignedUrl, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-04-03', 4 | devtools: { enabled: true }, 5 | 6 | css: ['primeicons/primeicons.css'], 7 | 8 | app: { 9 | head: { 10 | title: '🔈 Lombardi.ai', 11 | }, 12 | }, 13 | 14 | modules: [ 15 | '@primevue/nuxt-module', 16 | '@nuxtjs/tailwindcss', 17 | '@nuxt/fonts', 18 | '@vueuse/nuxt', 19 | 'vue-clerk/nuxt', 20 | '@nuxt/eslint', 21 | ], 22 | 23 | runtimeConfig: { 24 | tursoDBURL: process.env.TURSO_DB_URL, 25 | tursoDBToken: process.env.TURSO_DB_TOKEN, 26 | 27 | pinataJwt: process.env.PINATA_JWT, 28 | pinataGateway: process.env.PINATA_GATEWAY_URL, 29 | 30 | elevenlabsApiKey: process.env.ELEVENLABS_API_KEY, 31 | 32 | nodeEnv: process.env.NODE_ENV, 33 | }, 34 | 35 | primevue: { 36 | importTheme: { from: './assets/css/preset.ts' }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /composables/useGenerationCreate.ts: -------------------------------------------------------------------------------- 1 | import type { Generation } from "~/entities/Generation"; 2 | 3 | export const useGenerationCreate = () => { 4 | const loading = ref(false); 5 | const generation = ref() 6 | 7 | const title = ref(''); 8 | const content = ref(''); 9 | 10 | const create = async () => { 11 | if (!title.value || !content.value) { 12 | return 13 | } 14 | 15 | loading.value = true; 16 | 17 | try { 18 | 19 | const response = await $fetch('/api/generations', { 20 | method: 'POST', 21 | body: { 22 | title: title.value, 23 | content: content.value, 24 | } 25 | }) 26 | 27 | generation.value = response 28 | } catch (e) { 29 | console.error(e) 30 | } finally { 31 | loading.value = false; 32 | } 33 | } 34 | 35 | return { 36 | loading, 37 | title, 38 | content, 39 | generation, 40 | create, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/Hero.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `Extreme Week 2` 2 | 3 | Projeto final para a Extreme Week 2! 4 | 5 | ## Usando o boilerplate 6 | 7 | 👉🏻 [Boilerplate nessa branch](https://github.com/luckyhackersacademy/extreme-week2/tree/boilerplate) 8 | 9 | ``` 10 | # clone o repo 11 | git clone https://github.com/luckyhackersacademy/extreme-week2.git 12 | 13 | # entre na pasta 14 | cd extreme-week2 15 | 16 | # troque para a branch do boilerplate 17 | git checkout boilerplate 18 | ``` 19 | 20 | ## Tech Stack 21 | 22 | - Nuxt 3 23 | - Clerk 24 | - Pinata 25 | - Turso 26 | - ElevenLabs 27 | - Tailwind 28 | - PrimeVue 29 | 30 | ## Variavéis de ambiente 31 | 32 | ``` 33 | TURSO_DB_URL= 34 | TURSO_DB_TOKEN= 35 | 36 | PINATA_JWT= 37 | PINATA_GATEWAY_URL= 38 | 39 | ELEVENLABS_API_KEY= 40 | 41 | NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 42 | NUXT_CLERK_SECRET_KEY= 43 | 44 | CLERK_SIGN_IN_FORCE_REDIRECT_URL=http://localhost:3000/generations 45 | CLERK_SIGN_UP_FORCE_REDIRECT_URL=http://localhost:3000/generations 46 | ``` 47 | 48 | ## Rodando localmente 49 | 50 | Para rodar o projeto localmente, execute os seguintes comandos: 51 | 52 | ```bash 53 | pnpm install 54 | pnpm dev 55 | ``` 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@clerk/backend": "^1.13.8", 14 | "@libsql/client": "^0.14.0", 15 | "@nuxt/eslint": "^0.5.7", 16 | "@nuxt/fonts": "^0.9.2", 17 | "@nuxtjs/tailwindcss": "^6.12.1", 18 | "@primevue/themes": "^4.0.7", 19 | "dayjs": "^1.11.13", 20 | "drizzle-kit": "^0.24.2", 21 | "drizzle-orm": "^0.33.0", 22 | "elevenlabs": "^0.16.1", 23 | "nuxt": "^3.13.0", 24 | "pinata": "^1.5.0", 25 | "primeicons": "^7.0.0", 26 | "primevue": "^4.0.7", 27 | "tailwindcss-primeui": "^0.3.4", 28 | "uuid": "^10.0.0", 29 | "vue": "latest", 30 | "vue-clerk": "^0.6.17", 31 | "vue-router": "latest" 32 | }, 33 | "devDependencies": { 34 | "@primevue/nuxt-module": "^4.0.7", 35 | "@types/uuid": "^10.0.0", 36 | "@vueuse/core": "^11.1.0", 37 | "@vueuse/nuxt": "^11.1.0", 38 | "autoprefixer": "^10.4.20", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-prettier": "^5.2.1", 41 | "postcss": "^8.4.47", 42 | "prettier": "^3.3.3", 43 | "tailwindcss": "^3.4.13", 44 | "typescript": "^5.6.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 57 | -------------------------------------------------------------------------------- /components/Generation.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 61 | -------------------------------------------------------------------------------- /server/api/generations/index.post.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { getAuth } from '#clerk'; 3 | import { PinataSDK } from 'pinata'; 4 | import { ElevenLabsClient, ElevenLabs } from 'elevenlabs'; 5 | 6 | interface Request { 7 | title: string; 8 | content: string; 9 | } 10 | 11 | export default eventHandler(async (event) => { 12 | const { userId } = getAuth(event); 13 | if (!userId) { 14 | throw createError({ 15 | statusCode: 401, 16 | statusMessage: 'Unauthorized', 17 | }); 18 | } 19 | 20 | const db = useDatabase(); 21 | if (!db) { 22 | throw createError({ 23 | statusCode: 500, 24 | statusMessage: 'Internal Server Error', 25 | }); 26 | } 27 | 28 | const { toBlob } = useStreamToBlob(); 29 | const config = useRuntimeConfig(); 30 | const payload = await readBody(event); 31 | const id = v4(); 32 | 33 | const pinata = new PinataSDK({ 34 | pinataJwt: config.pinataJwt, 35 | pinataGateway: config.pinataGateway, 36 | }); 37 | 38 | const elevenLabs = new ElevenLabsClient({ 39 | apiKey: config.elevenlabsApiKey, 40 | }); 41 | 42 | const audio = await elevenLabs.generate({ 43 | voice: 'iP95p4xoKVk53GoZ742B', 44 | text: payload.content, 45 | output_format: ElevenLabs.OutputFormat.Mp32205032, 46 | model_id: 'eleven_multilingual_v2', 47 | }); 48 | 49 | const blob = await toBlob(audio); 50 | const file = new File([blob], `${userId}-${id}`, { type: 'audio/mpeg' }); 51 | const upload = await pinata.upload.file(file); 52 | 53 | const generation = { 54 | id, 55 | userId, 56 | title: payload.title, 57 | content: payload.content, 58 | audioId: upload.cid, 59 | createdAt: new Date(), 60 | }; 61 | 62 | await db.insert(tables.generations).values(generation); 63 | 64 | return generation; 65 | }); 66 | -------------------------------------------------------------------------------- /server/database/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "98b48a15-79f5-4291-a28e-65cd6d2fa0ff", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "generations": { 8 | "name": "generations", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "user_id": { 18 | "name": "user_id", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "title": { 25 | "name": "title", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | }, 31 | "content": { 32 | "name": "content", 33 | "type": "text", 34 | "primaryKey": false, 35 | "notNull": true, 36 | "autoincrement": false 37 | }, 38 | "audio_id": { 39 | "name": "audio_id", 40 | "type": "text", 41 | "primaryKey": false, 42 | "notNull": true, 43 | "autoincrement": false 44 | }, 45 | "created_at": { 46 | "name": "created_at", 47 | "type": "integer", 48 | "primaryKey": false, 49 | "notNull": true, 50 | "autoincrement": false, 51 | "default": "(CURRENT_DATE)" 52 | } 53 | }, 54 | "indexes": {}, 55 | "foreignKeys": {}, 56 | "compositePrimaryKeys": {}, 57 | "uniqueConstraints": {} 58 | } 59 | }, 60 | "enums": {}, 61 | "_meta": { 62 | "schemas": {}, 63 | "tables": {}, 64 | "columns": {} 65 | }, 66 | "internal": { 67 | "indexes": {} 68 | } 69 | } -------------------------------------------------------------------------------- /pages/generations/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 62 | -------------------------------------------------------------------------------- /pages/generations/new.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 |