Skip to content

feat: make updates DA-1395 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DIRECT_URL=
DATABASE_URL=
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
91 changes: 70 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,85 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Prisma Accelerate Hacker News Clone

## Getting Started
This project showcases how to use Prisma ORM with Prisma Accelerate, leveraging caching and on-demand cache invalidation, in a Next.js application to build a minimal Hacker News clone.

First, run the development server:
This app retrieves and caches the [top 20 latest posts](/app/page.tsx#L8) with a long Time-to-Live ([TTL](https://www.prisma.io/docs/accelerate/caching#time-to-live-ttl)). The cache is invalidated on-demand whenever a post is [upvoted](/app/actions/addVotes.ts) or a [new post is added](/app/submit/actions/addPost.ts).

![GIF of interaction](demo.gif)

## Prerequisites

To successfully run the project, you will need the following:

- The **connection string** of a PostgreSQL database
- Your **Accelerate connection string** (containing your **Accelerate API key**) which you can get by enabling Accelerate in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys))

## Getting started

### 1. Clone the repository

Clone the repository, navigate into it and install dependencies:

```
git clone git@github.com:prisma/prisma-examples.git --depth=1
cd prisma-examples/accelerate/accelerate-hacker-news
npm install
```

### 2. Configure environment variables

Create a `.env` in the root of the project directory:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
cp .env.example .env
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Now, open the `.env` file and set the `DATABASE_URL` and `DIRECT_URL` environment variables with the values of your connection string and your Accelerate connection string:

```bash
# .env

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
# Accelerate connection string (used for queries by Prisma Client)
DATABASE_URL="__YOUR_ACCELERATE_CONNECTION_STRING__"

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
# Database connection string (used for migrations by Prisma Migrate)
DIRECT_URL="__YOUR_DATABASE_CONNECTION_STRING__"
```

## Learn More
Note that `__YOUR_DATABASE_CONNECTION_STRING__` and `__YOUR_ACCELERATE_CONNECTION_STRING__` are placeholder values that you need to replace with the values of your database and Accelerate connection strings. Notice that the Accelerate connection string has the following structure: `prisma://accelerate.prisma-data.net/?api_key=__YOUR_ACCELERATE_API_KEY__`.

To learn more about Next.js, take a look at the following resources:
### 3. Run a migration to create the `Post` table

The Prisma schema file contains a single `Post` model. You can map this model to the database and create the corresponding `Post` table using the following command:

```
npx prisma migrate dev --name init
```

### 4. Generate Prisma Client for Accelerate

When using Accelerate, Prisma Client doesn't need a query engine. That's why you should generate it as follows:

```
npx prisma generate --no-engine
```

### 5. Start the app

You can run the app with the following command:

```
npm run dev
```

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You should now be able to:

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
- See the most recent post at http://localhost:3000 and upvote it by clicking the ▲ button.
- Submit a new post by navigating to http://localhost:3000/submit.

## Deploy on Vercel
When you make changes, it might take a few seconds to invalidate the cache and display the latest changes.

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
## Resources

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
- [Accelerate Speed Test](https://accelerate-speed-test.vercel.app/)
- [Accelerate documentation](https://www.prisma.io/docs/accelerate)
- [Prisma Discord](https://pris.ly/discord)
12 changes: 12 additions & 0 deletions app/actions/clearCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use server";

import prisma from "@/lib/db";

export async function clearCache() {
await prisma.$accelerate.invalidate({
tags: ["posts"],
});

console.log("Cleared cached posts");
return;
}
75 changes: 42 additions & 33 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
import { Post } from "@/components/Posts";
import prisma from "@/lib/db";
import Link from "next/link";
import js_ago from "js-ago";
import { clearCache } from "./actions/clearCache";

export default async function Home() {
const posts = await prisma.post.findMany({
take: 20,
orderBy: {
createdAt: "desc",
},
cacheStrategy: {
ttl: 120,
tags: ["posts"],
},
});
const { info, data } = await prisma.post
.findMany({
take: 20,
orderBy: {
createdAt: "desc",
},
cacheStrategy: {
ttl: 120,
tags: ["posts"],
},
})
.withAccelerateInfo();

return (
<>
<div className="bg-[#f6f6ef] h-full w-full">
<ol className="p-12 md:p-8 text-black">
{posts.map((post, itemNo) => (
<li
key={post.id}
className="mb-4"
>
<Post
id={post.id}
itemNo={itemNo + 1}
title={post.title}
votes={post.vote}
url={post.url}
/>
</li>
))}
</ol>
</div>
</>
);
return (
<>
<div className="bg-[#f6f6ef] h-full w-full">
<div className="p-12 md:p-8 font-bold text-lg text-black">
{info?.lastModified && (
<p>Showing cached data from: {js_ago(info.lastModified)}</p>
)}

<form action={clearCache}>
<button type="submit">Reset cache</button>
</form>
</div>
<ol className="pl-12 pr-12 md:pl-8 md:pr-8 text-black">
{data.map((post, itemNo) => (
<li key={post.id} className="mb-4">
<Post
id={post.id}
itemNo={itemNo + 1}
title={post.title}
votes={post.vote}
url={post.url}
/>
</li>
))}
</ol>
</div>
</>
);
}
36 changes: 24 additions & 12 deletions app/submit/actions/addPost.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
'use server'
"use server";

import prisma from '@/lib/db'
import prisma from "@/lib/db";
import { Filter } from "bad-words";

export async function createPost(formData: FormData) {
const title = formData.get('title')
const text = formData.get('text')
const url = formData.get('url')
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");

const filter = new Filter();

if (
filter.isProfane(title?.toString() ?? "") ||
filter.isProfane(text?.toString() ?? "") ||
filter.isProfane(url?.toString() ?? "")
) {
return false;
}

const newPost = await prisma.post.create({
data: {
title: title?.toString() ?? '',
content: text?.toString() ?? '',
url: url?.toString() ?? '',
title: title?.toString() ?? "",
content: text?.toString() ?? "",
url: url?.toString() ?? "",
vote: 0,
},
})
});

await prisma.$accelerate.invalidate({
tags: ['posts'],
})
tags: ["posts"],
});

console.log({ newPost }, 'has been created.')
console.log({ newPost }, "has been created.");
return true;
}
21 changes: 21 additions & 0 deletions app/submit/actions/deletePost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use server";

import prisma from "@/lib/db";

export async function deletePost(id: number) {
console.log({
deleting: id,
});

await prisma.post.delete({
where: {
id: id,
},
});

await prisma.$accelerate.invalidate({
tags: ["posts"],
});

return id;
}
21 changes: 13 additions & 8 deletions components/PostSubmissionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
'use client'
import { createPost } from '@/app/submit/actions/addPost'
import { useRef } from 'react'
"use client";
import { createPost } from "@/app/submit/actions/addPost";
import { useRef } from "react";
import toast, { Toaster } from "react-hot-toast";

export const PostForm = () => {
const ref = useRef<HTMLFormElement>(null)
const ref = useRef<HTMLFormElement>(null);
return (
<>
<form
ref={ref}
action={async (formData) => {
await createPost(formData)
ref.current?.reset()
const success = await createPost(formData);
if (!success) {
toast.error("Profanity is not cool.");
}
ref.current?.reset();
}}
className="grid grid-cols-2 max-w-lg p-4 space-y-2 space-x-1"
>
@@ -64,6 +68,7 @@ export const PostForm = () => {
Submit
</button>
</form>
<Toaster />
</>
)
}
);
};
53 changes: 34 additions & 19 deletions components/Posts.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,58 @@
'use client'
"use client";

import { addVotes } from '@/app/actions/addVotes'
import { useEffect, useState } from 'react'
import { addVotes } from "@/app/actions/addVotes";
import { deletePost } from "@/app/submit/actions/deletePost";
import { useEffect, useState } from "react";

interface PostProps {
id: number
itemNo: number
title: string
url: string
votes: number
id: number;
itemNo: number;
title: string;
url: string;
votes: number;
}

export const Post = ({ itemNo, title, url, votes, id }: PostProps) => {
useEffect(() => {}, [votes])
useEffect(() => {}, [votes]);

const [vote, controlVote] = useState<number>(votes)
const [vote, controlVote] = useState<number>(votes);

const increaseVotes = async () => {
try {
await addVotes(id)
controlVote((prev) => prev + 1)
await addVotes(id);
controlVote((prev) => prev + 1);
} catch (error) {
console.log(error)
console.log(error);
}
}
};

const clearPost = async () => {
try {
await deletePost(id);
controlVote((prev) => prev + 1);
} catch (error) {
console.log(error);
}
};

return (
<div className="w-full flex flex-col">
<p className="text-black">
{`${itemNo}. `}
<button className="text-slate-400" onClick={increaseVotes}>
</button>{' '}
{title}{' '}
</button>{" "}
{title}{" "}
<span className="text-sm text-slate-600 hover:underline">
<a href={url} target="_blank">{`(${url})`}</a>
</span>
</p>
<p className="text-slate-600 text-sm pl-10">{vote} points</p>
<p className="text-slate-600 text-sm pl-10">
{vote} points |{" "}
<button className="text-slate-400" onClick={clearPost}>
🗑️ delete post
</button>{" "}
</p>
</div>
)
}
);
};
Loading