Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
railway-bot committed Sep 1, 2022
0 parents commit 291ba1e
Show file tree
Hide file tree
Showing 19 changed files with 844 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: yarn start
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: NextJS Prisma
description: A NextJS app using Prisma with a PostgreSQL database
tags:
- next
- prisma
- postgresql
- typescript
---

# NextJS Prisma Example

This example is a [NextJS](https://nextjs.org/) todo app that uses
[Prisma](https://www.prisma.io/) to store todos in Postgres.

[![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)

## ✨ Features

- Prisma
- NextJS
- Postgres
- TypeScript

## 💁‍♀️ How to use

- [Provision a Postgres container on Railway](https://dev.new)
- Connect to your Railway project with `railway link`
- Migrate the database `railway run yarn migrate:dev`
- Run the NextJS app `railway run yarn dev`

## 📝 Notes

This app is a simple todo list where the data is persisted to Postgres. [Prisma
migrations](https://www.prisma.io/docs/concepts/components/prisma-migrate#prisma-migrate)
can be created with `railway run yarn migrate:dev` and deployed with `railway run yarn migrate:deploy`. The Prisma client can be regenerated with
`yarn generate`.

[swr](https://swr.vercel.app/) is used to fetch data on the client and perform optimistic updates.
2 changes: 2 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "with-nextjs-postgres",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "yarn migrate:deploy && next build",
"start": "next start --port ${PORT-3000}",
"migrate:dev": "prisma migrate dev --preview-feature",
"migrate:deploy": "prisma migrate deploy --preview-feature",
"migrate:status": "prisma migrate status --preview-feature",
"generate": "prisma generate"
},
"dependencies": {
"@prisma/client": "2.30.0",
"next": "12.1.0",
"pg": "^8.5.1",
"react": "17.0.1",
"react-dom": "17.0.1",
"swr": "^0.4.1"
},
"devDependencies": {
"prisma": "2.30.0",
"@types/node": "^14.14.22",
"@types/react": "^17.0.0",
"typescript": "^4.1.3"
}
}
9 changes: 9 additions & 0 deletions prisma/migrations/20210130000309_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "Todo" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"text" TEXT NOT NULL,
"completed" BOOLEAN NOT NULL,

PRIMARY KEY ("id")
);
2 changes: 2 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Please do not edit this file manually
provider = "postgresql"
18 changes: 18 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model Todo {
id String @id @default(uuid())
createdAt DateTime @default(now())
text String
completed Boolean
}
Binary file added public/favicon.ico
Binary file not shown.
4 changes: 4 additions & 0 deletions public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import useSWR, { mutate } from "swr";
import { Todo } from "./types";

const todoPath = "/api/todos";

export const useTodos = () => useSWR<Todo[]>(todoPath);

export const createTodo = async (text: string) => {
mutate(
todoPath,
todos => [{ text, completed: false, id: "new-todo" }, ...todos],
false,
);
await fetch(todoPath, {
method: "POST",
body: JSON.stringify({ text }),
});

mutate(todoPath);
};

export const toggleTodo = async (todo: Todo) => {
mutate(
todoPath,
todos =>
todos.map(t =>
t.id === todo.id ? { ...todo, completed: !t.completed } : t,
),
false,
);
await fetch(`${todoPath}?todoId=${todo.id}`, {
method: "PUT",
body: JSON.stringify({ completed: !todo.completed }),
});
mutate(todoPath);
};

export const deleteTodo = async (id: string) => {
mutate(todoPath, todos => todos.filter(t => t.id !== id), false);
await fetch(`${todoPath}?todoId=${id}`, { method: "DELETE" });
mutate(todoPath);
};
7 changes: 7 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}

export default MyApp
38 changes: 38 additions & 0 deletions src/pages/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") {
// get all todos
const todos = await prisma.todo.findMany({
orderBy: { createdAt: "desc" },
});
res.json(todos);
} else if (req.method === "POST") {
// create todo
const text = JSON.parse(req.body).text;
const todo = await prisma.todo.create({
data: { text, completed: false },
});

res.json(todo);
} else if (req.method === "PUT") {
// update todo
const id = req.query.todoId as string;
const data = JSON.parse(req.body);
const todo = await prisma.todo.update({
where: { id },
data,
});

res.json(todo);
} else if (req.method === "DELETE") {
// delete todo
const id = req.query.todoId as string;
await prisma.todo.delete({ where: { id } });

res.json({ status: "ok" });
}
};
95 changes: 95 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextPage } from "next";
import Head from "next/head";
import { useMemo, useState } from "react";
import { createTodo, deleteTodo, toggleTodo, useTodos } from "../api";
import styles from "../styles/Home.module.css";
import { Todo } from "../types";

export const TodoList: React.FC = () => {
const { data: todos, error } = useTodos();

if (error != null) return <div>Error loading todos...</div>;
if (todos == null) return <div>Loading...</div>;

if (todos.length === 0) {
return <div className={styles.emptyState}>Try adding a todo ☝️️</div>;
}

return (
<ul className={styles.todoList}>
{todos.map(todo => (
<TodoItem todo={todo} />
))}
</ul>
);
};

const TodoItem: React.FC<{ todo: Todo }> = ({ todo }) => (
<li className={styles.todo}>
<label
className={`${styles.label} ${todo.completed ? styles.checked : ""}`}
>
<input
type="checkbox"
checked={todo.completed}
className={`${styles.checkbox}`}
onChange={() => toggleTodo(todo)}
/>
{todo.text}
</label>

<button className={styles.deleteButton} onClick={() => deleteTodo(todo.id)}>
</button>
</li>
);

const AddTodoInput = () => {
const [text, setText] = useState("");

return (
<form
onSubmit={async e => {
e.preventDefault();
createTodo(text);
setText("");
}}
className={styles.addTodo}
>
<input
className={styles.input}
placeholder="Buy some milk"
value={text}
onChange={e => setText(e.target.value)}
/>
<button className={styles.addButton}>Add</button>
</form>
);
};

const Home: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Railway NextJS Prisma</title>
<link rel="icon" href="/favicon.ico" />
</Head>

<header className={styles.header}>
<h1 className={styles.title}>Todos</h1>
<h2 className={styles.desc}>
NextJS app connected to Postgres using Prisma and hosted on{" "}
<a href="https://railway.app">Railway</a>
</h2>
</header>

<main className={styles.main}>
<AddTodoInput />

<TodoList />
</main>
</div>
);
};

export default Home;
Loading

0 comments on commit 291ba1e

Please sign in to comment.