🪄✨How I built this Twitter scheduler using React and Hasura🔥
Published: 2024-01-20 12:00:05




## TL;DR 🔥
In this tutorial, you'll learn how to build a tweet scheduler. Enter your Twitter credentials and select a time and a date and your tweet will be tweeted at that time.

## Rocketgraph: An open-source replacement to Firebase 🚀
Just a quick background about us. [Rocketgraph](https://rocketgraph.io/) lets developers build web applications in minutes.
We do this by providing:
1. PostgresDB hosted on AWS RDS
2. Hasura console to GraphQLise your PostgresDB, manage your permissions, crons, schedulers and a lot more.
3. Built-in authentication that supports email/password, social, magic link and OTP
4. Serverless functions for any additional backend code.

[<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2184mdxeb8w8tjtkfia.png">](https://github.com/RocketsGraphQL/rgraph)

---

## Before we start

We are going to be using Hasura on-off schedulers to trigger Twitter API at the appropriate time and Rocketgraph's edge functions to setup the backend. So we need to create a project on [Rocketgraph](https://rocketgraph.io/). It's free.

### Signup
And create a project

<img width="100%" style="width:100%" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbnR0bnA1cHJzcGJiYW96eTByOTc5ZW9iYW4yNW9jNzRxbGk5cWZiayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/R4YW2LWcbwVMbr7yv5/source.gif">

It will setup a Hasura console. For now you don't need to know how to use it. Since we will be using it via the Hasura API.

## Let's set it up 🆙

We'll be using React.js and shadcn UI for the front-end and Node.js for our serverless functions.

create a folder for project, client and server:

```
mkdir twitter-app && cd twitter-app
mkdir client server
```

### Setting up Lambda functions

With Rocketgraph you can easily create lambda functions that act as your backend. To setup, note the project name from your project console on Rocketgraph:

```bash
npm init -y
npm install express body-parser axios axios-oauth-1.0a
npm install @codegenie/serverless-express
```
The last install is for express on the AWS Lambdas. You don't need to worry about that as Rocketgraph takes care of Lambda deployments.

Create a file `index.js` and add the following code

```js
const serverlessExpress = require('@codegenie/serverless-express')
const app = require('./app')
exports.handler = serverlessExpress({ app })
```

Now create a file named app.js and add the following code:

```js
// jshint esversion: 6

const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");
const addOAuthInterceptor = require("axios-oauth-1.0a");

const app = express();
const port = 3000

app.use(express.json());

const APP_URL = "https://guhys2s4uv24wbh6zoxl3by4xa0im.lambda-url.us-east-2.on.aws/tweet";



app.post("/tweet", function (req, res) {
    // Create a client whose requests will be signed
    const client = axios.create();
    // Specify the OAuth options
    const payload = req.body.payload;
    const options = {
        algorithm: 'HMAC-SHA1',
        key: `${payload.key}`,
        secret: `${payload.secret}`,
        token: `${payload.token}`,
        tokenSecret: `${payload.tokenSecret}`,
    };

    const data = {
        "text": `${payload.tweet}`
    };

    console.log("addOAuthInterceptor is not a function: ", addOAuthInterceptor)
    // Add interceptor that signs requests
    addOAuthInterceptor.default(client, options);

    console.log("params: ", req.body);
    client.post("https://api.twitter.com/2/tweets", 
    data)
    .then(response => {
        console.log("resp: ", response, req.body.key);
        res.status(200).send(`<h2> the result is : Tweeted </h2>` );
    })
    .catch(err => {
        console.log(err);

        res.status(500).send(`<h2> the result is : Error </h2>` );
    });
});

app.post("/schedule-tweet", function (req, res) {
    const body = JSON.parse(req.body);
    console.log("body", body)
    const jsonb = {
        "type": "create_scheduled_event",
        "args": {
            "webhook": `${APP_URL}`,
            "schedule_at": `${body.schedule_at}`,
            "include_in_metadata": true,
            "payload": {
                "tweet": `${body.tweet}`,
                "algorithm": 'HMAC-SHA1',
                "key": `${body.key}`,
                "secret": `${body.secret}`,
                "token": `${body.token}`,
                "tokenSecret": `${body.tokenSecret}`,
            },
            "comment": "sample scheduled event comment"
        }
    }
    console.log(req.body);

    axios.post("https://hasura-vlklxvo.rocketgraph.app/v1/metadata", jsonb, {
        "headers" : {
            "Content-Type": "application/json",
            "X-Hasura-Role":"admin",
            "X-hasura-admin-secret":"********"
        },   
    }).then(function (response) {
        console.log("response: ", response);
        res.status(200).send({
            message: "Your result is tweeted"
        })
    }).catch(function (error) {
        console.log("Error from Hasura: ", error);
        res.status(500).send(error)
    });
})

app.listen(port, () => console.log(`calculator listening on port ${port}!`))

module.exports = app;
```

Notice the `APP_URL`? Leave it empty for now. We will get this URL once we deploy the function to prod. So now let's go ahead and do that.

Also replace `X-hasura-admin-secret` and HASURA_URL with the ones shown on your project's Hasura tab

## Deploy the function ⛈️

Rgraph provides a cute CLI to assist you to develop your functions to AWS lambda:

```
npm install -g rgraph@0.1.10
```

And with your project name, execute this to deploy your function:

```
rgraph deploy <function name>
```

## Build the frontend 👼

### Create project

```
cd client
npx create-next-app@latest my-app --typescript --tailwind --eslint
```

Add shadcn ui

```
npx shadcn-ui@latest init
```

## Add necessary components for our app

```
npx shadcn-ui@latest add button popover calendar card input label
```

### Create UI

In `src/app/page.tsx`

```js
"use client"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import React from "react"
import { useState } from "react"
import { DatePickerDemo } from "./date"


type ScheduleTweetInput = {
  key: String,
  secret: String,
  token: String,
  tokenSecret: String,
  tweet: String,
  date: any,
  time: any,
}

const sendTweet = async (twitterData: ScheduleTweetInput) => {
  const { date, time, tweet, key, secret, token, tokenSecret } = twitterData;
  console.log(time.split(":"))
  date.setHours(...time.split(":"))
  console.log(twitterData, date);
  const isoDate = date.toISOString()
  console.log(isoDate);
  const url = "https://guhys2s4uv24wbh6zoxl3by4xa0imsdb.lambda-url.us-east-2.on.aws/schedule-tweet"
  const postData = {
      "tweet": tweet,
      "schedule_at": isoDate,
      "key": key,
      "secret": secret,
      "token": token,
      "tokenSecret": tokenSecret
  }


  let headersList = {
    "Accept": "*/*",
    "User-Agent": "Thunder Client (https://www.thunderclient.com)",
    "Content-Type": "application/json"
   }
   
   let bodyContent = JSON.stringify(postData);
   
   let response = await fetch(url, { 
     method: "POST",
     body: bodyContent,
     headers: headersList,
     mode: "no-cors"
   });
   
   let data = await response.text();
   console.log(data, response.status, response.json);
   console.log("Status", response.status, response);
}

export default function DemoCreateAccount() {
  const [key, setKey] = useState("");
  const [token, setToken] = useState("");
  const [tokenSecret, setTokenSecret] = useState("");
  const [tweet, setTweet] = useState("");
  const [secret, setSecret] = useState("");
  const [date, setDate] = React.useState<Date>()
  const [time, setTime] = useState<any>('10:00');

  console.log(key, "key")

  return (
    <Card>
      <CardHeader className="space-y-1">
        <CardTitle className="text-2xl">Twitter scheduler</CardTitle>
        <CardDescription>
          Enter your credentials below to schedule a tweet
        </CardDescription>
      </CardHeader>
      <CardContent className="grid gap-4">
        <div className="grid gap-2">
          <Label htmlFor="email">API Key</Label>
          <Input id="email" type="email" placeholder="API Key" onChange={(e) => setKey(e.target.value)}/>
        </div>
        <div className="grid gap-2">
          <Label htmlFor="email">API Secret</Label>
          <Input id="email" type="email" placeholder="API Secret" onChange={(e) => setSecret(e.target.value)}/>
        </div>
        <div className="grid gap-2">
          <Label htmlFor="email">Access Token</Label>
          <Input id="email" type="email" placeholder="Access Token" onChange={(e) => setToken(e.target.value)} />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="email">Access Secret</Label>
          <Input id="email" type="email" placeholder="Access Secret" onChange={(e) => setTokenSecret(e.target.value)}/>
        </div>
        <div className="grid gap-2">
          <Label htmlFor="email">Tweet</Label>
          <Input id="email" type="email" placeholder="Tweet your tweet here" onChange={(e) => setTweet(e.target.value)}/>
        </div>
        {DatePickerDemo(setDate, date)}
        <input aria-label="Time" type="time" onChange={(e) => setTime(e.target.value)}/>
      </CardContent>
      <CardFooter>
        <Button className="w-full" onClick={() => sendTweet({
          key,
          token,
          tokenSecret,
          date,
          time,
          tweet,
          secret,
        })}>Schedule tweet</Button>
      </CardFooter>
    </Card>
  )
}
```

Create a file called `src/app/date.tsx`

```js
"use client"

import * as React from "react"
import { CalendarIcon } from "@radix-ui/react-icons"
import { format } from "date-fns"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export function DatePickerDemo(onChange: any, date: any) {

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant={"outline"}
          className={cn(
            "w-[240px] justify-start text-left font-normal",
            !date && "text-muted-foreground"
          )}
        >
          <CalendarIcon className="mr-2 h-4 w-4" />
          {date ? format(date, "PPP") : <span>Pick a date</span>}
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0" align="start">
        <Calendar
          mode="single"
          selected={date}
          onSelect={onChange}
          initialFocus
        />
      </PopoverContent>
    </Popover>
  )
}
```

## Deploy

And deploy the front-end on Vercel. Hooray, your app is live 🥳