DEV Community

Cover image for Building a Custom Scheduler Using React and Supabase
Suraj Vishwakarma
Suraj Vishwakarma

Posted on

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:

  1. Adding articles to the database through the front end.
  2. The Cron job will run every minute to call the edge function.
  3. An edge function will be executed to check the current time as scheduled article. If there is an article it will post the article.
  4. 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;
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

The below command can be used to create the Edge function

    supabase functions new xscheduler
Enter fullscreen mode Exit fullscreen mode

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/std@0.168.0/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 }
        );
      }
    });
Enter fullscreen mode Exit fullscreen mode

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.

Cron job

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

Frontend

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.

Post on dev.to

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.

Top comments (2)

Collapse
 
theme_selection profile image
ThemeSelection

Informative. 👍🏻

Collapse
 
jacobdev profile image
Jacob Brown

Thanks for sharing this article! It’s super helpful and explains how to build a custom scheduler with React and Supabase in such a clear and practical way.