diff --git a/src/SUMMARY.md b/src/SUMMARY.md index ce9f6a9..82b47ff 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -91,7 +91,6 @@ - [The Project](chapter10/README.md) - [Setup](chapter10/01-setup.md) - - [Authentication](chapter10/02-authentication.md) - - [The Projects Page](chapter10/03-the-projects-page.md) - - [Task Page](chapter10/04-task-page.md) - - [Deployment](chapter10/05-deployment.md) + - [The Projects Page](chapter10/02-the-projects-page.md) + - [Task Page](chapter10/03-task-page.md) + - [Deployment](chapter10/04-deployment.md) diff --git a/src/chapter10/01-setup.md b/src/chapter10/01-setup.md index 1e50906..6182d3b 100644 --- a/src/chapter10/01-setup.md +++ b/src/chapter10/01-setup.md @@ -18,21 +18,23 @@ Give your project the name `easy-opus` and select the following options: - we want to use Tailwind CSS - we want to use the `src/` directory - we want to use the App Router -- we don't want to customize the defalt import alias +- we want to use Turbopack for `next dev` +- we don't want to customize the default import alias -Note that from now on we specify all paths relative to the `src` directory. -For example if we refer to a file `thingy/example.ts` that file will actually be in `src/thingy/example.ts`. +Note that from now on we specify all paths relative to the `src/app` directory. +For example if we refer to a file `thingy/example.ts` that file will actually be in `src/app/thingy/example.ts`. If you're unsure about the location of a file, you can also look at the end of this section, which contains the file tree you should have after the setup is completed. -### Removing Unneccessary Code +### Removing Unnecessary Code -Let's remove all the unneccessary code from the generated files. +Let's remove all the unnecessary code from the generated files. -Change the file `app/layout.tsx` to look like this: +Change the file `layout.tsx` to look like this: ```jsx import type { Metadata } from 'next'; import './globals.css'; +import Link from 'next/link'; export const metadata: Metadata = { title: 'Easy Opus', @@ -42,13 +44,22 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + + <>{children} + ); } ``` -Change the file `app/page.tsx` to look like this: +Change the file `page.tsx` to look like this: ```jsx export default function Home() { @@ -64,7 +75,7 @@ Change the file `app/globals.css` to look like this: @tailwind utilities; ``` -Additionally, feel free to delete the SVG files in the `public` directory and to change (or delete) the `favicon`. +Additionally, feel free to delete the SVG files in the `public` directory and to change the `favicon.ico`. Run `pnpm dev` and check out the page at `http://localhost:3000`. You should see the underlined text `Welcome to easy-opus`. @@ -74,7 +85,7 @@ You should see the underlined text `Welcome to easy-opus`. Next we need to setup our database. To accomplish this, we will simply follow the steps from the SQL chapter. -Create a new Supabase project, copy the database URL and create the following `.env.local` file: +Create a new Supabase project, copy the database URL and create the following `.env` file: ``` DATABASE_URL=$YOUR_DATABASE_URL_HERE @@ -82,28 +93,37 @@ DATABASE_URL=$YOUR_DATABASE_URL_HERE Of course, you need to specify the actual database URL you copied from Supabase instead of `$YOUR_DATABASE_URL_HERE`. -Note that if your password has special characters like `:` or `/`, you will need to replace them with their respective percent-encodings (also called URL encodings). -For example, `:` should be replaced with `%3A` and `/` should be replaced with `%2F`. -You can read more about percent-encodings in the [MDN docs](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding). +Remember that if your password has special characters like `:` or `/`, you will need to replace them with their respective percent-encodings. ### Setup Drizzle Next, we need to set up Drizzle. Here we will simply follow the steps from the Drizzle chapter. -Install Drizzle and `dotenv`: +Install `drizzle-orm` and `pg`: ```sh -pnpm add drizzle-orm postgres dotenv +pnpm add drizzle-orm pg pnpm add --save-dev tsx drizzle-kit ``` +Also, install the `@types/pg` package to get the type definitions for `pg`: + +```sh +pnpm add @types/pg --save-dev +``` + +Finally, install the `drizzle-kit` package as a dev dependency: + +```sh +pnpm add drizzle-kit --save-dev +``` + Create a new directory called `db`. This is where our database-related files will go. +You should also create a directory `db/migrations` where we will store the migrations. -> Remember that we specify all paths relative to `src`, i.e. you need to create the `db` directory in `src`. - -Now create a directory `db/migrations` to store the migrations. +> Remember that we specify all paths relative to `src/app`, i.e. you need to create the `db` directory in `src/app`. Create a file `db/drizzle.config.ts`: @@ -111,43 +131,15 @@ Create a file `db/drizzle.config.ts`: import { defineConfig } from 'drizzle-kit'; export default defineConfig({ + out: './src/app/db/migrations', + schema: './src/app/db/schema.ts', dialect: 'postgresql', - schema: './src/db/schema.ts', - out: './src/db/migrations', dbCredentials: { url: process.env.DATABASE_URL!, }, }); ``` -Next we create a file `db/migrate.ts`: - -```ts -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import dotenv from 'dotenv'; -import { migrate } from 'drizzle-orm/postgres-js/migrator'; - -dotenv.config({ path: ['.env.local', '.env'] }); - -const databaseURI = process.env.DATABASE_URL; - -if (databaseURI === undefined) { - console.log('You need to provide the database URI'); - process.exit(0); -} - -const client = postgres(databaseURI, { max: 1 }); -const db = drizzle(client); - -async function runMigrations() { - await migrate(db, { migrationsFolder: './src/db/migrations' }); - await client.end(); -} - -runMigrations().then(console.log).catch(console.error); -``` - Finally, let's create the initial schema at `db/schema.ts`: ```ts @@ -155,32 +147,30 @@ import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; export const projectTable = pgTable('project', { id: serial('id').primaryKey(), - userId: text('user_id').notNull(), name: text('name').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }); ``` -To simplify migrations, we will add the following scripts to `package.json`: +To simplify migrations, we will add the `generate` and `migrate` scripts to `package.json`: ```json { "scripts": { // other scripts - "db:generate": "pnpm drizzle-kit generate --config=src/db/drizzle.config.ts", - "db:migrate": "pnpm tsx src/db/migrate.ts" + "db:generate": "pnpm drizzle-kit generate --config=./src/app/db/drizzle.config.ts", + "db:migrate": "pnpm drizzle-kit migrate" } } ``` -Now run `pnpm db:generate` to generate the migration. +Now, run `pnpm db:generate` to generate the migration. -Inspect the migration (which would be something like `db/migrations/0000_curious_vanisher-sql`) and make sure that it contains the right content: +Inspect the migration (which would be something like `db/migrations/0000_curious_vanisher.sql`) and make sure that it contains the right content: ```sql CREATE TABLE IF NOT EXISTS "project" ( "id" serial PRIMARY KEY NOT NULL, - "user_id" text NOT NULL, "name" text NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL ); @@ -193,25 +183,21 @@ Finally, we create the `db/index.ts` file which exports the `db` object to allow ```ts import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; - -const databaseURL = process.env.DATABASE_URL!; -const client = postgres(databaseURL); -export const db = drizzle(client); +export const db = drizzle(process.env.DATABASE_URL!); ``` -> Yes, this subsection was essentially a repeat of things you already learned in the Drizzle chapter. +> Yes, this subsection was essentially a repeat of things you've already learned in the Drizzle chapter. ### Linting If you look through the scripts in `package.json`, you will see a curious little script called `lint` that executes `next lint`. -This scripts provides an integrated ESLint experience. +This script provides an integrated ESLint experience. ESLint is an awesome tool that statically analyzes your code to quickly find problems. Note that ESLint is not for finding syntax or type errors (your TypeScript compiler already takes care of that). -Instead it has a lot of rules for good code and bad code and attempts to help you with writing high-quality code. +Instead it has a lot of rules that help you avoid sketchy code. Let's run it: @@ -233,7 +219,7 @@ Currently, ESLint has nothing to tell us. This is the file structure you should have right now: ``` -├── .env.local +├── .env ├── .eslintrc.json ├── next.config.mjs ├── next-env.d.ts @@ -249,7 +235,6 @@ This is the file structure you should have right now: │ └── db │ ├── drizzle.config.ts │ ├── index.ts -│ ├── migrate.ts │ ├── migrations │ │ ├── 0000_curious_vanisher.sql │ │ └── meta @@ -280,10 +265,10 @@ The `postcss.config.js` file contains the configuration relevant for PostCSS (wh The file `src/app/page.tsx` specifies the root page and `src/app/layout.tsx` specifies the root layout. -The `globals.css` file specifies global styles - right we only really need it for the Tailwind directives. +The `globals.css` file specifies global styles—right we only really need it for the Tailwind directives. The `src/db` directory contains everything that is related to the database (including the migrations). The `.eslintrc.json` contain the `eslint` configuration. -The `.env.local` file contains our enviroment variables. +The `.env` file contains our environment variables. diff --git a/src/chapter10/02-authentication.md b/src/chapter10/02-authentication.md deleted file mode 100644 index d4531b5..0000000 --- a/src/chapter10/02-authentication.md +++ /dev/null @@ -1,138 +0,0 @@ -## Authentication - -The tasks and projects should be "owned" by individual users of our application. - -Of course, users should not be able to access or look at projects of other users. -This means that we need to implement authentication. - -That used to be very hard - luckily, nowadays there are prebuilt libraries to help us out (at least for the common use cases). -We will use a library called Clerk. - -Go to `dashboard.clerk.com` and create a new application. - -Let's give the application the name `easy-opus`. -You will get a bunch of sign in options, we will select "Email" and "Google". -Click "Create application". - -![](images/new-auth-app.png) - -After you've created an application, you will be redirected to a page that shows the values of two API keys, namely `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`. -You should copy these values to your `.env` file: - -``` -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$YOUR_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY -CLERK_SECRET_KEY=$YOUR_CLERK_SECRET_KEY -``` - -Next install `@clerk/nextjs`: - -```sh -pnpm add @clerk/nextjs -``` - -Next we will need to add a `` to our app in `layout.tsx`: - -```jsx -import { ClerkProvider } from '@clerk/nextjs'; - -// ... - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} -``` - -Now we need to add a `middleware.ts` file that will specify what routes we want to protect: - -```jsx -import { authMiddleware } from '@clerk/nextjs'; - -export default authMiddleware({ - publicRoutes: ['/'], -}); - -export const config = { - matcher: [ - // Exclude files with a "." followed by an extension, which are typically static files. - // Exclude files in the _next directory, which are Next.js internals. - '/((?!.+\\.[\\w]+$|_next).*)', - // Re-include any files in the api or trpc folders that might have an extension - '/(api)(.*)', - ], -}; -``` - -> Note that `middleware.ts` should be directly in `src`, not in `src/app`. - -Next, let's create the first version of the homepage `app/page.tsx`. - -We will keep it simple for now. -If the user is not logged in, we will show the `SignIn` button. -Otherwise we will show a placeholder text: - -```jsx -import { SignIn } from '@clerk/nextjs'; -import { auth } from '@clerk/nextjs/server'; -import * as React from 'react'; - -export default async function Home() { - const { userId } = auth(); - - if (userId === null) { - return ( -
- -
- ); - } - - return

Projects will be here

; -} -``` - -Finally, let's add a navbar to every page that will show a `UserButton`. -Modify `app/layout.tsx`: - -```jsx -import { ClerkProvider, UserButton } from '@clerk/nextjs'; -import type { Metadata } from 'next'; -import './globals.css'; - -export const metadata: Metadata = { - title: 'Easy Opus', - description: 'A simple task management application', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - - <>{children} - - - - ); -} -``` - -If you try accessing `localhost:3000` now you will see a sign-in form provided by Clerk.js. -You can use this to sign in with your Google account or create a new account with an email and password and sign in using that. - -Once you are signed in, you will see a page with a navbar, a user button in the top right corner and the placeholder text `Projects will be here`. diff --git a/src/chapter10/03-the-projects-page.md b/src/chapter10/02-the-projects-page.md similarity index 51% rename from src/chapter10/03-the-projects-page.md rename to src/chapter10/02-the-projects-page.md index fe82696..b77cf37 100644 --- a/src/chapter10/03-the-projects-page.md +++ b/src/chapter10/02-the-projects-page.md @@ -2,15 +2,15 @@ ### Project List -Let's create a component `ProjectList` in `app/project-list.tsx` that will show a nicely styled list of projects: +Let's create a component `ProjectList` in `project-list.tsx` that will show a nicely styled list of projects: ```jsx export function ProjectList({ projects }: { projects: { id: number, name: string }[] }) { return ( -
+
{projects.map((project) => ( -
- +
+ {project.name}
@@ -20,35 +20,26 @@ export function ProjectList({ projects }: { projects: { id: number, name: string } ``` -Update the `app/page.tsx` file to retrieve the projects show the project list: +Next, we will update the `page.tsx` file to show the project list. ```jsx -import { db } from '@/db'; -import { projectTable } from '@/db/schema'; -import { auth } from '@clerk/nextjs/server'; -import { SignIn } from '@clerk/nextjs'; +import { db } from './db'; +import { projectTable } from './db/schema'; import { ProjectList } from './project-list'; -import { eq } from 'drizzle-orm'; export default async function Home() { - const { userId } = auth(); - - if (userId === null) { - // ... - } - - const projects = await db.select().from(projectTable).where(eq(projectTable.userId, userId)); + const projects = await db.select().from(projectTable); return ; } ``` -Add a few projects with the correct user ID to the database and go to `localhost:3000`. +Add a few projects to the database and go to `http://localhost:3000`. You should see a project list containing the added projects. ### Fixing a Lint -While has no syntax errors, no type errors and generally works correctly, there is one issue. +While our project has no syntax errors, no type errors and generally works correctly, there is one issue. If you read this book carefully so far, you should theoretically be able to figure it out, but it might take a while. Let's use our awesome ESLint tool instead: @@ -67,26 +58,24 @@ You should see: Remember that if you want to render a list in React, you should give the individual elements a `key` prop. In this case, a good key prop would be the primary key from the database, so let's use that. -Add the `key` property to the project `div` in `app/project-list.tsx` like this: +Add the `key` property to the project `div` in `project-list.tsx` like this: ```jsx -// ...
- + {project.name}
-// ... ``` If you rerun `pnpm lint`, you should see no more errors. ### New Project Modal -Let's create a modal that will allow us to add new projects at `app/new-project-modal.tsx`: +Let's create a modal that will allow us to add new projects at `project-modal.tsx`: ```jsx "use client"; @@ -115,16 +104,16 @@ export function NewProjectModal({ } return ( -
-
+
+
-
-

Add Project

+ +

Add Project

@@ -149,43 +138,53 @@ export function NewProjectModal({ } ``` -Let's create a file `db/actions.ts` containing the `insertProject` function: +Let's create a file `api/project/route.ts` with a `POST` endpoint that allows us to add a new project: ```ts -'use server'; +import { db } from '@/app/db'; +import { projectTable } from '@/app/db/schema'; +import { NextRequest, NextResponse } from 'next/server'; -import { db } from '.'; -import { projectTable } from './schema'; +export async function POST(request: NextRequest) { + const { name } = await request.json(); -export async function insertProject(userId: string, name: string) { - await db.insert(projectTable).values({ userId, name }); + if (!name) { + return NextResponse.json({ error: 'Project name is required' }, { status: 400 }); + } + + await db.insert(projectTable).values({ name }); + + return NextResponse.json({ message: 'Project inserted successfully' }, { status: 200 }); } ``` -We need to show the modal in the project list by modifying the `app/project-list.tsx` file: +We need to show the modal in the project list by modifying the `project-list.tsx` file: ```jsx 'use client'; -import { insertProject } from '@/db/actions'; -import { NewProjectModal } from './new-project-modal'; +import { NewProjectModal } from './project-modal'; import * as React from 'react'; import { useRouter } from 'next/navigation'; -export function ProjectList({ - userId, - projects, -}: { - userId: string, - projects: { id: number, name: string }[], -}) { +export function ProjectList({ projects }: { projects: { id: number, name: string }[] }) { const router = useRouter(); const [showModal, setShowModal] = React.useState(false); async function handleNewProject(name: string) { - await insertProject(userId, name); + const response = await fetch('/api/project', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name }), + }); + + if (!response.ok) { + throw new Error('Failed to create project'); + } setShowModal(false); router.refresh(); } @@ -194,7 +193,7 @@ export function ProjectList({
@@ -202,21 +201,11 @@ export function ProjectList({ {/* Project list here */} {showModal && ( - setShowModal(true)} /> + setShowModal(false)} /> )}
); } ``` -Finally, we need to update the `app/page.tsx` file since the `ProjectList` component now takes a user ID: - -```jsx -export default async function Home() { - // ... - - return ; -} -``` - -Go to `localhost:3000` and try adding a few projects using the "Add new project" button and the project modal. +Go to `http://localhost:3000` and try adding a few projects. diff --git a/src/chapter10/04-task-page.md b/src/chapter10/03-task-page.md similarity index 56% rename from src/chapter10/04-task-page.md rename to src/chapter10/03-task-page.md index a90bcf2..327ffba 100644 --- a/src/chapter10/04-task-page.md +++ b/src/chapter10/03-task-page.md @@ -6,23 +6,36 @@ We now have a page where we can show the created projects. However, this is not terribly useful as long as we can't add tasks to the projects. First, we need a place to store the tasks in our database. -Create a new task table in `db/schema.ts`: +Create a new `task` table in `db/schema.ts`: ```ts -import { integer /*...*/ } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import { check, integer, pgEnum /*...*/ } from 'drizzle-orm/pg-core'; // ... -export const taskTable = pgTable('task', { - id: serial('id').primaryKey(), - title: text('title').notNull(), - description: text('description').notNull(), - status: text('status').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - projectId: integer('project_id') - .notNull() - .references(() => projectTable.id), -}); +export const statusEnum = pgEnum('status', ['todo', 'inprogress', 'done']); + +// Declare the task table +export const taskTable = pgTable( + 'task', + { + id: serial('id').primaryKey(), + title: text('title').notNull(), + description: text('description').notNull(), + status: statusEnum().notNull(), + duration: integer('duration'), + createdAt: timestamp('created_at').defaultNow().notNull(), + projectId: integer('project_id') + .notNull() + .references(() => projectTable.id), + }, + (table) => [ + { + durationCheckConstraint: check('duration_check', sql`${table.duration} > 0`), + }, + ], +); ``` Generate the migration: @@ -34,20 +47,17 @@ pnpm db:generate Review the migration (which might be something like `db/migrations/0001_loose_wonder_man.sql`): ```sql -CREATE TABLE IF NOT EXISTS "task" ( +CREATE TYPE "public"."status" AS ENUM('todo', 'inprogress', 'done'); +CREATE TABLE "task" ( "id" serial PRIMARY KEY NOT NULL, "title" text NOT NULL, "description" text NOT NULL, - "status" text NOT NULL, + "status" "status" NOT NULL, + "duration" integer, "created_at" timestamp DEFAULT now() NOT NULL, "project_id" integer NOT NULL ); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "task" ADD CONSTRAINT "task_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "project"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; +ALTER TABLE "task" ADD CONSTRAINT "task_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE no action ON UPDATE no action; ``` Execute the migration: @@ -63,21 +73,11 @@ Check that the `task` table is present in the database together with the right c Now let's create a page containing the tasks of a given project at `app/project/[id]/page.tsx`: ```jsx -import { db } from '@/db'; -import { projectTable, taskTable } from '@/db/schema'; +import { db } from '@/app/db'; +import { projectTable, taskTable } from '@/app/db/schema'; import { eq } from 'drizzle-orm'; -import { auth } from '@clerk/nextjs/server'; export default async function Project({ params: { id } }: { params: { id: number } }) { - const { userId } = auth(); - - const projects = await db.select().from(projectTable).where(eq(projectTable.id, id)); - const project = projects[0]; - - if (project.userId !== userId) { - return

Not allowed to access project

; - } - const tasks = await db.select().from(taskTable).where(eq(taskTable.projectId, id)); return ( @@ -90,12 +90,12 @@ export default async function Project({ params: { id } }: { params: { id: number } ``` -Add a few tasks to the project with the ID `1` and go to `localhost:3000/project/1` - you should see these tasks. -However, the UX is currently quite ugly, so let's improve it. +Add a few tasks to the project with the ID `1` and go to `http://localhost:3000/project/1` - you should see these tasks. +Next, we need to give the user a way to add tasks by themselves. ### Task List -Create the a `TaskList` component at `app/project/[id]/task-list.tsx`: +Create a `TaskList` component at `app/project/[id]/task-list.tsx`: ```jsx export function TaskList({ @@ -104,14 +104,11 @@ export function TaskList({ tasks: { id: number, title: string, description: string, status: string }[], }) { return ( -
+
{tasks.map((task) => ( -
+

{task.title}

-

{task.description}

+

{task.description}

{task.status}

))} @@ -136,7 +133,7 @@ export default async function Project({ params: { id } }: { params: { id: number ### New Task Modal Finally, let's create a modal that will allow us to add new tasks. -Create a file `app/project/[id]/new-task-modal.tsx`: +Create a file `app/project/[id]/task-modal.tsx`: ```jsx "use client"; @@ -165,16 +162,16 @@ export function NewTaskModal({ } return ( -
-
+
+
- -

Add Task

+ +

Add Task

@@ -200,12 +197,12 @@ export function NewTaskModal({ type="text" id="description" name="description" - className="mt-2 block w-full px-4 py-3 bg-gray-50 rounded-md border-transparent focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" + className="mt-1 block w-full px-3 py-2 bg-gray-50 rounded border focus:border-blue-500 focus:ring-blue-200" />
@@ -216,16 +213,26 @@ export function NewTaskModal({ } ``` -Let's add a new database function `insertTask` in `db/actions.ts`: +Let's add a new route in `task/route.ts`: ```ts -// ... -import { taskTable /*...*/ } from './schema'; - -// ... +import { db } from '@/app/db'; +import { taskTable } from '@/app/db/schema'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const { title, description, status, projectId } = await request.json(); + + if (!title || !description || !projectId) { + return NextResponse.json( + { error: 'Task title, description and project ID are required' }, + { status: 400 }, + ); + } -export async function insertTask(title: string, description: string, projectId: number) { await db.insert(taskTable).values({ title, description, status: 'inprogress', projectId }); + + return NextResponse.json({ message: 'Task inserted successfully' }, { status: 200 }); } ``` @@ -234,8 +241,7 @@ Use the new task modal in the `app/project/[id]/task-list.tsx` file: ```jsx 'use client'; -import { insertTask } from '@/db/actions'; -import { NewTaskModal } from './new-task-modal'; +import { NewTaskModal } from './task-modal'; import * as React from 'react'; import { useRouter } from 'next/navigation'; @@ -251,7 +257,17 @@ export function TaskList({ const router = useRouter(); async function handleNewTask(title: string, description: string) { - await insertTask(title, description, projectId); + const response = await fetch('/api/task', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title, description, projectId }), + }); + + if (!response.ok) { + throw new Error('Failed to create task'); + } setShowNewTaskModal(false); router.refresh(); } @@ -260,7 +276,7 @@ export function TaskList({
@@ -283,3 +299,15 @@ export default async function Project(/* ...*/) { ``` You should now be able to use the "Add new task" button and the modal to add new tasks to the project. + +Finally, let's make all the projects on the homepage into links that take you to the respective project pages: + +```jsx + + + {project.name} + + +``` + +> Don't forget to import the `` component here. diff --git a/src/chapter10/05-deployment.md b/src/chapter10/04-deployment.md similarity index 57% rename from src/chapter10/05-deployment.md rename to src/chapter10/04-deployment.md index 9aa8f41..fbfc7e0 100644 --- a/src/chapter10/05-deployment.md +++ b/src/chapter10/04-deployment.md @@ -8,7 +8,7 @@ We will use **GitHub** and **Vercel** to deploy our application. GitHub is a platform that allows developers to upload, store, manage and share their code. If you look at `easy-opus` you will see a directory `.git` and a file `.gitignore`. -This is because `pnpm create next-app` has initialize a git repository for you. +This is because `pnpm create next-app` has initialized a git repository for you. Let's update the index with our current content: @@ -16,7 +16,7 @@ Let's update the index with our current content: git add . ``` -If you run `git status` you will a bunch of change to be committed. +If you run `git status` you will see the changes to be committed. To actually commit them, run: ```sh @@ -33,10 +33,26 @@ git push --set-upstream origin main ### Deploying on Vercel Finally, we will deploy this project on Vercel. -Simply go to `vercel.com`, create a new project and import the git repository that we just created. +Simply go to `vercel.com`, create a new project and import the git repository that we've just created. Next, you need to add the environment variables in the "environment variables" section. Finally, Vercel will deploy your application and automatically set up a domain for it. Congratulations, you can now access `easy-opus` from anywhere in the world! + +> Note that normally we would also protect our webpage with authentication precisely in order to prevent everyone in the world from making changes to your tasks. +> However, authentication is out of scope for this book. + +### More Features + +Congratulations, you now have a working minimal task application. + +You should try adding more features, most importantly: + +- make the status field in the tasks a dropdown that allows you to change the status +- add a button that allows you to delete the tasks +- arrange the tasks in columns by their status +- show the creation date of the tasks in the UI + +And more! diff --git a/src/chapter10/images/new-auth-app.png b/src/chapter10/images/new-auth-app.png deleted file mode 100644 index 5a910de..0000000 Binary files a/src/chapter10/images/new-auth-app.png and /dev/null differ diff --git a/src/chapter5/05-migrations.md b/src/chapter5/05-migrations.md index be59b5b..8545cba 100644 --- a/src/chapter5/05-migrations.md +++ b/src/chapter5/05-migrations.md @@ -220,15 +220,7 @@ pnpm drizzle-kit generate pnpm drizzle-kit migrate ``` -Alternatively, you can read the environment variables from a `.env` file. - -To accomplish this, you first need to install the `dotenv` package: - -```sh -pnpm add dotenv -``` - -Next, create a `.env` file: +Alternatively, you can read the environment variables from a `.env` file: ``` DATABASE_URL=$YOUR_DATABASE_URL_HERE diff --git a/src/chapter8/01-setup.md b/src/chapter8/01-setup.md index 1b6154c..51c4141 100644 --- a/src/chapter8/01-setup.md +++ b/src/chapter8/01-setup.md @@ -19,6 +19,7 @@ Give your project a name and select the following options: - we want to use Tailwind CSS - we want to use the `src/` directory - we want to use the App Router +- we want to use Turbopack for `next dev` - we don't want to customize the default import alias ```sh