├── .env.example ├── .github └── dependabot.yml ├── .gitignore ├── README.md ├── app.vue ├── assets └── main.css ├── components ├── AuthorModal.vue ├── BookModal.vue ├── Form │ ├── AuthorInput.vue │ └── Input.vue └── Nav │ └── Header.vue ├── composables ├── authorStore.ts ├── bookStore.ts └── useToast.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── authors │ └── index.vue └── index.vue ├── plugins ├── datatabless.client.ts ├── toast.client.ts └── vee-validate.ts ├── server ├── api │ ├── authors │ │ ├── [id].delete.ts │ │ ├── [id].put.ts │ │ ├── create.post.ts │ │ └── index.ts │ └── books │ │ ├── [id].delete.ts │ │ ├── [id].put.ts │ │ ├── create.post.ts │ │ └── index.ts ├── db │ └── index.ts ├── models │ ├── Author.model.ts │ └── Book.model.ts └── validation.ts ├── tailwind.config.js ├── tsconfig.json └── types └── index.ts /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URI = -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | day: "sunday" 11 | time: "06:00" 12 | timezone: "America/Jamaica" 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 CRUD App 2 | 3 | A Nuxt 3 app that displays how to use [Server Routes & Middleware](https://v3.nuxtjs.org/guide/directory-structure/server) to perform CRUD operations. 4 | 5 | ## Stuff used 6 | 7 | - [Nuxt 3](https://v3.nuxtjs.org) 8 | - [TailwindCSS](https://tailwindcss.com/) 9 | - [Headless UI](https://headlessui.com/) 10 | - [Pinia](https://pinia.vuejs.org/) 11 | - [VeeValidate V4](https://vee-validate.logaretm.com/v4/) 12 | - [MongoDB](https://www.mongodb.com/) 13 | 14 | ## How to start 15 | 16 | 1. Add mongo uri to `.env` file 17 | 1. Install dependencies with `npm install --force` (Force if you get an error abot pinia) 18 | 1. Run dev with `npm run dev` 19 | 1. Build & deploy if you want to `npm run build` 20 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer components { 8 | .btn { 9 | @apply px-4 py-2.5 text-sm flex items-center justify-center 10 | text-white rounded-md font-medium 11 | bg-gradient-to-tr from-primary to-amber-400 hover:shadow-xl 12 | hover:shadow-primary/30 hover:scale-[1.03] 13 | focus:ring-4 focus:ring-primary/30 transition-all duration-300; 14 | } 15 | .input { 16 | @apply w-full border-gray-300 rounded-md focus:border-primary sm:text-sm 17 | focus:ring-4 focus:ring-primary/30 focus:border-primary/50 p-2.5; 18 | } 19 | .label { 20 | @apply inline-block mb-1 text-sm text-gray-600; 21 | } 22 | } 23 | 24 | .eztble { 25 | --easy-table-border: none; 26 | --easy-table-row-border: none; 27 | 28 | --easy-table-header-font-size: 14px; 29 | --easy-table-header-height: 0px; 30 | --easy-table-header-font-color: theme("colors.gray.700"); 31 | --easy-table-header-background-color: theme("colors.gray.50"); 32 | --easy-table-header-item-padding: 16px; 33 | 34 | /* Row & Body */ 35 | --easy-table-body-row-height: 0px; 36 | --easy-table-body-row-font-size: 14px; 37 | --easy-table-body-row-font-color: theme("colors.gray.600"); 38 | --easy-table-body-row-background-color: #fff; 39 | --easy-table-body-row-hover-font-color: theme("colors.gray.700"); 40 | --easy-table-body-row-hover-background-color: theme("colors.gray.100"); 41 | --easy-table-body-even-row-font-color: #373737; 42 | --easy-table-body-even-row-background-color: #fff; 43 | --easy-table-body-item-padding: 18px; 44 | 45 | /* Footer */ 46 | --easy-table-footer-background-color: #fff; 47 | --easy-table-footer-font-color: theme("colors.gray.500"); 48 | --easy-table-footer-font-size: 12px; 49 | --easy-table-footer-padding: 20px 5px; 50 | --easy-table-footer-height: 0px; 51 | --easy-table-rows-per-page-selector-width: auto; 52 | --easy-table-rows-per-page-selector-option-padding: 5px; 53 | 54 | /* Message */ 55 | --easy-table-message-font-color: #212121; /*Empty message related, when no items in table */ 56 | --easy-table-message-font-size: 16px; /*Empty message related, when no items in table */ 57 | --easy-table-message-padding: 30px 0px; /* Empty message related, when no items in table */ 58 | 59 | /* Loader */ 60 | --easy-table-loading-mask-background-color: #fff; 61 | --easy-table-loading-mask-opacity: 0.5; 62 | 63 | /* Scrollbar */ 64 | --easy-table-scrollbar-track-color: #fff; 65 | --easy-table-scrollbar-color: #fff; 66 | --easy-table-scrollbar-thumb-color: theme("colors.gray.200"); 67 | --easy-table-scrollbar-corner-color: #fff; 68 | --easy-table-buttons-pagination-border: 1px solid #e0e0e0; 69 | } 70 | 71 | .eztble th { 72 | @apply font-medium; 73 | } 74 | 75 | /* Toast styles */ 76 | 77 | .Vue-Toastification__toast { 78 | font-family: "Inter" !important; 79 | padding: 20px 17px !important; 80 | } 81 | .Vue-Toastification__toast-body { 82 | font-size: 14px !important; 83 | font-weight: 400 !important; 84 | } 85 | 86 | /* Vue select */ 87 | .vue-select { 88 | width: 100% !important; 89 | border: 1px solid theme("colors.slate.300") !important; 90 | @apply !rounded-md hover:!border-slate-400; 91 | } 92 | .vue-input input { 93 | @apply !p-2 !text-base; 94 | } 95 | 96 | .vue-dropdown { 97 | @apply !border-gray-300; 98 | } 99 | .vue-dropdown-item.highlighted { 100 | @apply !bg-primary-50; 101 | } 102 | 103 | .vue-dropdown-item.selected { 104 | @apply !bg-primary !text-white; 105 | } 106 | 107 | .vue-dropdown-item { 108 | @apply !p-3; 109 | } 110 | .vue-select .vue-tags { 111 | height: 50px; 112 | } 113 | 114 | .vue-tag.selected { 115 | background-color: theme("colors.primary.50") !important; 116 | border-radius: 50px !important; 117 | padding: 0 8px !important; 118 | } 119 | .vue-tag.selected img { 120 | height: 14px !important; 121 | width: 14px !important; 122 | min-height: 10px !important; 123 | min-width: 10px !important; 124 | max-height: 100% !important; 125 | max-width: 100% !important; 126 | margin: 0px 5px !important; 127 | } 128 | /* Transitions */ 129 | 130 | .page-enter-active, 131 | .page-leave-active { 132 | transition: all 0.2s; 133 | } 134 | 135 | .page-enter-from, 136 | .page-leave-to { 137 | opacity: 0; 138 | transform: scale(0.99); 139 | } 140 | -------------------------------------------------------------------------------- /components/AuthorModal.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /components/BookModal.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /components/Form/AuthorInput.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 93 | -------------------------------------------------------------------------------- /components/Form/Input.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /components/Nav/Header.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /composables/authorStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { IAuthor } from "~~/types"; 3 | import useToast from "./useToast"; 4 | 5 | export const useAuthorStore = defineStore("author-store", { 6 | state: () => ({ 7 | authors: [] as IAuthor[], 8 | }), 9 | actions: { 10 | // Get all authors from DB 11 | async getAll() { 12 | try { 13 | let data = await $fetch("/api/authors"); 14 | this.authors = data; 15 | return data as IAuthor[]; 16 | } catch (e) { 17 | useToast().error(e.message); 18 | } 19 | }, 20 | // Create a new author 21 | async create(name: string) { 22 | await $fetch("/api/authors/create", { 23 | method: "POST", 24 | body: { name }, 25 | }) 26 | .catch((e) => { 27 | useToast().error(e.data.message); 28 | }) 29 | .then(async () => { 30 | await this.getAll(); 31 | useToast().success("Author created"); 32 | }); 33 | }, 34 | // Update an author 35 | async update(id: string, name: string) { 36 | await $fetch(`/api/authors/${id}`, { 37 | method: "PUT", 38 | body: { name }, 39 | }) 40 | .catch((e) => { 41 | useToast().error(e.data.message); 42 | }) 43 | .then(async () => { 44 | await this.getAll(); 45 | useToast().success("Author updated"); 46 | }); 47 | }, 48 | // delete an author 49 | async remove(id: string) { 50 | await $fetch(`/api/authors/${id}`, { 51 | method: "DELETE", 52 | }) 53 | .catch((e) => { 54 | useToast().error(e.data.message); 55 | }) 56 | .then(async () => { 57 | await this.getAll(); 58 | useToast().success("Author removed"); 59 | }); 60 | }, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /composables/bookStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { IBook } from "~~/types"; 3 | import useToast from "./useToast"; 4 | 5 | export const useBookStore = defineStore("book-store", { 6 | state: () => ({ 7 | // List of all books 8 | books: [] as IBook[], 9 | }), 10 | actions: { 11 | // Get all books from DB 12 | async getAll() { 13 | try { 14 | let data = await $fetch("/api/books"); 15 | this.books = data; 16 | return data as IBook[]; 17 | } catch (e) { 18 | useToast().error(e.message); 19 | } 20 | }, 21 | // Create a new book 22 | async create(book: IBook) { 23 | await $fetch("/api/books/create", { 24 | method: "POST", 25 | body: book, 26 | }) 27 | .catch((e) => { 28 | useToast().error(e.data.message); 29 | }) 30 | .then(async () => { 31 | await this.getAll(); 32 | useToast().success("Book created"); 33 | }); 34 | }, 35 | // Update a book 36 | async update(id: string, book: IBook) { 37 | await $fetch(`/api/books/${id}`, { 38 | method: "PUT", 39 | body: book, 40 | }) 41 | .catch((e) => { 42 | useToast().error(e.data.message); 43 | }) 44 | .then(async () => { 45 | await this.getAll(); 46 | useToast().success("Book updated"); 47 | }); 48 | }, 49 | // delete a book 50 | async remove(id: string) { 51 | await $fetch(`/api/books/${id}`, { 52 | method: "DELETE", 53 | }) 54 | .catch((e) => { 55 | useToast().error(e.data.message); 56 | }) 57 | .then(async () => { 58 | await this.getAll(); 59 | useToast().success("Book removed"); 60 | }); 61 | }, 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /composables/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useToast as toasty } from "vue-toastification"; 2 | 3 | export default function () { 4 | const toast = toasty(); 5 | // export toast function 6 | return toast; 7 | } 8 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 2 | export default defineNuxtConfig({ 3 | // enable devtools 4 | devtools: { enabled: true }, 5 | 6 | modules: ["@nuxtjs/tailwindcss", "nuxt-icon", "@pinia/nuxt"], 7 | 8 | // custom tailwindcss path 9 | tailwindcss: { 10 | cssPath: "~/assets/main.css", 11 | }, 12 | 13 | // server config variable 14 | runtimeConfig: { 15 | MONGO_URI: process.env.MONGO_URI, 16 | }, 17 | 18 | // register nitro plugin 19 | nitro: { 20 | plugins: ["@/server/db/index.ts"], 21 | }, 22 | 23 | /// transpile afew packages 24 | build: { 25 | transpile: ["@headlessui/vue", "vue-toastification", "@headlessui/tailwindcss"], 26 | }, 27 | 28 | compatibilityDate: "2024-08-18", 29 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "dev:clean": "rm -rf node_modules package-lock.json && npm i --force && nuxt dev", 7 | "generate": "nuxt generate", 8 | "preview": "nuxt preview", 9 | "postinstall": "nuxt prepare" 10 | }, 11 | "author": { 12 | "email": "behon.baker@yahoo.com", 13 | "name": "Behon Baker", 14 | "url": "https://behonbaker.com" 15 | }, 16 | "devDependencies": { 17 | "@nuxtjs/tailwindcss": "^6.12.2", 18 | "nuxt": "^3.14.1592", 19 | "vue": "latest", 20 | "vue-router": "latest" 21 | }, 22 | "dependencies": { 23 | "@headlessui/tailwindcss": "^0.2.1", 24 | "@headlessui/vue": "^1.7.23", 25 | "@pinia/nuxt": "^0.7.0", 26 | "@tailwindcss/forms": "^0.5.9", 27 | "@vee-validate/rules": "^4.14.7", 28 | "dayjs": "^1.11.13", 29 | "joi": "^17.13.3", 30 | "mongoose": "^8.8.2", 31 | "nuxt-icon": "^0.6.10", 32 | "pinia": "^2.2.6", 33 | "vee-validate": "^4.14.7", 34 | "vue-toastification": "next", 35 | "vue3-easy-data-table": "^1.5.47" 36 | }, 37 | "overrides": { 38 | "vue": "latest" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/authors/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 95 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 102 | -------------------------------------------------------------------------------- /plugins/datatabless.client.ts: -------------------------------------------------------------------------------- 1 | import Vue3EasyDataTable from "vue3-easy-data-table"; 2 | import "vue3-easy-data-table/dist/style.css"; 3 | 4 | export default defineNuxtPlugin((app) => { 5 | app.vueApp.component("EasyDataTable", Vue3EasyDataTable); 6 | }); 7 | -------------------------------------------------------------------------------- /plugins/toast.client.ts: -------------------------------------------------------------------------------- 1 | import Toast from "vue-toastification"; 2 | import "vue-toastification/dist/index.css"; 3 | 4 | export default defineNuxtPlugin((app) => { 5 | app.vueApp.use(Toast); 6 | }); 7 | -------------------------------------------------------------------------------- /plugins/vee-validate.ts: -------------------------------------------------------------------------------- 1 | import { defineRule } from "vee-validate"; 2 | import { required, min, min_value, max, max_value } from "@vee-validate/rules"; 3 | 4 | export default defineNuxtPlugin(() => { 5 | defineRule("required", required); 6 | defineRule("min", min); 7 | defineRule("min_value", min_value); 8 | defineRule("max", max); 9 | defineRule("max_value", max_value); 10 | }); 11 | -------------------------------------------------------------------------------- /server/api/authors/[id].delete.ts: -------------------------------------------------------------------------------- 1 | import AuthorModel from "~~/server/models/Author.model"; 2 | import { AuthorSchema } from "~~/server/validation"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // Get id from params 6 | const id = event.context.params.id; 7 | 8 | // Remove author 9 | try { 10 | await AuthorModel.findByIdAndDelete(id); 11 | return { message: "Author removed" }; 12 | } catch (e) { 13 | throw createError({ 14 | message: e.message, 15 | }); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/authors/[id].put.ts: -------------------------------------------------------------------------------- 1 | import AuthorModel from "~~/server/models/Author.model"; 2 | import { AuthorSchema } from "~~/server/validation"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // Get data form body 6 | const body = await readBody(event); 7 | //Get id from params 8 | const id = event.context.params.id; 9 | 10 | // validate 11 | let { value, error } = AuthorSchema.validate(body, { abortEarly: true, allowUnknown: true }); 12 | if (error) { 13 | throw createError({ 14 | message: error.message.replace(/"/g, ""), 15 | statusCode: 400, 16 | fatal: false, 17 | }); 18 | } 19 | 20 | // Update author 21 | try { 22 | await AuthorModel.findByIdAndUpdate(id, body); 23 | return { message: "Author updated" }; 24 | } catch (e) { 25 | throw createError({ 26 | message: e.message, 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /server/api/authors/create.post.ts: -------------------------------------------------------------------------------- 1 | import AuthorModel from "~~/server/models/Author.model"; 2 | import { AuthorSchema } from "~~/server/validation"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // Get data form body 6 | const body = await readBody(event); 7 | 8 | // validate 9 | let { value, error } = AuthorSchema.validate(body); 10 | if (error) { 11 | throw createError({ 12 | message: error.message.replace(/"/g, ""), 13 | statusCode: 400, 14 | fatal: false, 15 | }); 16 | } 17 | 18 | // create author 19 | try { 20 | await AuthorModel.create(body); 21 | return { message: "Author created" }; 22 | } catch (e) { 23 | throw createError({ 24 | message: e.message, 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/authors/index.ts: -------------------------------------------------------------------------------- 1 | import AuthorModel from "~~/server/models/Author.model"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | // return all authors 5 | return await AuthorModel.find(); 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/books/[id].delete.ts: -------------------------------------------------------------------------------- 1 | import BookModel from "~~/server/models/Book.model"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | // Get id from params 5 | const id = event.context.params.id; 6 | 7 | // Remove book 8 | try { 9 | await BookModel.findByIdAndDelete(id); 10 | return { message: "Book removed" }; 11 | } catch (e) { 12 | throw createError({ 13 | message: e.message, 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/books/[id].put.ts: -------------------------------------------------------------------------------- 1 | import BookModel from "~~/server/models/Book.model"; 2 | import { BookSchema } from "~~/server/validation"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // Get data form body 6 | const body = await readBody(event); 7 | // get id from params 8 | const id = event.context.params.id; 9 | 10 | // validate 11 | let { error } = BookSchema.validate(body, { abortEarly: true, allowUnknown: true }); 12 | if (error) { 13 | throw createError({ 14 | message: error.message.replace(/"/g, ""), 15 | statusCode: 400, 16 | fatal: false, 17 | }); 18 | } 19 | 20 | // Update book 21 | try { 22 | await BookModel.findByIdAndUpdate(id, body); 23 | return { message: "Author updated" }; 24 | } catch (e) { 25 | throw createError({ 26 | message: e.message, 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /server/api/books/create.post.ts: -------------------------------------------------------------------------------- 1 | import BookModel from "~~/server/models/Book.model"; 2 | import { BookSchema } from "~~/server/validation"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // Get data form body 6 | const body = await readBody(event); 7 | 8 | // validate 9 | let { error } = BookSchema.validate(body); 10 | if (error) { 11 | throw createError({ 12 | message: error.message.replace(/"/g, ""), 13 | statusCode: 400, 14 | fatal: false, 15 | }); 16 | } 17 | 18 | // create book 19 | try { 20 | await BookModel.create(body); 21 | return { message: "Book created" }; 22 | } catch (e) { 23 | throw createError({ 24 | message: e.message, 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/books/index.ts: -------------------------------------------------------------------------------- 1 | import BookModel from "~~/server/models/Book.model"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | // get all books & populate the authors 5 | return await BookModel.find().populate("authors"); 6 | }); 7 | -------------------------------------------------------------------------------- /server/db/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | // nitro has to be imported as a type, otherwise it will throw an error 3 | import type { Nitro } from "nitropack"; 4 | 5 | // Nitro plugin 6 | // Thanks to https://github.com/UnderKoen for the answer to this 7 | // https://github.com/nuxt/framework/discussions/4923 8 | export default async (_nitroApp: Nitro) => { 9 | //run your connect code here 10 | const config = useRuntimeConfig(); 11 | // connect to mongodb 12 | mongoose 13 | .connect(config.MONGO_URI) 14 | .then(() => console.log(`Connected to DB`)) 15 | .catch((e) => console.log(e)); 16 | }; 17 | -------------------------------------------------------------------------------- /server/models/Author.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | // author schema 4 | const schema: mongoose.Schema = new mongoose.Schema( 5 | { 6 | name: { 7 | type: String, 8 | requied: true, 9 | }, 10 | }, 11 | { timestamps: true } 12 | ); 13 | 14 | // author model 15 | export default mongoose.model("Author", schema); 16 | -------------------------------------------------------------------------------- /server/models/Book.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | // book schema 4 | const schema: mongoose.Schema = new mongoose.Schema( 5 | { 6 | title: { 7 | type: String, 8 | requied: true, 9 | }, 10 | published: { 11 | type: Date, 12 | required: true, 13 | }, 14 | isbn: String, 15 | authors: [ 16 | { 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: "Author", 19 | }, 20 | ], 21 | pageCount: Number, 22 | }, 23 | { timestamps: true } 24 | ); 25 | 26 | // book model 27 | export default mongoose.model("Book", schema); 28 | -------------------------------------------------------------------------------- /server/validation.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | // author validation 4 | export const AuthorSchema = Joi.object({ 5 | name: Joi.string().min(3).required(), 6 | }); 7 | 8 | // book validation 9 | export const BookSchema = Joi.object({ 10 | title: Joi.string().min(3).required(), 11 | isbn: Joi.string().min(3).required(), 12 | authors: Joi.array(), 13 | published: Joi.date().required(), 14 | pageCount: Joi.number(), 15 | }); 16 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("tailwindcss/colors"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ["Inter"], 10 | }, 11 | colors: { 12 | primary: { 13 | DEFAULT: colors.orange[500], 14 | ...colors.orange, 15 | }, 16 | }, 17 | }, 18 | }, 19 | plugins: [require("@tailwindcss/forms"), require("@headlessui/tailwindcss")], 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IBook { 2 | _id?: string; 3 | title: string; 4 | isbn: string; 5 | pageCount: number; 6 | published: string; 7 | author: IAuthor; 8 | } 9 | 10 | export interface IAuthor { 11 | name: string; 12 | _id?: string; 13 | } 14 | --------------------------------------------------------------------------------