DEV Community

Cover image for πŸͺ„βœ¨How I built this Twitter scheduler using React and HasuraπŸ”₯
Kaushik Varanasi
Kaushik Varanasi

Posted on

πŸͺ„βœ¨How I built this Twitter scheduler using React and HasuraπŸ”₯

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


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. It's free.

Signup

And create a project

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
Enter fullscreen mode Exit fullscreen mode

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:

npm init -y
npm install express body-parser axios axios-oauth-1.0a
npm install @codegenie/serverless-express
Enter fullscreen mode Exit fullscreen mode

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

const serverlessExpress = require('@codegenie/serverless-express')
const app = require('./app')
exports.handler = serverlessExpress({ app })
Enter fullscreen mode Exit fullscreen mode

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

// 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

rgraph deploy <function name>
Enter fullscreen mode Exit fullscreen mode

Build the frontend πŸ‘Ό

Create project

cd client
npx create-next-app@latest my-app --typescript --tailwind --eslint
Enter fullscreen mode Exit fullscreen mode

Add shadcn ui

npx shadcn-ui@latest init
Enter fullscreen mode Exit fullscreen mode

Add necessary components for our app

npx shadcn-ui@latest add button popover calendar card input label
Enter fullscreen mode Exit fullscreen mode

Create UI

In src/app/page.tsx

"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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Create a file called src/app/date.tsx

"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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Deploy

And deploy the front-end on Vercel. Hooray, your app is live πŸ₯³

Top comments (3)

Collapse
 
get_pieces profile image
Pieces 🌟

Great article. Well explained too! πŸ‘

Collapse
 
kaushik94 profile image
Kaushik Varanasi

Thank you. You might want to checkout Rocketgraph. If you have any trouble using it, please reach out to me at kaushik@rocketgraph.io and I will give you a lifetime discount on our platform.

Collapse
 
get_pieces profile image
Pieces 🌟

I will be sure to check it out.