Skip to content

Commit e8295b7

Browse files
committed
Initial Commit
0 parents  commit e8295b7

19 files changed

+844
-0
lines changed

.gitignore

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
.env.development.local
30+
.env.test.local
31+
.env.production.local
32+
33+
# vercel
34+
.vercel

Procfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: yarn start

README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: NextJS Prisma
3+
description: A NextJS app using Prisma with a PostgreSQL database
4+
tags:
5+
- next
6+
- prisma
7+
- postgresql
8+
- typescript
9+
---
10+
11+
# NextJS Prisma Example
12+
13+
This example is a [NextJS](https://nextjs.org/) todo app that uses
14+
[Prisma](https://www.prisma.io/) to store todos in Postgres.
15+
16+
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new?template=https%3A%2F%2Fgithub.com%2Frailwayapp%2Fexamples%2Ftree%2Fmaster%2Fexamples%2Fnextjs-prisma&plugins=postgresql)
17+
18+
## ✨ Features
19+
20+
- Prisma
21+
- NextJS
22+
- Postgres
23+
- TypeScript
24+
25+
## 💁‍♀️ How to use
26+
27+
- [Provision a Postgres container on Railway](https://dev.new)
28+
- Connect to your Railway project with `railway link`
29+
- Migrate the database `railway run yarn migrate:dev`
30+
- Run the NextJS app `railway run yarn dev`
31+
32+
## 📝 Notes
33+
34+
This app is a simple todo list where the data is persisted to Postgres. [Prisma
35+
migrations](https://www.prisma.io/docs/concepts/components/prisma-migrate#prisma-migrate)
36+
can be created with `railway run yarn migrate:dev` and deployed with `railway run yarn migrate:deploy`. The Prisma client can be regenerated with
37+
`yarn generate`.
38+
39+
[swr](https://swr.vercel.app/) is used to fetch data on the client and perform optimistic updates.

next-env.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/types/global" />

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "with-nextjs-postgres",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "yarn migrate:deploy && next build",
8+
"start": "next start --port ${PORT-3000}",
9+
"migrate:dev": "prisma migrate dev --preview-feature",
10+
"migrate:deploy": "prisma migrate deploy --preview-feature",
11+
"migrate:status": "prisma migrate status --preview-feature",
12+
"generate": "prisma generate"
13+
},
14+
"dependencies": {
15+
"@prisma/client": "2.30.0",
16+
"next": "12.1.0",
17+
"pg": "^8.5.1",
18+
"react": "17.0.1",
19+
"react-dom": "17.0.1",
20+
"swr": "^0.4.1"
21+
},
22+
"devDependencies": {
23+
"prisma": "2.30.0",
24+
"@types/node": "^14.14.22",
25+
"@types/react": "^17.0.0",
26+
"typescript": "^4.1.3"
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- CreateTable
2+
CREATE TABLE "Todo" (
3+
"id" TEXT NOT NULL,
4+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
5+
"text" TEXT NOT NULL,
6+
"completed" BOOLEAN NOT NULL,
7+
8+
PRIMARY KEY ("id")
9+
);

prisma/migrations/migration_lock.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Please do not edit this file manually
2+
provider = "postgresql"

prisma/schema.prisma

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This is your Prisma schema file,
2+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
3+
4+
datasource db {
5+
provider = "postgresql"
6+
url = env("DATABASE_URL")
7+
}
8+
9+
generator client {
10+
provider = "prisma-client-js"
11+
}
12+
13+
model Todo {
14+
id String @id @default(uuid())
15+
createdAt DateTime @default(now())
16+
text String
17+
completed Boolean
18+
}

public/favicon.ico

14.7 KB
Binary file not shown.

public/vercel.svg

+4
Loading

src/api.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import useSWR, { mutate } from "swr";
2+
import { Todo } from "./types";
3+
4+
const todoPath = "/api/todos";
5+
6+
export const useTodos = () => useSWR<Todo[]>(todoPath);
7+
8+
export const createTodo = async (text: string) => {
9+
mutate(
10+
todoPath,
11+
todos => [{ text, completed: false, id: "new-todo" }, ...todos],
12+
false,
13+
);
14+
await fetch(todoPath, {
15+
method: "POST",
16+
body: JSON.stringify({ text }),
17+
});
18+
19+
mutate(todoPath);
20+
};
21+
22+
export const toggleTodo = async (todo: Todo) => {
23+
mutate(
24+
todoPath,
25+
todos =>
26+
todos.map(t =>
27+
t.id === todo.id ? { ...todo, completed: !t.completed } : t,
28+
),
29+
false,
30+
);
31+
await fetch(`${todoPath}?todoId=${todo.id}`, {
32+
method: "PUT",
33+
body: JSON.stringify({ completed: !todo.completed }),
34+
});
35+
mutate(todoPath);
36+
};
37+
38+
export const deleteTodo = async (id: string) => {
39+
mutate(todoPath, todos => todos.filter(t => t.id !== id), false);
40+
await fetch(`${todoPath}?todoId=${id}`, { method: "DELETE" });
41+
mutate(todoPath);
42+
};

src/pages/_app.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import '../styles/globals.css'
2+
3+
function MyApp({ Component, pageProps }) {
4+
return <Component {...pageProps} />
5+
}
6+
7+
export default MyApp

src/pages/api/todos.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
import { PrismaClient } from "@prisma/client";
3+
4+
const prisma = new PrismaClient();
5+
6+
export default async (req: NextApiRequest, res: NextApiResponse) => {
7+
if (req.method === "GET") {
8+
// get all todos
9+
const todos = await prisma.todo.findMany({
10+
orderBy: { createdAt: "desc" },
11+
});
12+
res.json(todos);
13+
} else if (req.method === "POST") {
14+
// create todo
15+
const text = JSON.parse(req.body).text;
16+
const todo = await prisma.todo.create({
17+
data: { text, completed: false },
18+
});
19+
20+
res.json(todo);
21+
} else if (req.method === "PUT") {
22+
// update todo
23+
const id = req.query.todoId as string;
24+
const data = JSON.parse(req.body);
25+
const todo = await prisma.todo.update({
26+
where: { id },
27+
data,
28+
});
29+
30+
res.json(todo);
31+
} else if (req.method === "DELETE") {
32+
// delete todo
33+
const id = req.query.todoId as string;
34+
await prisma.todo.delete({ where: { id } });
35+
36+
res.json({ status: "ok" });
37+
}
38+
};

src/pages/index.tsx

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { NextPage } from "next";
2+
import Head from "next/head";
3+
import { useMemo, useState } from "react";
4+
import { createTodo, deleteTodo, toggleTodo, useTodos } from "../api";
5+
import styles from "../styles/Home.module.css";
6+
import { Todo } from "../types";
7+
8+
export const TodoList: React.FC = () => {
9+
const { data: todos, error } = useTodos();
10+
11+
if (error != null) return <div>Error loading todos...</div>;
12+
if (todos == null) return <div>Loading...</div>;
13+
14+
if (todos.length === 0) {
15+
return <div className={styles.emptyState}>Try adding a todo ☝️️</div>;
16+
}
17+
18+
return (
19+
<ul className={styles.todoList}>
20+
{todos.map(todo => (
21+
<TodoItem todo={todo} />
22+
))}
23+
</ul>
24+
);
25+
};
26+
27+
const TodoItem: React.FC<{ todo: Todo }> = ({ todo }) => (
28+
<li className={styles.todo}>
29+
<label
30+
className={`${styles.label} ${todo.completed ? styles.checked : ""}`}
31+
>
32+
<input
33+
type="checkbox"
34+
checked={todo.completed}
35+
className={`${styles.checkbox}`}
36+
onChange={() => toggleTodo(todo)}
37+
/>
38+
{todo.text}
39+
</label>
40+
41+
<button className={styles.deleteButton} onClick={() => deleteTodo(todo.id)}>
42+
43+
</button>
44+
</li>
45+
);
46+
47+
const AddTodoInput = () => {
48+
const [text, setText] = useState("");
49+
50+
return (
51+
<form
52+
onSubmit={async e => {
53+
e.preventDefault();
54+
createTodo(text);
55+
setText("");
56+
}}
57+
className={styles.addTodo}
58+
>
59+
<input
60+
className={styles.input}
61+
placeholder="Buy some milk"
62+
value={text}
63+
onChange={e => setText(e.target.value)}
64+
/>
65+
<button className={styles.addButton}>Add</button>
66+
</form>
67+
);
68+
};
69+
70+
const Home: NextPage = () => {
71+
return (
72+
<div className={styles.container}>
73+
<Head>
74+
<title>Railway NextJS Prisma</title>
75+
<link rel="icon" href="/favicon.ico" />
76+
</Head>
77+
78+
<header className={styles.header}>
79+
<h1 className={styles.title}>Todos</h1>
80+
<h2 className={styles.desc}>
81+
NextJS app connected to Postgres using Prisma and hosted on{" "}
82+
<a href="https://railway.app">Railway</a>
83+
</h2>
84+
</header>
85+
86+
<main className={styles.main}>
87+
<AddTodoInput />
88+
89+
<TodoList />
90+
</main>
91+
</div>
92+
);
93+
};
94+
95+
export default Home;

0 commit comments

Comments
 (0)