🤯 Get visibility on X (Twitter): Schedule your posts with NextJS 🚀🚀
Tags: webdev, programming, tutorial, react
Published: 2023-10-11 12:55:25

# TL;DR

In this tutorial, you will learn how to create an X (Twitter) post scheduler 🔥
- Authenticates users via X (Twitter).
- Schedule posts and save them to Supabase.
- Post to X (Twitter) with Trigger.dev.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fa9izjmafzgrtb9j9itz.gif)

<hr />

## Your background job management for NextJS

[Trigger.dev](https://trigger.dev/) is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

If you can spend 10 seconds giving us a star, I would be super grateful 💖
https://github.com/triggerdotdev/trigger.dev

[![GiveStar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wsljq85zi71b0pksld7.png)](https://github.com/triggerdotdev/trigger.dev
)

<hr />

## Let's set it up 🔥

Let's create a TypeScript Next.js application by running the code snippet below.

```bash
npx create-next-app x-post-scheduler
```

Install the [React Icons](https://react-icons.github.io/react-icons/) and the [Headless UI](https://headlessui.com/) packages. React Icons enable us to use different icons within the application, and we'll leverage Headless UI to add [animated custom modals](https://headlessui.com/react/dialog) to the application.

```bash
npm install @headlessui/react react-icons
```


![Schedule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7h9s4js023y6a0x7x1w.gif)

<hr />

### The right way for authentication 🔐

Here, I'll walk you through how to add X authentication to your Next.js application and enable permissions to send posts on behalf of your users.

To create a Twitter Developers’ project, you must have an [X account](https://twitter.com/home). Visit the [homepage and create a new project](https://developer.twitter.com/en/portal/dashboard).


![ScheduleTweets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1jxow00jo317qkrdkpl.png)

Provide a suitable project name and answer all the required questions.


![name your project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/249azavsajefumd5eyw3.png)


Next, create an App under the project and copy the tokens generated into a `.env.local` file within your Next.js project.

```bash
TWITTER_API_KEY=<your_api_key>
TWITTER_API_SECRET=<your_api_secret>
TWITTER_BEARER_TOKEN=<your_bearer_token>
```

![ScrollDownThePage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xf5h6wdbvilsbtjl286r.png)

Scroll down the page and set up user authentication to enable users to sign in to your application via X.


![Scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xnybbxbk00j3vo06ekkc.png)

Select `Read and write` as the app permission, enable `Request email from users`, and select `Web app` as the type of app.


![SelectReadWrite](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akro4bpommgd4nwr52mo.png)


Scroll down to the next section and provide your app's callback URL, website URL, and the required information. If you are using the Next.js development server, you may use the same inputs in the image below. After authentication, users are redirected to `http://www.localhost:3000/dashboard`, which is another route on the client side.


![ScrollDownToTheNext](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcq1ku1moifw8qz2unzn.png)

After setting up the authentication process, save the OAuth 2.0 Client ID and secret into the `.env.local` file.

```bash
TWITTER_CLIENT_ID=<app_client_id>
TWITTER_CLIENT_SECRET=<app_client_secret>
```

### Adding X authentication to Next.js

Create a `Sign in with Twitter` link element within the `index.ts` file that redirects users to X and allows them to grant your app access to their profile.

```tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
import Link from "next/link";

export default function Home() {
	const router = useRouter();

	const getTwitterOauthUrl = () => {
		const rootUrl = "https://twitter.com/i/oauth2/authorize";
		const options = {
			redirect_uri: "<your_callback_URL>",
			client_id: process.env.TWITTER_CLIENT_ID!,
			state: "state",
			response_type: "code",
			code_challenge: "y_SfRG4BmOES02uqWeIkIgLQAlTBggyf_G7uKT51ku8",
			code_challenge_method: "S256",
			//👇🏻 required scope for authentication and posting tweets
			scope: ["users.read", "tweet.read", "tweet.write"].join(" "),
		};
		const qs = new URLSearchParams(options).toString();
		return `${rootUrl}?${qs}`;
	};

	return (
		<main
			className={`flex items-center flex-col justify-center min-h-screen ${inter.className}`}
		>
			<h2 className='font-bold text-2xl '>X Scheduler</h2>
			<p className='mb-3 text-md'>Get started and schedule posts</p>
			<Link
				href={getTwitterOauthUrl()}
				className='bg-blue-500 py-3 px-4 text-gray-50 rounded-lg'
			>
				Sign in with Twitter
			</Link>
		</main>
	);
}
```

From the code snippet above, the `Sign in with Twitter` link executes the `getTwitterOauthUrl` function, redirecting users to X and enabling them to grant the app access to their profile.


![Redirect](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bupugq7kukie9pv9mmpz.gif)

When users authorize your app, they are redirected to the callback URL page, and you need to access the `code` parameter added to the URL and send this code to the server for further processing.

```
http://www.localhost:3000/dashboard?state=state&code=WTNhMUFYSDQwVnBsbEFoWGM0cmIwMWhKd3lJOFM1Q3FuVEdtdE5ESU1mVjIwOjE2OTY3NzMwMTEyMzc6M
```

Next, create the `/dashboard` client route by creating a `dashboard.tsx` file that extracts the code parameter from the URL and sends it to the server when the component mounts.

```tsx
import React, { useCallback, useEffect } from "react";

const Dashboard = () => {
	const sendAuthRequest = useCallback(async (code: string | null) => {
		try {
			const request = await fetch("/api/twitter/auth", {
				method: "POST",
				body: JSON.stringify({ code }),
				headers: {
					"Content-Type": "application/json",
				},
			});
			const response = await request.json();
			console.log("RES >>>", response);
		} catch (err) {
			console.error(err);
		}
	}, []);

	useEffect(() => {
		const params = new URLSearchParams(window.location.href);
		const code = params.get("code");
		sendAuthRequest(code);
	}, [sendAuthRequest]);

	return (
		<main className='w-full min-h-screen'>
			<p>Dashboard</p>
		</main>
	);
};

export default Dashboard;
```

The code snippet above retrieves the code parameter from the URL and sends it to an `/api/twitter/auth` endpoint on the server where the user will be authenticated.

On the server, the code received fetches the user's access token. With this token, you can retrieve the user's details and save or send them to the client.

Therefore, create an `api/twitter/auth.ts` file (server route) that receives the code parameter from the client. Copy the code snippet below to the top of the file.

```tsx
import type { NextApiRequest, NextApiResponse } from "next";

const BasicAuthToken = Buffer.from(
	`${process.env.TWITTER_CLIENT_ID!}:${process.env.TWITTER_CLIENT_SECRET!}`,
	"utf8"
).toString("base64");

const twitterOauthTokenParams = {
	client_id: process.env.TWITTER_CLIENT_ID!,
//👇🏻 according to the code_challenge provided on the client
	code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
	redirect_uri: `<your_callback_URL>`,
	grant_type: "authorization_code",
};

//gets user access token
export const fetchUserToken = async (code: string) => {
	try {
		const formatData = new URLSearchParams({
			...twitterOauthTokenParams,
			code,
		});
		const getTokenRequest = await fetch(
			"https://api.twitter.com/2/oauth2/token",
			{
				method: "POST",
				body: formatData.toString(),
				headers: {
					"Content-Type": "application/x-www-form-urlencoded",
					Authorization: `Basic ${BasicAuthToken}`,
				},
			}
		);
		const getTokenResponse = await getTokenRequest.json();
		return getTokenResponse;
	} catch (err) {
		return null;
	}
};

//gets user's data from the access token
export const fetchUserData = async (accessToken: string) => {
	try {
		const getUserRequest = await fetch("https://api.twitter.com/2/users/me", {
			headers: {
				"Content-type": "application/json",
				Authorization: `Bearer ${accessToken}`,
			},
		});
		const getUserProfile = await getUserRequest.json();
		return getUserProfile;
	} catch (err) {
		return null;
	}
};
```

- From the code snippet above,
    - The `BasicAuthToken` variable contains the encoded version of your tokens.
    - The `twitterOauthTokenParams` contains the parameters required for getting the users' access token.
    - The `fetchUserToken` function sends a request to Twitter's endpoint and returns the user's access token, and the `fetchUserData` function accepts the token and retrieves the user's X profile.

Finally, create the endpoint as done below.

```tsx
export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	const { code } = req.body;
	try {
		const tokenResponse = await fetchUserToken(code);
		const accessToken = tokenResponse.access_token;
		if (accessToken) {
			const userDataResponse = await fetchUserData(accessToken);
			const userCredentials = { ...tokenResponse, ...userDataResponse };
			return res.status(200).json(userCredentials);
		}
	} catch (err) {
		return res.status(400).json({ err });
	}
}
```

The code snippet above retrieves the user's access token and profile details using the functions declared above, merge them into a single object, and sends them back to the client.

Congratulations! You've successfully added X (Twitter) authentication to your Next.js application.

<hr />

## Building the schedule dashboard ⏰

In this section, you'll learn how to create the user interface for the application by building a calendar-like table to enable users to add and delete scheduled posts within each cell.


![BeforeWeProceed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/djdiwl7l5hvia71sw8dj.gif)

Before we proceed, create a `utils/util.ts` file. It will contain some of the functions used within the application.

```bash
mkdir utils
cd utils
touch util.ts
```

Copy the code snippet below into the `util.ts` file. It describes the structure of the table header and its contents (time and schedule). They will mapped into the table on the user interface.

```tsx
export interface Content {
	minutes?: number;
	content?: string;
	published?: boolean;
	day?: number;
}
export interface AvailableScheduleItem {
	time: number;
	schedule: Content[][];
}
// table header
export const tableHeadings: string[] = [
	"Time",
	"Sunday",
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday",
];

// table contents
export const availableSchedule: AvailableScheduleItem[] = [
	{
		time: 0,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 1,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 2,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 3,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 4,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 5,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 6,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 7,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 8,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 9,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 10,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 11,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 12,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 13,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 14,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 15,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 16,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 17,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 18,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 19,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 20,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 21,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 22,
		schedule: [[], [], [], [], [], [], []],
	},
	{
		time: 23,
		schedule: [[], [], [], [], [], [], []],
	},
];
```

Add the code snippet below into the file to help format the time into a more readable format for your users.

```tsx
export const formatTime = (value: number) => {
	if (value === 0) {
		return `Midnight`;
	} else if (value < 10) {
		return `${value}am`;
	} else if (value >= 10 && value < 12) {
		return `${value}am`;
	} else if (value === 12) {
		return `${value}noon`;
	} else {
		return `${value % 12}pm`;
	}
};
```

Update the `dashboard.tsx` file to display the table header and contents on the webpage within a table.

```tsx
"use client";
import React, { useState } from "react";
import {
	Content,
	availableSchedule,
	formatTime,
	tableHeadings,
} from "@/utils/util";
import { FaClock } from "react-icons/fa6";

const Dashboard = () => {
	const [yourSchedule, updateYourSchedule] = useState(availableSchedule);

	//👇🏻 add scheduled post
	const handleAddPost = (id: number, time: number) => {
		console.log({ id, time });
	};

	//👇🏻 delete scheduled post
	const handleDeletePost = (
		e: React.MouseEvent<HTMLParagraphElement>,
		content: Content,
		time: number
	) => {
		e.stopPropagation();
		if (content.day !== undefined) {
			console.log({ time, content });
		}
	};

	return (
		<main className='w-full min-h-screen'>
			<header className='w-full flex items-center mb-6 justify-center'>
				<h2 className='text-center font-extrabold text-3xl mr-2'>
					Your Post Schedules
				</h2>
				<FaClock className='text-3xl text-pink-500' />
			</header>
			<div className=' p-8'>
				<div className='w-full h-[80vh] overflow-y-scroll'>
					<table className='w-full border-collapse'>
						<thead>
							<tr>
								{tableHeadings.map((day, index) => (
									<th
										key={index}
										className='bg-[#F8F0DF] text-lg p-4 font-bold'
									>
										{day}
									</th>
								))}
							</tr>
						</thead>
						<tbody>
							{yourSchedule.map((item, index) => (
								<tr key={index}>
									<td className='bg-[#F8F0DF] text-lg font-bold'>
										{formatTime(item.time)}
									</td>
									{item.schedule.map((sch, id) => (
										<td
											key={id}
											onClick={() => handleAddPost(id, item.time)}
											className='cursor-pointer'
										>
											{sch.map((content, ind: number) => (
												<div
													key={ind}
													onClick={(e) =>
														handleDeletePost(e, content, item.time)
													}
													className={`p-3 ${
														content.published ? "bg-pink-500" : "bg-green-600"
													}  mb-2 rounded-md text-xs cursor-pointer`}
												>
													<p className='text-gray-700 mb-2'>
														{content.minutes === 0
															? "o'clock"
															: `at ${content.minutes} minutes past`}
													</p>
													<p className=' text-white'>{content.content}</p>
												</div>
											))}
										</td>
									))}
								</tr>
							))}
						</tbody>
					</table>
				</div>
			</div>
		</main>
	);
};

export default Dashboard;
```

The `handleAddPost` function runs when the user clicks on an empty cell on the table, and the `handleDeletePost` is executed when the user clicks on a scheduled post within a cell.

Within the `util.ts` file, create the Typescript interface for newly added posts and selected posts to be deleted. The `DelSectedCell` Typescript interface defines the properties of a post within a cell, and the `SelectedCell` represents the structure of the newly added post.

```tsx
export interface DelSelectedCell {
	content?: string;
	day_id?: number;
	day?: string;
	time_id?: number;
	time?: string;
	minutes?: number;
}
export interface SelectedCell {
	day_id?: number;
	day?: string;
	time_id?: number;
	time?: string;
	minutes?: number;
}
```

Import the `DelSelectedCell` and `SelectedCell` interfaces into the `dashboard.tsx` file and create the states holding both data structures. Add two boolean states that display the needed modal when the user performs an add or delete event.

```tsx
const [selectedCell, setSelectedCell] = useState<SelectedCell>({
	day_id: 0,
	day: "",
	time_id: 0,
	time: "",
});
const [delSelectedCell, setDelSelectedCell] = useState<DelSelectedCell>({
	content: "",
	day_id: 0,
	day: "",
	time_id: 0,
	time: "",
	minutes: 0,
});
//👇🏻 triggers the add post modal
const [addPostModal, setAddPostModal] = useState(false);
//👇🏻 triggers the delete post modal
const [deletePostModal, setDeletePostModal] = useState(false);
```

Modify the `handleAddPost` and `handleDeletePost` functions to update the recently added states as follows.

```tsx
const handleAddPost = (id: number, time: number) => {
	setSelectedCell({
		day_id: id + 1,
		day: tableHeadings[id + 1],
		time_id: time,
		time: formatTime(time),
	});
	setAddPostModal(true);
};

const handleDeletePost = (
	e: React.MouseEvent<HTMLParagraphElement>,
	content: Content,
	time: number
) => {
	e.stopPropagation();
	if (content.day !== undefined) {
		setDelSelectedCell({
			content: content.content,
			day_id: content.day,
			day: tableHeadings[content.day],
			time_id: time,
			time: formatTime(time),
			minutes: content.minutes,
		});
		setDeletePostModal(true);
	}
};
```

### The Delete and Add post modals with Headless UI

In this section, how to add posts when a user clicks on a cell and delete posts from the schedule when users click on a specific post in a cell.

Create a components folder containing the add and delete post modals.

```bash
mkdir components
cd components
touch AddPostModal.tsx DeletePostModal.tsx
```

Display both components within the `dashboard.tsx` when the user wants to add or delete a post.

```tsx
return (
	<main>
		{/*-- other dashboard elements --*/}
		{addPostModal && (
			<AddPostModal
				setAddPostModal={setAddPostModal}
				addPostModal={addPostModal}
				selectedCell={selectedCell}
				yourSchedule={yourSchedule}
				updateYourSchedule={updateYourSchedule}
				profile={username}
			/>
		)}
		{deletePostModal && (
			<DeletePostModal
				setDeletePostModal={setDeletePostModal}
				deletePostModal={deletePostModal}
				delSelectedCell={delSelectedCell}
				yourSchedule={yourSchedule}
				updateYourSchedule={updateYourSchedule}
				profile={username}
			/>
		)}
	</main>
);
```

Both components accept the schedule, the states containing the post to be added or deleted, and the user's X username as props.

Copy the code snippet below into the `AddPostModal.tsx` file.

```tsx
import {
	SelectedCell,
	AvailableScheduleItem,
	updateSchedule,
} from "@/utils/util";
import { Dialog, Transition } from "@headlessui/react";
import {
	FormEventHandler,
	Fragment,
	useState,
	Dispatch,
	SetStateAction,
} from "react";

interface Props {
	setAddPostModal: Dispatch<SetStateAction<boolean>>;
	updateYourSchedule: Dispatch<SetStateAction<AvailableScheduleItem[]>>;
	addPostModal: boolean;
	selectedCell: SelectedCell;
	profile: string | any;
	yourSchedule: AvailableScheduleItem[];
}

const AddPostModal: React.FC<Props> = ({
	setAddPostModal,
	addPostModal,
	selectedCell,
	updateYourSchedule,
	profile,
	yourSchedule,
}) => {
	const [content, setContent] = useState<string>("");
	const [minute, setMinute] = useState<number>(0);
	const closeModal = () => setAddPostModal(false);

const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
		e.preventDefault();
}
	return ()
}
```

The code snippet above accepts the values passed via props into the component. The content and minute states hold the post's content and the post's scheduled minute. The `handleSubmit` function is executed when the user clicks the `Save` button to schedule a post.

Return the JSX elements below from the `AddPostModal` component. The code snippet below uses the [Headless UI components](https://headlessui.com/react/dialog) to render an animated and already customised modal.

```tsx
<div>
	<Transition appear show={addPostModal} as={Fragment}>
		<Dialog as='div' className='relative z-10' onClose={closeModal}>
			<Transition.Child
				as={Fragment}
				enter='ease-out duration-300'
				enterFrom='opacity-0'
				enterTo='opacity-100'
				leave='ease-in duration-200'
				leaveFrom='opacity-100'
				leaveTo='opacity-0'
			>
				<div className='fixed inset-0 bg-black bg-opacity-80' />
			</Transition.Child>

			<div className='fixed inset-0 overflow-y-auto'>
				<div className='flex min-h-full items-center justify-center p-4 text-center'>
					<Transition.Child
						as={Fragment}
						enter='ease-out duration-300'
						enterFrom='opacity-0 scale-95'
						enterTo='opacity-100 scale-100'
						leave='ease-in duration-200'
						leaveFrom='opacity-100 scale-100'
						leaveTo='opacity-0 scale-95'
					>
						<Dialog.Panel className='w-full max-w-xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all'>
							<Dialog.Title
								as='h3'
								className='text-xl font-bold leading-6 text-gray-900'
							>
								Schedule a post on {selectedCell.day} by {selectedCell.time}
							</Dialog.Title>

							<form className='mt-2' onSubmit={handleSubmit}>
								{minute > 59 && (
									<p className='text-red-600'>
										Error, please minute must be less than 60
									</p>
								)}
								<label htmlFor='minute' className='opacity-60'>
									How many minutes past?
								</label>
								<input
									type='number'
									className='w-full border-[1px] px-4 py-2 rounded-md mb-2'
									name='title'
									id='title'
									value={minute.toString()}
									onChange={(e) => setMinute(parseInt(e.target.value, 10))}
									max={59}
									required
								/>

								<label htmlFor='content' className='opacity-60'>
									Post content
								</label>
								<textarea
									className='w-full border-[1px] px-4 py-2 rounded-md mb-2 text-sm'
									name='content'
									id='content'
									value={content}
									onChange={(e) => setContent(e.target.value)}
									required
								/>

								<div className='mt-4 flex items-center justify-between space-x-4'>
									<button
										type='submit'
										className='inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
									>
										Save
									</button>

									<button
										type='button'
										className='inline-flex justify-center rounded-md border border-transparent bg-red-100 px-4 py-2 text-sm font-medium text-red-900 hover:bg-red-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2'
										onClick={closeModal}
									>
										Cancel
									</button>
								</div>
							</form>
						</Dialog.Panel>
					</Transition.Child>
				</div>
			</div>
		</Dialog>
	</Transition>
</div>
```


![AsDownBelow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ms6dzup9rjdqyajb1b1.gif)

Update the `handleSubmit` function as done below.

```tsx
const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
	e.preventDefault();
	if (
		Number(minute) < 60 &&
		content.trim().length > 0 &&
		selectedCell.time_id !== undefined &&
		selectedCell.day_id !== undefined
	) {
		const newSchedule = [...yourSchedule];
		const selectedDay =
			newSchedule[selectedCell.time_id].schedule[selectedCell.day_id - 1];
		selectedDay.push({
			content,
			published: false,
			minutes: minute,
			day: selectedCell.day_id,
		});
		updateYourSchedule(newSchedule);
		closeModal();
	}
};
```

The function first validates if the minute entered by the user is valid, ensures the content is not empty, and confirms that the selected time and day are defined. Then, it retrieves the user's post schedule, identifies the selected day scheduled for the post, and updates the array with the new post details.

![ValidateEntry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3pofpl782sjcoy6oc2td.gif)

Add the code snippet below into the `DeletePostModal.tsx` file.

```tsx
"use client";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, Dispatch, SetStateAction } from "react";
import {
	DelSelectedCell,
	AvailableScheduleItem,
	updateSchedule,
} from "../utils/util";

interface Props {
	setDeletePostModal: Dispatch<SetStateAction<boolean>>;
	deletePostModal: boolean;
	delSelectedCell: DelSelectedCell;
	profile: string | any;
	yourSchedule: AvailableScheduleItem[];
	updateYourSchedule: Dispatch<SetStateAction<AvailableScheduleItem[]>>;
}

const DeletePostModal: React.FC<Props> = ({
	setDeletePostModal,
	deletePostModal,
	delSelectedCell,
	yourSchedule,
	updateYourSchedule,
	profile,
}) => {
	const closeModal = () => setDeletePostModal(false);

	const handleDelete = () => {};

	return <main>{/**-- JSX elements --**/}</main>;
};
```

Render these JSX elements within the `DeletePostModal` component.

```tsx
return (
	<div>
		<Transition appear show={deletePostModal} as={Fragment}>
			<Dialog as='div' className='relative z-10' onClose={closeModal}>
				<Transition.Child
					as={Fragment}
					enter='ease-out duration-300'
					enterFrom='opacity-0'
					enterTo='opacity-100'
					leave='ease-in duration-200'
					leaveFrom='opacity-100'
					leaveTo='opacity-0'
				>
					<div className='fixed inset-0 bg-black bg-opacity-25' />
				</Transition.Child>

				<div className='fixed inset-0 overflow-y-auto'>
					<div className='flex min-h-full items-center justify-center p-4 text-center'>
						<Transition.Child
							as={Fragment}
							enter='ease-out duration-300'
							enterFrom='opacity-0 scale-95'
							enterTo='opacity-100 scale-100'
							leave='ease-in duration-200'
							leaveFrom='opacity-100 scale-100'
							leaveTo='opacity-0 scale-95'
						>
							<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all'>
								<Dialog.Title
									as='h3'
									className='text-xl font-bold leading-6 text-gray-900'
								>
									Delete post
								</Dialog.Title>
								<div className='mt-2'>
									<p className='mb-3'>Are you sure you want to delete?</p>
									<p className='text-sm text-gray-500'>
										{`"${delSelectedCell.content}" scheduled for ${delSelectedCell.day} at ${delSelectedCell.time_id}:${delSelectedCell.minutes}`}
									</p>
								</div>

								<div className='mt-4 flex items-center justify-between space-x-4'>
									<button
										type='button'
										className='inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
										onClick={handleDelete}
									>
										Yes
									</button>
									<button
										type='button'
										className='inline-flex justify-center rounded-md border border-transparent bg-red-100 px-4 py-2 text-sm font-medium text-red-900 hover:bg-red-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2'
										onClick={closeModal}
									>
										Cancel
									</button>
								</div>
							</Dialog.Panel>
						</Transition.Child>
					</div>
				</div>
			</Dialog>
		</Transition>
	</div>
);
```

Modify the `handleDelete` function to accept the properties of the selected post and remove it from the schedule.

```tsx
const handleDelete = () => {
	if (
		delSelectedCell.time_id !== undefined &&
		delSelectedCell.day_id !== undefined
	) {
		//👇🏻 gets the user's post schedule
		const initialSchedule = [...yourSchedule];
		//👇🏻 gets the exact day the post is scheduled for
		let selectedDay =
			initialSchedule[delSelectedCell.time_id].schedule[
				delSelectedCell.day_id - 1
			];
		//👇🏻 filters the array to remove the selected post
		const updatedPosts = selectedDay.filter(
			(day) =>
				day.content !== delSelectedCell.content &&
				day.minutes !== delSelectedCell.minutes
		);
		//👇🏻 updates the schedule
		initialSchedule[delSelectedCell.time_id].schedule[
			delSelectedCell.day_id - 1
		] = updatedPosts;
		//👇🏻 updates the schedule
		updateYourSchedule(initialSchedule);
		closeModal();
	}
};
```


![Congratulations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smrnzkgyfgi33278lqhe.gif)

Congratulations on making it thus far! You can now add and remove scheduled posts from each cell. Next, let's connect a backend database (Supabase) to the application to persist data when the page is refreshed.

<hr />

## Saving everything to the database 📀

[Supabase](https://supabase.com/) is an open-source Firebase alternative that enables you to add authentication, file storage, Postgres, and real-time database to your software applications. With Supabase, you can build secured and scalable applications in a few minutes.

In this section, you'll learn how to integrate Supabase into your Next.js application and save and update the user's schedule via Supabase. Before we proceed, install the Supabase package.

```tsx
npm install @supabase/supabase-js
```

Visit the [Supabase homepage](https://supabase.com/) and create a new organization and project.


![Supa1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bs9yhr0vz3zb5p3lqps.png)

To set up the database for the application, you need to create two tables - `schedule_posts` containing the scheduled posts and the `users` table containing all the user's information retrieved from X.

The `users` table has three columns containing the username, ID, and access token retrieved after authentication. Ensure you make the username column the primary key for the table.


![schedule_posts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kft9jze78dr9e6wjvzaf.png)

The `schedule_posts` table contains six columns: a unique ID for each row, the timestamp indicating when the post will be live, the post content for X, the post status, the user's username on X, and `day_id` representing the scheduled day. `day_id` is necessary for retrieving existing schedules.


![foreignkey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1ubjnnsg58sng14i0l0.png)

Next, make the `profile` column a foreign key to the username column on the `users` table. We'll need this connection when making queries that merge the data within both tables.


![SideBar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iql487kmgct63jdgjic8.png)


Click `API` on the sidebar menu and copy the project's URL and API into the `.env.local` file.

```
NEXT_PUBLIC_SUPABASE_URL=<public_supabase_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<supabase_anon_key>
```


![SupabaseApi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dq9h843y15cdjs3lf958.png)

Finally, create a `src/supbaseClient.ts` file and create the Supabase client for the application.

```tsx
import { createClient } from "@supabase/supabase-js";

const supabaseURL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseURL, supabaseAnonKey, {
  auth: { persistSession: false },
});
```

Finally, create an `api/schedule` folder on the server containing a read, create, and delete route. The `api/schedule/create` adds a new post schedule to the database, `api/schedule/delete` deletes a selected post, and `api/schedule/read` fetches all the posts created by a user.

```bash
cd pages/api
mkdir schedule
touch create.ts delete.ts read.ts
```

Before we continue, update the `api/twitter/auth` endpoint to save the user’s access token and profile information to Supabase after authentication.

```tsx
import { supabase } from '../../../../supabaseClient';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
    const { code } = req.body
        try { 
            const tokenResponse = await fetchUserToken(code)
            const accessToken = tokenResponse.access_token
            
            if (accessToken) {
                const userDataResponse = await fetchUserData(accessToken)
                const userCredentials = { ...tokenResponse, ...userDataResponse };
								//👇🏻 saves user's information to Supabase
                const { data, error } = await supabase.from("users").insert({
                    id: userCredentials.data.id,
                    accessToken: userCredentials.access_token,
                    username: userCredentials.data.username
                });
                if (!error) {
                    return res.status(200).json(userCredentials)
                    
                } else {c
                    return res.status(400).json({error})
                }         
            }

        } catch (err) {
            return res.status(400).json({err})
        }
}
```

### Scheduling new posts

Modify the `api/schedule/create` endpoint to accept the post details and save them to the Supabase.

```tsx
import type { NextApiRequest, NextApiResponse } from "next";
import { supabase } from "../../../../supabaseClient";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	const { profile, timestamp, content, published, day_id } = req.body;
	
	const { data, error } = await supabase.from("schedule_posts").insert({
		profile, timestamp, content, published, day_id
	});
	res.status(200).json({data, error});

}
```

Add a function within the `utils/util.ts` file that accepts all the post details from the client and sends them to the endpoint. Execute the function when a user adds a new schedule.

```tsx
export const getNextDayOfWeek = (dayOfWeek: number, hours: number, minutes: number) => {
    var today = new Date();
    var daysUntilNextDay = dayOfWeek - today.getDay();
    if (daysUntilNextDay < 0) {
        daysUntilNextDay += 7;
    }
	today.setDate(today.getDate() + daysUntilNextDay);
	today.setHours(hours);
	today.setMinutes(minutes);
    return today;
}

export const updateSchedule = async (
	profile: string,
	schedule: any
) => {
	const { day_id, time, minutes, content, published } = schedule
	const timestampFormat = getNextDayOfWeek(day_id, time, minutes)
	try {
	await fetch("/api/schedule/create", {
			method: "POST",
			body: JSON.stringify({profile, timestamp: timestampFormat, content, published, day_id}),
			headers: {
				"Content-Type": "application/json",
			},
		});
        
	} catch (err) {
		console.error(err);
	}
};
```

The `getNextDayOfWeek` function accepts the day, hour, and minutes of the post, gets the date of the selected day, and converts the post's time and date into a datetime format - the acceptable format on the database, before adding it to the request's body. The `profile` parameter contains the user’s X username stored in local storage after authentication.

### Deleting scheduled posts

Update the `api/schedule/delete` endpoint to accept the post’s data and delete it from database.

```tsx
import type { NextApiRequest, NextApiResponse } from "next";
import { supabase } from "../../../../supabaseClient";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	const { profile, timestamp, content } = req.body;

	const { data, error } = await supabase
		.from("schedule_posts").delete().eq("content", content).eq("timestamp", timestamp.toISOString())
  
	res.status(200).json({ data, error });
}
```

Create the client-side function within the `utils/util.ts` file that sends a request to the endpoint when the user deletes a post. 

```tsx
export const deleteSchedule = async (
	profile: string,
	schedule: any
) => {
	const { day_id, time_id, minutes, content, published } = schedule
	const timestampFormat = getNextDayOfWeek(day_id, time_id, minutes)

	try {
		await fetch("/api/schedule/delete", {
			method: "POST",
			body: JSON.stringify({ profile, timestamp: timestampFormat,  content}),
			headers: {
				"Content-Type": "application/json",
			},
		});
	} catch (err) {
		console.error(err);
	}
	
};
```

### Fetching users’ schedule

Modify the `api/schedule/create` endpoint to accept a post details and add them to the database.

```tsx
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { supabase } from "../../../../supabaseClient";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	const { profile, timestamp, content, published, day_id } = req.body;
	
	const { data, error } = await supabase.from("schedule_posts").insert({
		profile, timestamp, content, published, day_id
	});
	res.status(200).json({data, error});

}
```

Create its request function on the client that retrieves all the scheduled posts and converts them to the format required on the front end.

```tsx
export const fetchSchedule = async (
	profile: string, updateYourSchedule: Dispatch<SetStateAction<AvailableScheduleItem[]>>
) => {
	try {
		const request = await fetch("/api/schedule/read", {
			method: "POST",
			body: JSON.stringify({ profile }),
			headers: {
				"Content-Type": "application/json",
			},
		});
		const response = await request.json();
		const { data } = response
		//👇🏻 if there is a response
		if (data) {
			const result = data.map((item: any) => {
				const date = new Date(item.timestamp);
				//👇🏻 returns a new array in the format below
				return {
					//👇🏻 converts 24 hour to 0 hour
					time: date.getUTCHours() + 1 < 24 ? date.getUTCHours() + 1 : 0,
					schedule: {
						content: item.content,
						published: item.published,
						minutes: date.getUTCMinutes(),
						day: item.day_id
					},
				}
			
			})
			//👇🏻 loops through retrieved schedule and add them to the large table array 
			result.forEach((object: any) => {
				const matchingObjIndex = availableSchedule.findIndex((largeObj) => largeObj.time === object.time);
				if (matchingObjIndex !== -1) {
					availableSchedule[matchingObjIndex].schedule[object.schedule.day].push(object.schedule)
				}
			})
			updateYourSchedule(availableSchedule)
		}
	} catch (err) {
		console.error(err);
	}

};
```

The code snippet above receives the scheduled posts from the server, converts it into the format required by the table UI, then loops through the data, adds them to the large array - `availableSchedule` that contains the table layout, and updates the schedule state with the modified version.

Congratulations! You've successfully added Supabase to the application.

<hr />

## Sending the post at the right time ⏳

[Trigger.dev](http://Trigger.dev) offers three communication methods: webhook, schedule, and event. Schedule is ideal for recurring tasks, events activate a job upon sending a payload, and webhooks trigger real-time jobs when specific events occur. 

In this section, you’ll learn how to schedule recurring tasks with Trigger.dev by comparing the current time with the time scheduled post times and automatically posting them to X at their scheduled times.

### Adding Trigger.dev to a Next.js app

Before we continue, you need to [create a Trigger.dev account](https://trigger.dev/). Once registered, create an organization and choose a project name for your jobs.


![TriggerAccount](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/if69k2eukq1k1vkz3871.jpeg)

Select Next.js as your framework and follow the process for adding Trigger.dev to an existing Next.js project.


![NextTrigger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/88a4e40fntty5c3rkxaa.png)

Otherwise, click `Environments & API Keys` on the sidebar menu of your project dashboard.


![Environments](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c58wadjmiwo6qsjp61k0.png)

Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.

```bash
npx @trigger.dev/cli@latest init
```

Start your Next.js project.

```bash
npm run dev
```

In another terminal, run the following code snippet to establish a tunnel between Trigger.dev and your Next.js project.

```bash
npx @trigger.dev/cli@latest dev
```

Finally, rename the `jobs/examples.ts` file to `jobs/functions.ts`. This is where all the jobs are processed.

Congratulations!🎉 You've successfully added Trigger.dev to your Next.js app.

### Posting scheduled contents on X with Trigger.dev

Here, you'll learn how to create recurring jobs with Trigger. The job will check the scheduled posts every minute and post content to X at the exact time stated by the user.

To integrate Trigger.dev with Supabase, you need to install the [Trigger.dev Supabase package](https://trigger.dev/docs/integrations/apis/supabase/client).

```bash
npm install @trigger.dev/supabase
```

Import the `[cronTrigger](https://trigger.dev/docs/sdk/crontrigger)` and [Supabase](https://trigger.dev/docs/integrations/apis/supabase/client) from their packages into the `jobs/functions.ts` file. The `cronTrigger()` function enables us to execute jobs on a recurring schedule.

> PS: Scheduled Triggers **do not** trigger Jobs in the DEV **[Environment](https://trigger.dev/docs/documentation/concepts/environments-endpoints)**. When you’re working locally you should use **[the Test feature](https://trigger.dev/docs/documentation/guides/testing-jobs)** to trigger any scheduled jobs.
> 

```tsx
import { cronTrigger } from "@trigger.dev/sdk";
import { Supabase } from "@trigger.dev/supabase";

const supabase = new Supabase({
	id: "supabase",
	supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
	supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
});
```

Modify the job within the `jobs/functions.ts` file to fetch posts that are yet to be published, and whose schedule time matches the current time.

```tsx
client.defineJob({
	id: "post-schedule",
	name: "Post Schedule",
	//👇🏻 integrate Supabase
	integrations: { supabase },
	version: "0.0.1",
	//👇🏻 runs every minute
	trigger: cronTrigger({
	  cron: "* * * * *",
	}),
	run: async (payload, io, ctx) => {
		await io.logger.info("Job started! 🌟");

		const { data, error } = await io.supabase.runTask(
			"find-schedule",
			async (db) => {
				return (await db.from("schedule_posts")
					.select(`*, users (username, accessToken)`)
					.eq("published", false)
					.lt("timestamp", new Date().toISOString()))
			}
		);
			
		await io.logger.info(JSON.stringify(data))
}
});
```

The code snippet above uses the foreign key (`username`) created between the `users` and the `schedule_posts` table to perform a [join query](https://supabase.com/docs/guides/api/joins-and-nesting). This query returns the access token and username from the `users` table and all the data within `schedule_posts`.


![Iterate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvfz3uht3qjjpjpiigit.png)

Finally, iterate through the posts and post its content on X.

```tsx
      for (let i = 0; i < data?.length; i++) {
			try {
				const postTweet = await fetch("https://api.twitter.com/2/tweets", {
					method: "POST",
					headers: {
						"Content-type": "application/json",
						Authorization: `Bearer ${data[i].users.accessToken}`,
					},
					body: JSON.stringify({ text: data[i].content })
				})
				const getResponse = await postTweet.json()
				await io.logger.info(`${i}`)
				await io.logger.info(`Tweet created successfully!${i} - ${getResponse.data}`)

			} catch (err) {
				await io.logger.error(err)
			}
				
		}
```

Congratulations! You have completed the project for this tutorial.


![Conclusion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70a77hvzgaf49qyallu8.gif)

<hr />

## Conclusion

So far, you've learned how to add Twitter (X) authentication to a Next.js application, save data to Supabase, and create recurring tasks with Trigger.dev.

As an open-source developer, you're invited to join our [community](https://discord.gg/nkqV9xBYWy) to contribute and engage with maintainers. Don't hesitate to visit our [GitHub repository](https://github.com/triggerdotdev/trigger.dev) to contribute and create issues related to Trigger.dev.

The source for this tutorial is available here:

https://github.com/triggerdotdev/blog/tree/main/x-post-scheduler

Thank you for reading!

<hr />

## Help me out 🩷

If you can spend 10 seconds giving us a star, I would be super grateful 💖
https://github.com/triggerdotdev/trigger.dev


[![GiveStar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wsljq85zi71b0pksld7.png)](https://github.com/triggerdotdev/trigger.dev
)