Building a Custom Scheduler Using React and Supabase
Introduction
Scheduling is one of the critical features of modern applications. It can enable us to run periodic task that can be automated. Task such as such as sending reminders, scheduling posts, updating data, or automating workflows.
So, In this article, we are going to build a scheduler to post articles on dev.to. Although, dev.to has scheduling features but we are going to implement them in our way, which can be used to build any kind of scheduler application.
So, let’s get started.
Tech Stack
We are going to use the following tech stack:
- React: We are going to use React, particularly ViteJS with React to build the fronted.
-
Supabase: It provides an all-in-one solution for building applications. It provides a database, auth, storage, edge function, and many more. We are going to use the following from Supbase:
- Database: This is used to store the article information and schedule time.
- Cron Job: For running periodically to call the Edge function
- Edge Function: This will check if any article has the current time as the scheduled time. If then it will post the article.
That will be enough to build a scheduler application with ease.
Working on the Application
Let’s discuss how the application works, which makes it quite easy to understand the flow of the application. Here is the flow one by one:
- Adding articles to the database through the front end.
- The Cron job will run every minute to call the edge function.
- An edge function will be executed to check the current time as scheduled article. If there is an article it will post the article.
- Article data in the post table will be updated. # Building the Frontend
The building frontend has become quiet lately with a lot of generative AI. One of the such AI we are going to use is bolt.new. Why bolt.new? It can generate complete React applications with dependencies and all the configurations such as tailwindcss. You can directly edit articles using StackBlitz and also deploy the application. If you require you can download the code to run locally. The bonus point is that it integrate with Supabase quite well so you can generate a working React application with the Supbase integration.
I have used it to generate the front. Here are all the pages.
App.tsx
This will handle the page for displaying components and providing the landing page.
function App() {
const [posts, setPosts] = useState<ScheduledPost[]>([]);
const handleSchedulePost = async (data: CreatePostData) => {
// In a real app, this would make an API call to your edge function
const newPost: ScheduledPost = {
content: data.content,
scheduled_time: data.scheduledTime,
status: 'pending',
title: data.title,
tags: data.tags
};
const { error } = await supabase
.from('scheduled_posts')
.insert(newPost)
if (error){
alert(`Erorr: ${error}`)
return
}
// setPosts((prev) => [...prev, newPost]);
};
const fetchScheduedPost = async () => {
const { data, error } = await supabase
.from('scheduled_posts')
.select()
if(error){
alert(`Erorr Fetching Data: ${error}`)
return
}
setPosts(data)
}
useEffect(() => {
fetchScheduedPost()
},[])
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center gap-2">
<Newspaper className="h-8 w-8 text-blue-500" />
<h1 className="text-xl font-bold text-gray-900">Dev.to Post Scheduler</h1>
</div>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
<div className="grid gap-8 md:grid-cols-2">
<div>
<h2 className="text-xl font-semibold text-gray-800 mb-4">Schedule New Post</h2>
<PostForm onSubmit={handleSchedulePost} />
</div>
<div>
<ScheduledPosts posts={posts} />
</div>
</div>
</main>
</div>
);
}
export default App;
SchudledPost.tsx
This displays the scheduled articles.
const StatusIcon = ({ status }: { status: ScheduledPost['status'] }) => {
switch (status) {
case 'posted':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'failed':
return <XCircle className="h-5 w-5 text-red-500" />;
default:
return <Clock3 className="h-5 w-5 text-yellow-500" />;
}
};
export function ScheduledPosts({ posts }: ScheduledPostsProps) {
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-800">Scheduled Posts</h2>
{posts.length === 0 ? (
<p className="text-gray-500 text-center py-8">No scheduled posts yet</p>
) : (
<div className="space-y-4">
{posts.map((post, index) => (
<div
key={index}
className="bg-white p-4 rounded-lg shadow-md border border-gray-100"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-gray-800 mb-2">{post.title}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{new Date(post.scheduled_time).toLocaleDateString()}
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{new Date(post.scheduled_time).toLocaleTimeString()}
</div>
</div>
</div>
<StatusIcon status={post.status} />
</div>
</div>
))}
</div>
)}
</div>
);
}
PostForm.tsx
This will handle the form where the user can give information about the article.
export function PostForm({ onSubmit }: PostFormProps) {
const [content, setContent] = useState('');
const [title, setTitle] = useState('');
const [tags, setTags] = useState<string[]>(['javascript', 'react']);
const [scheduledTime, setScheduledTime] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ content, title, scheduledTime, tags });
setContent('');
setTitle('');
setScheduledTime('');
setTags([]);
};
const handleTagChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedOptions = Array.from(e.target.selectedOptions);
const selectedTags = selectedOptions.map(option => option.value);
if(tags.length<4){
setTags(prevTags => {
const newTags = selectedTags.filter(tag => !prevTags.includes(tag));
return [...prevTags, ...newTags];
});
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
return (
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 rounded-lg shadow-md">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
Post Title
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Title of the post"
required
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
Post Content
</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={4}
maxLength={280}
placeholder="What's happening?"
required
/>
</div>
<div>
<label htmlFor="scheduledTime" className="block text-sm font-medium text-gray-700 mb-2">
Schedule Time
</label>
<div className="relative">
<input
type="datetime-local"
id="scheduledTime"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent pl-10"
required
/>
<Calendar className="absolute left-3 top-3.5 h-5 w-5 text-gray-400" />
</div>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-2">
Tags
</label>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs"
>
{tag}
<button
type="button"
className="ml-1 text-gray-500 hover:text-gray-700"
onClick={() => removeTag(tag)}
>
x
</button>
</span>
))}
<select
id="tags"
value={tags}
onChange={handleTagChange}
multiple
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
size={4}
required
>
{tagOptions.map((tag) => (
<option key={tag.value} value={tag.value}>
{tag.label}
</option>
))}
</select>
</div>
<div className="text-sm text-gray-500 mt-1">
Select up to 4 tags
</div>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors flex items-center justify-center gap-2"
>
<Send className="h-5 w-5" />
Schedule Post
</button>
</form>
);
}
I will provide the whole code as a GitHub repository at the end.
Now, let’s look at Supbase Integration.
Supabase
First create an account on supabase, if you don’t have one. You can look at this article to get information about the creating an account on Supbase, Using ChatGPT with Your Own Data using LangChain and Supabase.
Create the table scheduled_post. You can use the below SQL code to run in the SQL Editor to create the table or you can create the table with Table Editor.
create table
public.scheduled_posts (
id serial not null,
content text not null,
scheduled_time timestamp with time zone not null,
status text null default 'pending'::text,
created_at timestamp without time zone null default now(),
title character varying null,
devto_article_id character varying null,
posted_at character varying null,
tags character varying[] null,
error_message character varying null,
constraint scheduled_posts_pkey primary key (id)
) tablespace pg_default;
create index if not exists idx_scheduled_time_status on public.scheduled_posts using btree (scheduled_time, status) tablespace pg_default;
Edge Function
Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third parties like Stripe. Edge Functions are developed using Deno.
For running and deploying the edge function locally you need to have the following:
- Supbase CLI: You can install CLI locally using this guide. It is simple just using the npm and npx.
- Docker Desktop: Install the docker desktop from here.
So, after installing this, you can use your frontend code directory or other to create the Supabase Edge Function.
Run the below command to initiate a supabase project:
npx supabase init
The below command can be used to create the Edge function
supabase functions new xscheduler
The above command will create a directory functions/xscheduler inside the supabase. There you can find the index.ts. Edge function uses Deno environment.
The below code is for the edge function:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
// Ensure these are set in your environment variables
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") || "";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
const DEVTO_ACCESS_TOKEN = Deno.env.get("DEVTO_ACCESS_TOKEN") || "";
// Initialize Supabase client
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
// Function to post a new article to Dev.to
async function postToDevTo(title: string, content: string) {
const url = "https://dev.to/api/articles";
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": DEVTO_ACCESS_TOKEN,
},
body: JSON.stringify({ article: { title, body_markdown: content, published: true } })
});
if (response.ok) {
const result = await response.json();
console.log("Article posted successfully:", result);
return {
success: true,
articleId: result.id,
};
} else {
const errorBody = await response.text();
console.error("Dev.to API Error:", errorBody);
return {
success: false,
error: errorBody,
};
}
} catch (error) {
console.error("Error posting to Dev.to:", error);
return {
success: false,
error: error.message,
};
}
}
// Serve an HTTP endpoint for testing
serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
try {
// Get the current timestamp rounded to the nearest minute
const currentDate = new Date();
const currentHour = currentDate.getHours();
const currentMinute = currentDate.getMinutes();
const currentDay = currentDate.getDate();
const currentMonth = currentDate.getMonth() + 1;
const currentYear = currentDate.getFullYear();
const currentDateTimeString = `${currentYear}-${currentMonth}-${currentDay} ${currentHour}:${currentMinute - 1}`;
const nextDateTimeString = `${currentYear}-${currentMonth}-${currentDay} ${currentHour}:${currentMinute + 1}`;
// Fetch articles scheduled for the current time from Supabase
const { data: articles, error: fetchError } = await supabase
.from("scheduled_posts")
.select()
.gt("scheduled_time", currentDateTimeString)
.lt("scheduled_time", nextDateTimeString) // Check within the next minute
if (fetchError) {
console.error("Error fetching scheduled articles:", fetchError);
return new Response(JSON.stringify({ message: "Error fetching scheduled articles" }), { status: 500 });
}
if (!articles || articles.length === 0) {
return new Response(JSON.stringify({ message: "No articles scheduled for posting." }), { status: 404 });
}
// Post each article to Dev.to and update its status in Supabase
for (const article of articles) {
const result = await postToDevTo(article.title, article.content);
if (result.success) {
// Update the article status in Supabase
const { error } = await supabase
.from("scheduled_posts")
.update({
status: "posted",
devto_article_id: result.articleId,
posted_at: new Date().toISOString(),
})
.eq("id", article.id);
if (error) {
console.error("Failed to update article status in Supabase", error);
return new Response(
JSON.stringify({ message: "Failed to update article status", error: error.message }),
{ status: 500 }
);
}
console.log(`Article ${article.id} posted successfully on Dev.to.`);
} else {
console.error(`Failed to post article ${article.id}:`, result.error);
return new Response(
JSON.stringify({ message: `Failed to post article ${article.id}`, error: result.error }),
{ status: 500 }
);
}
}
return new Response(
JSON.stringify({ message: "All scheduled articles posted successfully." }),
{ status: 200 }
);
} catch (error) {
console.error("Unexpected error:", error);
return new Response(
JSON.stringify({ message: "Internal Server Error", error: error.message }),
{ status: 500 }
);
}
});
For the ENV such as SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are automatically available to you. For DEVTO_ACCESS_TOKEN, you can generate it from here, and go to Project Setting → Edge Functions to add the token. This token will be available in the Deno environment.
You can use this guide for deploying the edge function, which is needed.
Cron Job
Supbase recently updated the Cron job functionality. Now you can use the dashboard to create the corn job previously you had to write code for that. You can create a job that can run the following:
- SQL Snippet
- Database Function
- HTTP Request
- Supbase Edge Function
We are going to use the Edge Function, You can add the details of the Edge function such as name and Authorization with the Anon key as a Bearer Token.
Working of the Application
Now, that we have created the application let’s look at the working now. Run the fronted with the below command:
npm run dev
Add the details such as Title, Content, Time, and Tags. Once added click on the Schedule Post. The cron job will run every minute once the article’s scheduled time matches the current time. It will be posted.
The article will be posted on the dev.to when the time range matches.
Additional Features
Using the above technique you can build a scheduler application for anything such as X, Instagram, LinkedIn, etc. You can work on it and add functionality such as the following:
- Image: Use the supabase storage to upload and fetch images for thumbnails.
- Edge function invoke from SQL: You can make it even more efficient by calling the edge function from an SQL snippet or Database function. This is so that only when the article matches the current time is the edge function invoked.
You can look into the code of this project on GitHub here.
Conclusion
Creating a scheduler application simplifies automating tasks like posting articles, sending reminders, and managing workflows. Using React for the frontend and Supabase for the backend, we built a scalable solution that leverages databases, cron jobs, and edge functions. This approach can be adapted for various use cases, enabling efficient automation. With these tools, you’re equipped to build powerful scheduler applications tailored to your needs.
I hope this article has provided you with an understanding of the cron job. Thanks for reading the article.