├── .eslintrc.json ├── .gitignore ├── README.md ├── drizzle.config.ts ├── drizzle ├── 0000_complete_proudstar.sql ├── meta │ ├── 0000_snapshot.json │ └── _journal.json └── schema.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ ├── quotes │ │ │ └── route.ts │ │ └── randomquote │ │ │ └── [id] │ │ │ └── route.ts │ ├── components │ │ └── Quote.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── db │ ├── config.ts │ └── schema.ts └── lib │ ├── getAllQuotes.ts │ └── getRandomQuote.ts ├── tailwind.config.js ├── tsconfig.json └── types.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "Next.js - What is Cold Boot Duration" 2 | 3 | ## With a Next.js Project Refactor 4 | 5 | --- 6 | 7 | ### Author Links 8 | 9 | 👋 Hello, I'm Dave Gray. 10 | 11 | 👉 [My Courses](https://courses.davegray.codes/) 12 | 13 | ✅ [Check out my YouTube Channel with hundreds of tutorials](https://www.youtube.com/DaveGrayTeachesCode). 14 | 15 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn) 16 | 17 | ☕ [Buy Me A Coffee](https://buymeacoffee.com/DaveGray) 18 | 19 | 🚀 Follow Me: 20 | 21 | - [Twitter](https://twitter.com/yesdavidgray) 22 | - [LinkedIn](https://www.linkedin.com/in/davidagray/) 23 | - [Blog](https://yesdavidgray.com) 24 | - [Reddit](https://www.reddit.com/user/DaveOnEleven) 25 | 26 | --- 27 | 28 | ### Description 29 | 30 | 📺 [YouTube Video](https://youtu.be/NWc7FU_qQ8g) for this repository. 31 | 32 | 📺 [Original Video](https://youtu.be/d7XJjQesDtE) for this project. 33 | 34 | --- 35 | 36 | ### 🎓 Academic Honesty 37 | 38 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiarism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/). 39 | 40 | --- 41 | 42 | ### ⚙ Free Web Dev Tools 43 | - 🔗 [Google Chrome Web Browser](https://google.com/chrome/) 44 | - 🔗 [Visual Studio Code (aka VS Code)](https://code.visualstudio.com/) 45 | - 🔗 [ES7 React Snippets](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets) 46 | - 🔗 [Multiple Cursor Case Preserve](https://marketplace.visualstudio.com/items?itemName=Cardinal90.multi-cursor-case-preserve) 47 | 48 | ### 📚 References 49 | - 🔗 [Next.js Official Site](https://nextjs.org/) 50 | - 🔗 [Warm a Vercel-hosted Next.js website with Cloudflare workers](https://awstip.com/keeping-a-vercel-hosted-next-js-website-warm-with-cloudflare-workers-b977633ec985) 51 | 52 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import dotenv from 'dotenv' 3 | dotenv.config({ path: '.env.local' }) 4 | 5 | export default { 6 | schema: "./src/db/*", 7 | out: "./drizzle", 8 | connectionString: process.env.DB_URL, 9 | } satisfies Config -------------------------------------------------------------------------------- /drizzle/0000_complete_proudstar.sql: -------------------------------------------------------------------------------- 1 | -- Current sql file was generated after introspecting the database 2 | -- If you want to run this migration please uncomment this code before executing migrations 3 | /* 4 | CREATE TABLE `authors` ( 5 | `id` int AUTO_INCREMENT PRIMARY KEY NOT NULL, 6 | `author` varchar(255) NOT NULL); 7 | 8 | CREATE TABLE `categories` ( 9 | `id` int AUTO_INCREMENT PRIMARY KEY NOT NULL, 10 | `category` varchar(255) NOT NULL); 11 | 12 | CREATE TABLE `quotes` ( 13 | `id` int AUTO_INCREMENT PRIMARY KEY NOT NULL, 14 | `author_id` int NOT NULL, 15 | `category_id` int NOT NULL, 16 | `quote` text NOT NULL); 17 | 18 | CREATE INDEX `author_id_idx` ON `quotes` (`author_id`); 19 | CREATE INDEX `category_id_idx` ON `quotes` (`category_id`); 20 | */ -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "00000000-0000-0000-0000-000000000000", 3 | "prevId": "", 4 | "version": "5", 5 | "dialect": "mysql", 6 | "tables": { 7 | "authors": { 8 | "name": "authors", 9 | "columns": { 10 | "id": { 11 | "autoincrement": true, 12 | "name": "id", 13 | "type": "int", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "author": { 18 | "autoincrement": false, 19 | "name": "author", 20 | "type": "varchar(255)", 21 | "primaryKey": false, 22 | "notNull": true 23 | } 24 | }, 25 | "compositePrimaryKeys": {}, 26 | "indexes": {}, 27 | "foreignKeys": {} 28 | }, 29 | "categories": { 30 | "name": "categories", 31 | "columns": { 32 | "id": { 33 | "autoincrement": true, 34 | "name": "id", 35 | "type": "int", 36 | "primaryKey": true, 37 | "notNull": true 38 | }, 39 | "category": { 40 | "autoincrement": false, 41 | "name": "category", 42 | "type": "varchar(255)", 43 | "primaryKey": false, 44 | "notNull": true 45 | } 46 | }, 47 | "compositePrimaryKeys": {}, 48 | "indexes": {}, 49 | "foreignKeys": {} 50 | }, 51 | "quotes": { 52 | "name": "quotes", 53 | "columns": { 54 | "id": { 55 | "autoincrement": true, 56 | "name": "id", 57 | "type": "int", 58 | "primaryKey": true, 59 | "notNull": true 60 | }, 61 | "author_id": { 62 | "autoincrement": false, 63 | "name": "author_id", 64 | "type": "int", 65 | "primaryKey": false, 66 | "notNull": true 67 | }, 68 | "category_id": { 69 | "autoincrement": false, 70 | "name": "category_id", 71 | "type": "int", 72 | "primaryKey": false, 73 | "notNull": true 74 | }, 75 | "quote": { 76 | "autoincrement": false, 77 | "name": "quote", 78 | "type": "text", 79 | "primaryKey": false, 80 | "notNull": true 81 | } 82 | }, 83 | "compositePrimaryKeys": {}, 84 | "indexes": { 85 | "author_id_idx": { 86 | "name": "author_id_idx", 87 | "columns": [ 88 | "author_id" 89 | ], 90 | "isUnique": false 91 | }, 92 | "category_id_idx": { 93 | "name": "category_id_idx", 94 | "columns": [ 95 | "category_id" 96 | ], 97 | "isUnique": false 98 | } 99 | }, 100 | "foreignKeys": {} 101 | } 102 | }, 103 | "schemas": {}, 104 | "_meta": { 105 | "schemas": {}, 106 | "tables": {}, 107 | "columns": {} 108 | } 109 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1686867196760, 9 | "tag": "0000_complete_proudstar", 10 | "breakpoints": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /drizzle/schema.ts: -------------------------------------------------------------------------------- 1 | import { mysqlTable, mysqlSchema, AnyMySqlColumn, int, varchar, index, text } from "drizzle-orm/mysql-core" 2 | import { sql } from "drizzle-orm" 3 | 4 | 5 | export const authors = mysqlTable("authors", { 6 | id: int("id").autoincrement().primaryKey().notNull(), 7 | author: varchar("author", { length: 255 }).notNull(), 8 | }); 9 | 10 | export const categories = mysqlTable("categories", { 11 | id: int("id").autoincrement().primaryKey().notNull(), 12 | category: varchar("category", { length: 255 }).notNull(), 13 | }); 14 | 15 | export const quotes = mysqlTable("quotes", { 16 | id: int("id").autoincrement().primaryKey().notNull(), 17 | authorId: int("author_id").notNull(), 18 | categoryId: int("category_id").notNull(), 19 | quote: text("quote").notNull(), 20 | }, 21 | (table) => { 22 | return { 23 | authorIdIdx: index("author_id_idx").on(table.authorId), 24 | categoryIdIdx: index("category_id_idx").on(table.categoryId), 25 | } 26 | }); -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rqm-refactor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "introspect": "drizzle-kit introspect:mysql" 11 | }, 12 | "dependencies": { 13 | "@planetscale/database": "^1.7.0", 14 | "@types/node": "20.3.1", 15 | "@types/react": "18.2.12", 16 | "@types/react-dom": "18.2.5", 17 | "autoprefixer": "10.4.14", 18 | "drizzle-orm": "^0.26.5", 19 | "eslint": "8.42.0", 20 | "eslint-config-next": "13.4.5", 21 | "next": "13.4.5", 22 | "postcss": "8.4.24", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "tailwindcss": "3.3.2", 26 | "typescript": "5.1.3" 27 | }, 28 | "devDependencies": { 29 | "dotenv": "^16.1.4", 30 | "drizzle-kit": "^0.18.1" 31 | } 32 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/quotes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import getAllQuotes from '@/lib/getAllQuotes' 3 | 4 | export async function GET(request: Request) { 5 | 6 | const quotes = await getAllQuotes() 7 | 8 | return NextResponse.json(quotes) 9 | } -------------------------------------------------------------------------------- /src/app/api/randomquote/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | import getRandomQuote from '@/lib/getRandomQuote' 4 | 5 | // Choosing edge to avoid a Cold Boot Duration 6 | export const runtime = 'edge' // 'nodejs' (default) | 'edge' 7 | 8 | type Props = { 9 | params: { 10 | id: string, 11 | } 12 | } 13 | 14 | export async function GET(request: Request, { params: { id } }: Props) { 15 | 16 | const quote = await getRandomQuote(id) 17 | 18 | return NextResponse.json(quote) 19 | } -------------------------------------------------------------------------------- /src/app/components/Quote.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Poppins, Roboto } from 'next/font/google' 4 | 5 | const roboto = Roboto({ 6 | subsets: ['latin'], 7 | display: 'swap', 8 | weight: "400", 9 | }) 10 | 11 | const poppins = Poppins({ 12 | subsets: ['latin'], 13 | display: 'swap', 14 | weight: "400", 15 | }) 16 | 17 | import { useState, useEffect } from 'react' 18 | 19 | export default function Quote() { 20 | 21 | const [fade, setFade] = useState(false) 22 | const [quote, setQuote] = useState(undefined) 23 | const [id, setId] = useState(10000) 24 | 25 | 26 | useEffect(() => { 27 | 28 | const getQuote = async () => { 29 | const res = await fetch( 30 | `/api/randomquote/${id}`, { cache: 'no-store' } 31 | ) 32 | const json = await res.json() 33 | setQuote(json) 34 | } 35 | 36 | getQuote() 37 | }, [id]) 38 | 39 | if (!quote) return

Loading...

40 | 41 | return ( 42 |
43 | 44 |
45 | 56 | 57 |

Author: {quote.author}

58 |

Category: {quote.category}

59 |
60 | 61 |
62 |

{quote.quote}

65 |
66 |
67 | ) 68 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/rqm-refactor/b1d44185db99eca81eedf98bccee69447554ae8e/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Random Quote Machine', 8 | description: 'This page generates a random quote.', 9 | } 10 | 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | 20 |
21 | {children} 22 |
23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Quote from './components/Quote' 2 | 3 | export default async function Home() { 4 | 5 | return 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/db/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | host: process.env.DATABASE_HOST, 3 | username: process.env.DATABASE_USERNAME, 4 | password: process.env.DATABASE_PASSWORD 5 | } 6 | -------------------------------------------------------------------------------- /src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { mysqlTable, mysqlSchema, AnyMySqlColumn, int, varchar, index, text } from "drizzle-orm/mysql-core" 2 | import { sql } from "drizzle-orm" 3 | 4 | 5 | export const authors = mysqlTable("authors", { 6 | id: int("id").autoincrement().primaryKey().notNull(), 7 | author: varchar("author", { length: 255 }).notNull(), 8 | }); 9 | 10 | export const categories = mysqlTable("categories", { 11 | id: int("id").autoincrement().primaryKey().notNull(), 12 | category: varchar("category", { length: 255 }).notNull(), 13 | }); 14 | 15 | export const quotes = mysqlTable("quotes", { 16 | id: int("id").autoincrement().primaryKey().notNull(), 17 | authorId: int("author_id").notNull(), 18 | categoryId: int("category_id").notNull(), 19 | quote: text("quote").notNull(), 20 | }, 21 | (table) => { 22 | return { 23 | authorIdIdx: index("author_id_idx").on(table.authorId), 24 | categoryIdIdx: index("category_id_idx").on(table.categoryId), 25 | } 26 | }); -------------------------------------------------------------------------------- /src/lib/getAllQuotes.ts: -------------------------------------------------------------------------------- 1 | import { connect } from '@planetscale/database' 2 | import { config } from '@/db/config' 3 | 4 | import { drizzle } from "drizzle-orm/planetscale-serverless"; 5 | import { quotes, authors, categories } from "@/db/schema" 6 | import { eq } from 'drizzle-orm' 7 | 8 | export default async function getAllQuotes(): Promise { 9 | 10 | const conn = connect(config) 11 | const db = drizzle(conn) 12 | 13 | const results: Quote[] = await db.select({ 14 | id: quotes.id, 15 | quote: quotes.quote, 16 | author: authors.author, 17 | category: categories.category, 18 | }) 19 | .from(quotes) 20 | .innerJoin(authors, eq(quotes.authorId, authors.id)) 21 | .innerJoin(categories, eq(quotes.categoryId, categories.id)) 22 | 23 | 24 | return results 25 | } -------------------------------------------------------------------------------- /src/lib/getRandomQuote.ts: -------------------------------------------------------------------------------- 1 | 2 | import getAllQuotes from './getAllQuotes' 3 | 4 | export default async function getRandomQuote(id: string): Promise { 5 | 6 | const prevQuoteId = parseInt(id) 7 | 8 | const results: Quote[] = await getAllQuotes() 9 | 10 | const ids: number[] = results.map(q => q.id) 11 | 12 | let randomId = prevQuoteId 13 | 14 | while (randomId === prevQuoteId) { 15 | const randomIndex = Math.floor(Math.random() * ids.length) 16 | randomId = ids[randomIndex] 17 | } 18 | 19 | const newQuote = results.find(q => q.id === randomId) as Quote 20 | 21 | return newQuote 22 | 23 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | type Quote = { 3 | id: number, 4 | quote: string, 5 | author: string, 6 | category: string, 7 | } 8 | --------------------------------------------------------------------------------