Connect with us

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:

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.

  1. Create a brand new Twitter app by getting into its identify and click on on the Full button.

    Create a new Twitter app

  2. Copy the API key, API secret key and Bearer token within the subsequent display.

    The credentials of our Twitter app

  3. Change the App permissions from Learn Solely to Learn and Write within the subsequent display.

    Twitter app permissions

  4. Click on on the Edit button subsequent to the Authentication settings to allow 3-legged OAuth.

    Authentication settings for our Twitter app

  5. 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.

    Edit the authentication settings of our Twitter app

  6. The Web site URL, Phrases of service and Privateness coverage information could be something (corresponding to https://yourwebsite.com, https://yourwebsite.com/phrases and https://yourwebsite.com/privateness respectively).

Our 3-legged OAuth ought to be enabled now.

Enable the 3-legged OAuth of our Twitter app

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:

Sign in 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:

  1. 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
  1. 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
  1. 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")
   }
  1. 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")
   }
  1. 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.

List of tweets from the API endpoint

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:

List of tweets

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:

Textarea to add new tweets

We must also have the ability to add a brand new tweet utilizing the textarea (this gained’t tweet to your precise account!):

Add a new tweet

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:

List of users

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.

Profile of a user with all the tweets posted by that user

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.



Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *