Technology
Construct a Twitter Clone Utilizing TypeScript, Prisma and Subsequent.js – SitePoint
One of the simplest ways to study a instrument like React is to construct one thing with it. Subsequent.js is a strong framework that helps you construct for manufacturing. On this tutorial, we’ll learn to construct a clone of Twitter utilizing Subsequent.js and Prisma.
Our app may have the next options:
- authentication utilizing NextAuth and Twitter OAuth
- an choice so as to add a brand new tweet
- an choice to view an inventory of tweets
- an choice to view the profile of a person with solely their tweets
The code for the app we’ll be constructing is out there on GitHub. We’ll be utilizing TypeScript to construct our app.
Preliminaries
Subsequent.js is without doubt one of the hottest React.js frameworks. It has plenty of options like server-side rendering, TypeScript help, picture optimization, I18n help, file-system routing, and extra.
Prisma is an ORM for Node.js and TypeScript. It additionally offers plenty of options like uncooked database entry, seamless relation API, native database varieties, and so forth.
Software program required
We’ll want the next put in for the needs of working our app:
These applied sciences might be used within the app:
- Subsequent.js: for constructing our app
- Prisma: for fetching and saving knowledge into the database
- Chakra UI: for including kinds to our app
- NextAuth: for dealing with authentication
- React Question: for fetching and updating knowledge in our app
Creating a brand new Subsequent.js App
Now, let’s get began! We’ll first create a brand new Subsequent.js app by working the next command from our terminal:
yarn create next-app
We’ll must enter the identify of the app when the command prompts for it. We will identify it something we would like. Nonetheless, on this case, I’ll identify it twitter-clone. We should always have the ability to see an identical output on our terminal:
$ yarn create next-app
yarn create v1.22.5
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Constructing recent packages...
success Put in "create-next-app@10.0.4" with binaries:
- create-next-app
✔ What's your undertaking named? twitter-clone
Creating a brand new Subsequent.js app in /twitter-clone.
....
Initialized a git repository.
Success! Created twitter-clone at /twitter-clone
Inside that listing, you possibly can run a number of instructions:
yarn dev
Begins the event server.
yarn construct
Builds the app for manufacturing.
yarn begin
Runs the constructed app in manufacturing mode.
We propose that you simply start by typing:
cd twitter-clone
yarn dev
We will now go contained in the twitter-clone listing and begin our app by working the next command:
cd twitter-clone && yarn dev
Our Subsequent.js app ought to be up and working on http://localhost:3000. We should always have the ability to see the next display:
Including a Dockerized PostgreSQL Database
Subsequent, let’s add a Dockerized PostgreSQL database in order that we are able to save the customers and tweets into it. We will create a brand new docker-compose.yml
file within the root of our app with the next content material:
model: "3"
providers:
db:
container_name: db
picture: postgres:11.3-alpine
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/knowledge
restart: except-stopped
volumes:
db_data:
If Docker is working on our machine, we are able to execute the next command from the foundation of our app to start out our PostgreSQL container:
docker-compose up
The above command will begin the PostgreSQL container and it may be accessed on postgresql://postgres:@localhost:5432/postgres
. Observe that you would be able to additionally use a native set up of Postgres as a substitute of a Dockerized one.
Including Chakra UI
Chakra UI is a quite simple React.js element library. It’s very fashionable and has the options like accessibility, help for each gentle and darkish mode, and extra. We’ll be utilizing Chakra UI for styling our person interface. We will set up that package deal by working the next command from the foundation of our app:
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
Let’s rename our _app.js
file to _app.tsx
contained in the pages
listing and change its content material with the next:
import { ChakraProvider } from "@chakra-ui/react";
import { AppProps } from "subsequent/app";
import Head from "subsequent/head";
import React from "react";
const App = ({ Part, pageProps }: AppProps) => {
return (
<>
<Head>
<hyperlink rel="shortcut icon" href="/pictures/favicon.ico" />
</Head>
<ChakraProvider>
<Part {...pageProps} />
</ChakraProvider>
</>
);
};
export default App;
Since we added a brand new TypeScript file, we’ll must restart our Subsequent.js server. As soon as we restart our server, we’ll get the next error:
$ yarn dev
yarn run v1.22.5
$ subsequent dev
prepared - began server on http://localhost:3000
It seems such as you're attempting to make use of TypeScript however do not have the required package deal(s) put in.
Please set up typescript, @varieties/react, and @varieties/node by working:
yarn add --dev typescript @varieties/react @varieties/node
If you're not attempting to make use of TypeScript, please take away the tsconfig.json file out of your package deal root (and any TypeScript information in your pages listing).
It’s because we added a brand new TypeScript file however didn’t add the required dependencies which might be required to run them. We will repair that by putting in the lacking dependencies. From the foundation of our app, we are able to execute the next command to put in the lacking dependencies:
yarn add --dev typescript @varieties/react @varieties/node
Now, if we begin our Subsequent.js server, our app ought to compile:
$ yarn dev
yarn run v1.22.5
$ subsequent dev
prepared - began server on http://localhost:3000
We detected TypeScript in your undertaking and created a tsconfig.json file for you.
occasion - compiled efficiently
Including NextAuth
NextAuth is an authentication library for Subsequent.js. It’s easy and simple to know, versatile and safe by default. To arrange NextAuth in our app, we’ll want to put in it by working the next command from the foundation of our app:
yarn add next-auth
Subsequent, we’ll must replace our pages/_app.tsx
file with the next content material:
import { ChakraProvider } from "@chakra-ui/react";
import { Supplier as NextAuthProvider } from "next-auth/shopper";
import { AppProps } from "subsequent/app";
import Head from "subsequent/head";
import React from "react";
const App = ({ Part, pageProps }: AppProps) => {
return (
<>
<Head>
<hyperlink rel="shortcut icon" href="/pictures/favicon.ico" />
</Head>
<NextAuthProvider session={pageProps.session}>
<ChakraProvider>
<Part {...pageProps} />
</ChakraProvider>
</NextAuthProvider>
</>
);
};
export default App;
Right here, we’re wrapping our app with NextAuthProvider
. Subsequent, we’ll must create a brand new file named [...nextauth].ts
contained in the pages/api/auth
listing with the next content material:
import { NextApiRequest, NextApiResponse } from "subsequent";
import NextAuth from "next-auth";
import Suppliers from "next-auth/suppliers";
const choices = {
suppliers: [
Providers.Twitter({
clientId: process.env.TWITTER_KEY,
clientSecret: process.env.TWITTER_SECRET,
}),
],
};
export default NextAuth(choices);
The above file might be chargeable for dealing with our authentication utilizing Subsequent.js API routes. Subsequent, we’ll create a brand new filed named .env
within the root of our app to retailer all the environment variables with the next content material:
DATABASE_URL="postgresql://postgres:@localhost:5432/postgres?synchronize=true"
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:3000
TWITTER_KEY=""
TWITTER_SECRET=""
The Twitter surroundings variables might be generated from the Twitter API. We’ll be doing that subsequent. We will create a brand new Twitter app from the Twitter Developer dashboard.
-
Create a brand new Twitter app by getting into its identify and click on on the Full button.
-
Copy the API key, API secret key and Bearer token within the subsequent display.
-
Change the App permissions from Learn Solely to Learn and Write within the subsequent display.
-
Click on on the Edit button subsequent to the Authentication settings to allow 3-legged OAuth.
-
Allow 3-legged OAuth and Request electronic mail deal with from customers and add http://localhost:3000/api/auth/callback/twitter as a Callback URL.
-
The Web site URL, Phrases of service and Privateness coverage information could be something (corresponding to
https://yourwebsite.com
,https://yourwebsite.com/phrases
andhttps://yourwebsite.com/privateness
respectively).
Our 3-legged OAuth ought to be enabled now.
Paste the worth of the API key from Step 2 into the TWITTER_KEY surroundings variable and the worth of API secret key into the TWITTER_SECRET surroundings variable.
Our .env
file ought to appear to be this now:
DATABASE_URL="postgresql://postgres:@localhost:5432/postgres"
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:3000
TWITTER_KEY="1234" // Substitute this with your individual API key
TWITTER_SECRET="secret" // Replaces this with your individual API secret key
Now, if we restart our Subsequent.js server and go to http://localhost:3000/api/auth/signin, we must always have the ability to see the Register with Twitter button:
If we click on on that button, we’ll have the ability to authorize our Twitter app however we gained’t have the ability to log in to our app. Our terminal will present the next error:
[next-auth][warn][jwt_auto_generated_signing_key]
https://next-auth.js.org/warnings
We’ll repair this difficulty subsequent after we’ll be including and configuring Prisma.
Including and Configuring Prisma
First, we have to set up all the required dependencies. We will try this by working the next command from the foundation of our app:
yarn add prisma @prisma/shopper
Subsequent, let’s create a brand new file named prisma.ts
contained in the lib/purchasers
listing with the next content material:
import { PrismaClient } from "@prisma/shopper";
const prisma = new PrismaClient();
export default prisma;
This PrismaClient
might be re-used throughout a number of information. Subsequent, we’ll must replace our pages/api/auth/[...nextauth].ts
file with the next content material:
....
import prisma from "../../../lib/purchasers/prisma";
import Adapters from "next-auth/adapters";
....
const choices = {
suppliers: [
....
],
adapter: Adapters.Prisma.Adapter({ prisma }),
};
....
Now, if we go to http://localhost:3000/api/auth/signin, we’ll get the next error on our terminal:
Error: @prisma/shopper didn't initialize but. Please run "prisma generate" and attempt to import it once more.
To repair this difficulty, we’ll must do the next:
- Run
npx prisma init
from the foundation of our app:
$ npx prisma init
Atmosphere variables loaded from .env
✔ Your Prisma schema was created at prisma/schema.prisma.
Now you can open it in your favourite editor.
warn Prisma would have added DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" however it already exists in .env
Subsequent steps:
1. Set the DATABASE_URL in the .env file to level to your present database. In case your database has no tables but, learn https://pris.ly/d/getting-started.
2. Set the supplier of the datasource block in schema.prisma to match your database: postgresql, mysql or sqlite.
3. Run prisma introspect to show your database schema right into a Prisma knowledge mannequin.
4. Run prisma generate to set up Prisma Consumer. You'll be able to then begin querying your database.
Extra info in our documentation:
https://pris.ly/d/getting-started
- Run
npx prisma generate
from the foundation of our app:
$ npx prisma generate
4s
Atmosphere variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Error:
You have no fashions outlined in your schema.prisma, so nothing might be generated.
You'll be able to outline a mannequin like this:
mannequin Person {
id Int @id @default(autoincrement())
electronic mail String @distinctive
identify String?
}
Extra info in our documentation:
https://pris.ly/d/prisma-schema
- Replace the
prisma/schema.prisma
file with the schema that NextAuth expects:
// prisma/schema.prisma
generator shopper {
supplier = "prisma-client-js"
}
datasource db {
supplier = "postgresql"
url = env("DATABASE_URL")
}
mannequin Account {
id Int @id @default(autoincrement())
compoundId String @distinctive @map("compound_id")
userId Int @map("user_id")
providerType String @map("provider_type")
providerId String @map("provider_id")
providerAccountId String @map("provider_account_id")
refreshToken String? @map("refresh_token")
accessToken String? @map("access_token")
accessTokenExpires DateTime? @map("access_token_expires")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
@@index([providerAccountId], identify: "providerAccountId")
@@index([providerId], identify: "providerId")
@@index([userId], identify: "userId")
@@map("accounts")
}
mannequin Session {
id Int @id @default(autoincrement())
userId Int @map("user_id")
expires DateTime
sessionToken String @distinctive @map("session_token")
accessToken String @distinctive @map("access_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
@@map("periods")
}
mannequin Person {
id Int @id @default(autoincrement())
identify String?
electronic mail String? @distinctive
emailVerified DateTime? @map("email_verified")
picture String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
tweets Tweet[]
@@map("customers")
}
mannequin VerificationRequest {
id Int @id @default(autoincrement())
identifier String
token String @distinctive
expires DateTime
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
@@map("verification_requests")
}
- Add the schema for Tweet within the
prisma/schema.prisma
file:
// prisma/schema.prisma
....
mannequin Tweet {
id Int @id @default(autoincrement())
physique String
userId Int
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
writer Person @relation(fields: [userId], references: [id])
@@map("tweets")
}
- Run
npx prisma migrate dev --preview-feature
from the foundation of our app to create a brand new migration. Enter the identify of the migration (corresponding to init-database) when prompted.
Now, if we go to http://localhost:3000/api/auth/signin and click on on the Register with Twitter button, we’ll be logged in to our app utilizing Twitter.
Including Some Seed Information
In order that the UI isn’t utterly naked as we work on the app, let’s add some seed knowledge.
Let’s begin off by putting in a few dependencies:
yarn add -D faker ts-node
This pulls in faker.js, which is able to help us in producing pretend knowledge, in addition to its ts-node dependency.
Subsequent, create a brand new seed.ts
file within the prisma
folder, and add the next content material:
import faker from "faker";
import prisma from "../lib/purchasers/prisma";
async operate fundamental() {
const listOfNewUsers = [...new Array(5)].map(() => {
return {
electronic mail: faker.web.electronic mail(),
identify: faker.identify.findName(),
picture: faker.picture.picture(),
tweets: {
create: {
physique: faker.lorem.sentence(),
},
},
};
});
for (let knowledge of listOfNewUsers) {
const person = await prisma.person.create({
knowledge,
});
console.log(person);
}
}
fundamental()
.catch((e) => {
console.error(e);
course of.exit(1);
})
.lastly(async () => {
await prisma.$disconnect();
});
We’ll additionally must replace our tsconfig.json
file, as proven:
{
"compilerOptions": {
"goal": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "protect",
"baseUrl": ".",
"paths": {
"*": [
"/*"
],
"parts/*": [
"components/*"
],
"pages/*": [
"pages/*"
],
"varieties/*": [
"types/*"
],
"lib/*": [
"lib/*"
],
},
},
"embrace": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
Lastly, we are able to run npx prisma db seed --preview-feature
to seed our database with some take a look at knowledge.
Including React Question
React Question is a very fashionable and performant method of fetching knowledge in React.js apps. Let’s add React Question to our app. We will set up React Question by working the next command from the foundation of our app:
yarn add react-query
Subsequent, let’s create a brand new file named react-query.ts
contained in the lib/purchasers
listing with the next content material:
import { QueryClient } from "react-query";
const queryClient = new QueryClient();
export default queryClient;
We’ll additionally must replace our pages/_app.tsx
file with the next content material:
....
import { QueryClientProvider } from "react-query";
import { Hydrate } from "react-query/hydration";
import queryClient from "../lib/purchasers/react-query";
const App = ({ Part, pageProps }: AppProps) => {
return (
<QueryClientProvider shopper={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Head>
<hyperlink rel="shortcut icon" href="/pictures/favicon.ico" />
</Head>
<NextAuthProvider session={pageProps.session}>
<ChakraProvider>
<Part {...pageProps} />
</ChakraProvider>
</NextAuthProvider>
</Hydrate>
</QueryClientProvider>
);
};
export default App;
Right here, we’re wrapping our app with QueryClientProvider, which is able to present a QueryClient
to our app.
Choice to View a Listing of Tweets
Let’s create a brand new file known as fetch-tweets.ts
contained in the lib/queries
listing, with the next content material:
const fetchTweets = async () => {
const res = await fetch(`${course of.env.NEXT_PUBLIC_API_URL}/api/tweets`);
const knowledge = await res.json();
return knowledge;
};
export default fetchTweets;
This operate might be chargeable for fetching all of the tweets in our app. Subsequent, create a brand new file known as tweets.tsx
contained in the pages
listing with the next content material:
import fetchTweets from "../lib/queries/fetch-tweets";
import queryClient from "../lib/purchasers/react-query";
import { GetServerSideProps, InferGetServerSidePropsType } from "subsequent";
import { useSession } from "next-auth/shopper";
import Head from "subsequent/head";
import React from "react";
import { useQuery } from "react-query";
import { dehydrate } from "react-query/hydration";
const TweetsPage: InferGetServerSidePropsType<
typeof getServerSideProps
> = ({}) => {
const { knowledge } = useQuery("tweets", fetchTweets);
const [session] = useSession();
if (!session) {
return <div>Not authenticated.</div>;
}
return (
<>
<Head>
<title>All tweets</title>
</Head>
{console.log(JSON.stringify(knowledge, null, 2))}
</>
);
};
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
await queryClient.prefetchQuery("tweets", fetchTweets);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
export default TweetsPage;
getServerSideProps is a Subsequent.js operate that helps in fetching knowledge on the server. Let’s additionally create a brand new file named index.ts
contained in the pages/api/tweets
listing with the next content material:
import prisma from "../../../lib/purchasers/prisma";
import sort { NextApiRequest, NextApiResponse } from "subsequent";
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.methodology === "POST") {
attempt {
const { physique } = req;
const tweet = await prisma.tweet.create({ knowledge: JSON.parse(physique) });
return res.standing(200).json(tweet);
} catch (error) {
return res.standing(422).json(error);
}
} else if (req.methodology === "GET") {
attempt {
const tweets = await prisma.tweet.findMany({
embrace: {
writer: true,
},
orderBy: [
{
createdAt: "desc",
},
],
});
return res.standing(200).json(tweets);
} catch (error) {
return res.standing(422).json(error);
}
}
res.finish();
};
Right here, we’re checking the request. If it’s a POST
request, we’re creating a brand new tweet. If it’s a GET
request, we’re sending all of the tweets with the small print of writer. Now, if we go to http://localhost:3000/tweets, we’ll view all of the tweets in our browser’s console.
Observe that, as faker.js generates random knowledge, what you see logged to your browser’s console will fluctuate from the screenshot. We’ll add the choice so as to add a tweet later.
Subsequent, let’s construct the person interface for exhibiting the record of tweets. We will create a brand new file named index.tsx
contained in the parts/pages/tweets
listing with the next content material:
import { Field, Grid, Stack } from "@chakra-ui/react";
import Tweet from "./tweet";
import React from "react";
import ITweet from "varieties/tweet";
const TweetsPageComponent = ({ tweets }) => {
return (
<Stack spacing={8}>
<Grid templateColumns={["1fr", "1fr", "repeat(2, 1fr)"]} hole={8}>
{tweets?.map((tweet: ITweet) => {
return (
<Field key={tweet.id}>
<Tweet tweet={tweet} />
</Field>
);
})}
</Grid>
</Stack>
);
};
export default TweetsPageComponent;
Let’s additionally create a brand new file named tweet.tsx
inside the identical listing (parts/pages/tweets
) with the next content material:
import { Avatar, Field, Stack, Textual content } from "@chakra-ui/react";
import React, { FC } from "react";
const Tweet: FC = ({ tweet }) => {
const authorNode = () => {
return (
<Stack
spacing={4}
isInline
alignItems="heart"
p={4}
borderBottomWidth={1}
>
<Avatar identify={tweet.writer.identify} src={tweet.writer.picture} />
<Stack>
<Textual content fontWeight="daring">{tweet.writer.identify}</Textual content>
</Stack>
</Stack>
);
};
const bodyNode = () => {
return (
<Textual content fontSize="md" p={4}>
{tweet.physique}
</Textual content>
);
};
return (
<Field shadow="lg" rounded="lg">
<Stack spacing={0}>
{authorNode()}
{bodyNode()}
</Stack>
</Field>
);
};
export default Tweet;
Subsequent, let’s replace our pages/tweets.tsx
file with the next content material:
....
import Web page from "../parts/pages/tweets";
....
const TweetsPage: InferGetServerSidePropsType<
typeof getServerSideProps
> = ({}) => {
....
return (
<>
<Head>
<title>All tweets</title>
</Head>
<Web page tweets={knowledge} />
</>
);
....
}
....
Right here, we’ve modified the interface of our app. Now, if we go to http://localhost:3000/tweets, we must always have the ability to see the next:
Choice to Add a New Tweet
Let’s add a textual content space via which we are able to add a brand new tweet. To do this, let’s create a brand new file named add-new-tweet-form.tsx
contained in the parts/pages/tweets
listing with the next content material:
import {
Field,
Button,
FormControl,
FormLabel,
Stack,
Textarea,
} from "@chakra-ui/react";
import saveTweet from "../../../lib/mutations/save-tweet";
import fetchTweets from "../../../lib/queries/fetch-tweets";
import queryClient from "../../../lib/purchasers/react-query";
import { useSession } from "next-auth/shopper";
import React, { ChangeEvent, useState } from "react";
import { useMutation, useQuery } from "react-query";
const AddNewTweetForm = () => {
const [body, setBody] = useState("");
const [session] = useSession();
const { refetch } = useQuery("tweets", fetchTweets);
const mutation = useMutation(saveTweet, {
onSuccess: async () => {
await queryClient.invalidateQueries("tweets");
refetch();
},
});
if (!session) {
return <div>Not authenticated.</div>;
}
const handleSubmit = () => {
const knowledge = {
physique,
writer: {
join: { electronic mail: session.person.electronic mail },
},
};
mutation.mutate(knowledge);
if (!mutation.error) {
setBody("");
}
};
return (
<Stack spacing={4}>
<Field p={4} shadow="lg" rounded="lg">
<Stack spacing={4}>
<FormControl isRequired>
<FormLabel htmlFor="physique">What's in your thoughts?</FormLabel>
<Textarea
id="physique"
worth={physique}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
setBody(e.currentTarget.worth)
}
/>
</FormControl>
<FormControl>
<Button
loadingText="Posting..."
onClick={handleSubmit}
isDisabled={!physique.trim()}
>
Put up
</Button>
</FormControl>
</Stack>
</Field>
</Stack>
);
};
export default AddNewTweetForm;
The mutation operate is chargeable for doing the POST
request to the server. It additionally re-fetches the info as soon as the request is profitable. Additionally, let’s create a brand new file named save-tweet.ts
contained in the lib/mutations
listing with the next content material:
const saveTweet = async (physique: any) => {
const res = await fetch(`${course of.env.NEXT_PUBLIC_API_URL}/api/tweets`, {
methodology: "POST",
physique: JSON.stringify(physique),
});
const knowledge = await res.json();
return knowledge;
};
export default saveTweet;
We additionally want to switch our parts/pages/tweets/index.tsx
file with following content material:
....
import AddNewTweetForm from "./add-new-tweet-form";
....
const TweetsPageComponent = ({ tweets }) => {
return (
<Stack spacing={8}>
<Field>
<AddNewTweetForm />
</Field>
....
</Stack>
);
};
export default TweetsPageComponent;
Now, we must always have the ability to view a textarea if we go to http://localhost:3000/tweets:
We must also have the ability to add a brand new tweet utilizing the textarea (this gained’t tweet to your precise account!):
Subsequent, we’ll add the choice to view the profile of a person which reveals solely the tweets posted by that person.
Choice to View the Profile of a Person with solely Their Tweets
First, we’ll create a web page that may present an inventory of all of the customers. To do this, we’ll must create a brand new file named index.tsx
contained in the pages/customers
listing with the next content material:
import { GetServerSideProps, InferGetServerSidePropsType } from "subsequent";
import { useSession } from "next-auth/shopper";
import Head from "subsequent/head";
import React from "react";
import { useQuery } from "react-query";
import { dehydrate } from "react-query/hydration";
import Web page from "../../parts/pages/customers";
import queryClient from "../../lib/purchasers/react-query";
import fetchUsers from "../../lib/queries/fetch-users";
const MyAccountPage: InferGetServerSidePropsType<
typeof getServerSideProps
> = ({}) => {
const { knowledge } = useQuery("customers", fetchUsers);
const [session] = useSession();
if (!session) {
return <div>Not authenticated.</div>;
}
return (
<>
<Head>
<title>All customers</title>
</Head>
<Web page customers={knowledge} />
</>
);
};
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
await queryClient.prefetchQuery("customers", fetchUsers);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
export default MyAccountPage;
We’ll additionally must create a brand new file named fetch-users.ts
contained in the lib/queries
listing with the next content material:
const fetchUsers = async () => {
const res = await fetch(`${course of.env.NEXT_PUBLIC_API_URL}/api/customers`);
const knowledge = await res.json();
return knowledge;
};
export default fetchUsers;
This operate might be chargeable for fetching all of the customers from the API endpoint. We’ll additionally must create a brand new file named index.tsx
contained in the parts/pages/customers
listing with the next content material:
import { Field, Grid, Stack } from "@chakra-ui/react";
import React from "react";
import Person from "./person";
const UsersPageComponent = ({ customers }) => {
return (
<Stack spacing={8}>
<Grid templateColumns={["1fr", "1fr", "repeat(2, 1fr)"]} hole={8}>
{customers?.map((person) => {
return (
<Field key={person.id}>
<Person person={person} />
</Field>
);
})}
</Grid>
</Stack>
);
};
export default UsersPageComponent;
Subsequent, let’s create a file named person.tsx
inside the identical listing (parts/pages/customers
) with the next content material:
import { Avatar, Field, Stack, Textual content, Button } from "@chakra-ui/react";
import Hyperlink from "subsequent/hyperlink";
import React, { FC } from "react";
const Person: FC = ({ person }) => {
const authorNode = () => {
return (
<Stack
spacing={4}
isInline
alignItems="heart"
p={4}
borderBottomWidth={1}
>
<Avatar identify={person.identify} src={person.picture} />
<Stack>
<Textual content fontWeight="daring">{person.identify}</Textual content>
</Stack>
</Stack>
);
};
const bodyNode = () => {
return (
<Textual content fontSize="md" p={4}>
{person.electronic mail}
</Textual content>
);
};
const buttonNode = () => {
return (
<Field p={4} borderTopWidth={1}>
<Hyperlink href={`/customers/${person.id}`}>
<Button>View profile</Button>
</Hyperlink>
</Field>
);
};
return (
<Field shadow="lg" rounded="lg">
<Stack spacing={0}>
{authorNode()}
{bodyNode()}
{buttonNode()}
</Stack>
</Field>
);
};
export default Person;
And yet another file named index.ts
contained in the pages/api/customers
listing with the next content material:
import prisma from "../../../lib/purchasers/prisma";
import sort { NextApiRequest, NextApiResponse } from "subsequent";
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.methodology === "GET") {
attempt {
const customers = await prisma.person.findMany({
orderBy: [
{
createdAt: "desc",
},
],
});
return res.standing(200).json(customers);
} catch (error) {
return res.standing(422).json(error);
}
}
res.finish();
};
The above operate is chargeable for sending the small print of all of the customers. Now, if we go to http://localhost:3000/customers, we must always have the ability to see an inventory of customers:
Now, let’s create the web page to indicate the small print for a single person. To do this, we’ll must create a brand new file named [id].tsx
contained in the pages/customers
listing with the next content material:
import Web page from "../../parts/pages/customers/[id]";
import queryClient from "../../lib/purchasers/react-query";
import fetchUser from "../../lib/queries/fetch-user";
import { GetServerSideProps, InferGetServerSidePropsType } from "subsequent";
import { getSession, useSession } from "next-auth/shopper";
import Head from "subsequent/head";
import React from "react";
import { useQuery } from "react-query";
import { dehydrate } from "react-query/hydration";
const MyAccountPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
id,
}) => {
const { knowledge } = useQuery("person", () => fetchUser(parseInt(id as string)));
const [session] = useSession();
if (!session) {
return <div>Not authenticated.</div>;
}
return (
<>
<Head>
<title>{session.person.identify}'s profile</title>
</Head>
<Web page person={knowledge} />
</>
);
};
export const getServerSideProps: GetServerSideProps = async ({ question }) => {
await queryClient.prefetchQuery("person", () =>
fetchUser(parseInt(question.id as string))
);
return {
props: {
dehydratedState: dehydrate(queryClient),
id: question.id,
},
};
};
export default MyAccountPage;
The worth of question.id
determines the id
of the present person. We’ll additionally must create a brand new file named fetch-user.ts
contained in the lib/queries
listing with the next content material:
const fetchUser = async (userId: quantity) => {
const res = await fetch(
`${course of.env.NEXT_PUBLIC_API_URL}/api/customers/${userId}`
);
const knowledge = await res.json();
return knowledge;
};
export default fetchUser;
The above operate might be chargeable for doing the GET
request to the API endpoint. Subsequent, we’ll must create a brand new file named index.tsx
contained in the parts/pages/customers/[id]
listing with the next content material:
import { Avatar, Field, Grid, Stack, Textual content } from "@chakra-ui/react";
import Tweet from "./tweet";
import React, { FC } from "react";
const UsersPageComponent: FC = ({ person }) => {
const authorNode = () => {
return (
<Stack spacing={4} isInline alignItems="heart">
<Avatar identify={person?.identify} src={person?.picture} />
<Stack>
<Textual content fontWeight="daring" fontSize="4xl">
{person?.identify}
</Textual content>
</Stack>
</Stack>
);
};
return (
<Stack spacing={8}>
{authorNode()}
<Grid templateColumns={["1fr", "1fr", "repeat(2, 1fr)"]} hole={8}>
{person?.tweets.map((tweet) => {
return (
<Field key={tweet.id}>
<Tweet tweet={tweet} />
</Field>
);
})}
</Grid>
</Stack>
);
};
export default UsersPageComponent;
Subsequent, we’ll must create yet another file named tweet.tsx
inside the identical listing (parts/pages/customers/[id]
) with the next content material:
import { Field, Stack, Textual content } from "@chakra-ui/react";
import React, { FC } from "react";
const Tweet: FC = ({ tweet }) => {
const bodyNode = () => {
return (
<Textual content fontSize="md" p={4}>
{tweet.physique}
</Textual content>
);
};
return (
<Field shadow="lg" rounded="lg">
<Stack spacing={0}>{bodyNode()}</Stack>
</Field>
);
};
export default Tweet;
Lastly, we’ll must create yet another file named [id].ts
contained in the pages/api/customers
listing with the next content material:
import prisma from "../../../lib/purchasers/prisma";
import sort { NextApiRequest, NextApiResponse } from "subsequent";
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.methodology === "GET") {
const userId = parseInt(req.question.id as string);
attempt {
const tweets = await prisma.person.findUnique({
embrace: {
tweets: true,
},
the place: {
id: userId,
},
});
return res.standing(200).json(tweets);
} catch (error) {
console.log(error);
return res.standing(422).json(error);
}
}
res.finish();
};
The above operate might be chargeable for sending the small print of the person whose id
is similar as req.question.id
. We’re changing it to a quantity, as Prisma requires it to be numeric. Now, if we go to http://localhost:3000/customers and click on on the View profile button for a person, we’ll have the ability to see an inventory of tweets posted by that person.
Conclusion
On this tutorial, we’ve discovered how we are able to use Subsequent.js and Prisma collectively to construct a clone of Twitter. Clearly, Twitter consists of plenty of different options like retweet, remark and sharing functionalities for every tweet. Nonetheless, this tutorial ought to present the bottom for constructing such options.
The code for the app we constructed is out there on GitHub. Be at liberty to test it out. You too can try a dwell demo of the app we’ve been constructing right here.



Mukesh Ambani once more falls to second richest Asian spot after briefly surpassing China’s Zhong Shanshan

Aswath Damodaran on the COVID Crucible: A Play in Three Acts

Blockchain and crypto will problem present finance, Nigeria VP says

33 Black Historical past Month Actions for February and Past

Entrance-Finish Efficiency Guidelines 2021 — Smashing Journal
