<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ David Asaolu - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ David Asaolu - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:23:53 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/de/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI Social Media Post Scheduler Using Gemini and Late API in Next.js ]]>
                </title>
                <description>
                    <![CDATA[ Social media has become a vital tool for people and businesses to share ideas, promote products, and connect with their target audience. But creating posts regularly and managing schedules across multiple platforms can be time-consuming and repetitiv... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-ai-social-media-post-scheduler-using-gemini-and-late-api-in-nextjs/</link>
                <guid isPermaLink="false">697cebbfa59b6f85b8ca1c5f</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ social media ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #ai-tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Fri, 30 Jan 2026 17:34:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769794478505/be596e27-4c88-45b3-8547-f715c82e0eda.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Social media has become a vital tool for people and businesses to share ideas, promote products, and connect with their target audience. But creating posts regularly and managing schedules across multiple platforms can be time-consuming and repetitive.</p>
<p>In this tutorial, you’ll learn how to build an AI-powered social media post scheduler using <a target="_blank" href="https://ai.google.dev/gemini-api/docs/quickstart"><strong>Gemini</strong></a><strong>,</strong> <a target="_blank" href="https://docs.getlate.dev/">Late API</a>, and <a target="_blank" href="https://nextjs.org/">Next.js</a>.</p>
<p>We’ll use the Gemini API to generate engaging social media content from user prompts, Next.js to handle both the frontend and backend of the application, and Late API to publish and schedule posts across multiple social media platforms from a single platform.</p>
<p><img src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExd3hreWI5cGw0MnRxenc5NGJqdHczYjdmNjh2Zmxrb2c3ZXo2empzYyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/azjux6zMBW3cWxK21P/giphy.gif" alt="Social media platforms" class="image--center mx-auto" width="340" height="340" loading="lazy"></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setup-and-installation">Setup and Installation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-schedule-social-media-posts-with-late">How to Schedule Social Media Posts with Late</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-nextjs-app-interface">How to Build the Next.js App Interface</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-gemini-api-for-post-generation">How to integrate Gemini API for Post Generation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-late-api-in-nextjs">How to Use Late API in Next.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To fully understand this tutorial, you need to have a basic understanding of React or Next.js.</p>
<p>We will use the following tools:</p>
<ul>
<li><p><a target="_blank" href="https://getlate.dev/"><strong>Late API</strong></a>: A social media API that lets you create and schedule posts across 13 social media platforms from a single dashboard.</p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/"><strong>Next.js</strong></a>: A React framework for building fast, scalable web applications, handling both the frontend and backend.</p>
</li>
<li><p><a target="_blank" href="https://ai.google.dev/gemini-api/docs/quickstart"><strong>Google Gemini API</strong></a>: Provides access to Google’s AI models for generating text and other content based on user prompts.</p>
</li>
</ul>
<h2 id="heading-setup-and-installation">Setup and Installation</h2>
<p>Create a new Next.js project using the following code snippet:</p>
<pre><code class="lang-bash">npx create-next-app post-scheduler
</code></pre>
<p>Install the project dependencies. We’ll use <a target="_blank" href="https://github.com/iamkun/dayjs"><strong>Day.js</strong></a> to work with JavaScript dates, making it easier to schedule and publish social media posts at the correct time.</p>
<pre><code class="lang-bash">npm install @google/genai dayjs utc
</code></pre>
<p>Next, add a <code>.env.local</code> file containing your Gemini API key at the root of your Next.js project:</p>
<pre><code class="lang-bash">GEMINI_API_KEY=&lt;paste_your API key&gt;
</code></pre>
<p>Once everything is set up, your Next.js project is ready. Now, let's start building! 🚀</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769613408264/abd09717-9403-4480-a319-a0d430b635a3.png" alt="Late API and the available social media platforms" class="image--center mx-auto" width="2496" height="1369" loading="lazy"></p>
<h2 id="heading-how-to-schedule-social-media-posts-with-late">How to Schedule Social Media Posts with Late</h2>
<p><strong>Late</strong> is an all-in-one social media scheduling platform that allows you to connect your social media accounts and publish posts across multiple platforms. In this section, you’ll learn how to create and schedule social media posts using the Late dashboard.</p>
<p>To get started, create a <a target="_blank" href="https://getlate.dev/"><strong>Late account</strong></a> and sign in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769613666616/61074a32-5fa3-4c14-8e96-dfc8a86fec0f.png" alt="Sign in and get Late API key" class="image--center mx-auto" width="2503" height="1247" loading="lazy"></p>
<p>Create an API key and add it to the <code>.env.local</code> file within your Next.js project.</p>
<pre><code class="lang-bash">LATE_API_KEY=&lt;your_API_key&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769613764201/5dbd7d92-0fa8-46d3-9bf0-a145ef4fe65e.png" alt="Copy Late API key" class="image--center mx-auto" width="2536" height="1402" loading="lazy"></p>
<p>Connect your social media accounts to Late so you can manage and publish posts across all platforms.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769613835179/fa6ec3df-4cca-4fc9-bfac-3b5f2815dfdd.png" alt="Social media platforms" class="image--center mx-auto" width="2455" height="1249" loading="lazy"></p>
<p>After connecting your social media accounts via OAuth, you can start writing, posting, and scheduling content directly to your social media platforms.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769613994931/d9862aa7-2a57-4373-afe0-8513eea6bffd.png" alt="Twitter (X) account connected" class="image--center mx-auto" width="2559" height="1425" loading="lazy"></p>
<p>Late lets you write your post content and attach media files directly from the dashboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769614064645/0eea26fe-6d51-4ae3-8b33-d96cc539e88d.png" alt="Create Social media contents from your dashboard" class="image--center mx-auto" width="2559" height="1419" loading="lazy"></p>
<p>You can choose when your content should be published: post immediately, schedule for later, add it to a job queue, or save it as a draft.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769614116012/c241c2ed-37d3-4973-8d62-0e288e97f561.png" alt="Publish your post" class="image--center mx-auto" width="2536" height="1423" loading="lazy"></p>
<p>Once a post is published, you can view its status and preview it directly in the dashboard using the post link.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769614187249/574ec680-7aad-4701-aa7f-9db4aebb146a.png" alt="Social media post created with Late" class="image--center mx-auto" width="2538" height="1427" loading="lazy"></p>
<p>🎉 <strong>Congratulations!</strong> You’ve successfully created your first post using the Late dashboard. In the next sections, you’ll learn how to use the <a target="_blank" href="https://docs.getlate.dev/core/posts">Late API</a> to create and schedule posts directly from your applications.</p>
<h2 id="heading-how-to-build-the-nextjs-app-interface">How to Build the Next.js App Interface</h2>
<p>In this section, you’ll build the user interface for the application. The app uses a single-page route with conditional rendering to display recent posts, an AI prompt input field, and a form that allows users to create or schedule posts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769614332014/a6348263-bdba-4100-a63e-0698ee7a9ed4.gif" alt="App Overview" class="image--center mx-auto" width="800" height="443" loading="lazy"></p>
<p>Before we proceed, create a <code>types.d.ts</code> file within your Next.js project and copy the following code snippet into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> Post {
    _id: <span class="hljs-built_in">string</span>;
    content: <span class="hljs-built_in">string</span>;
    scheduledFor: <span class="hljs-built_in">string</span>;
    status: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">interface</span> AIFormProps {
    handleGeneratePost: <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
    useAI: <span class="hljs-built_in">boolean</span>;
    setUseAI: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">boolean</span>&gt;&gt;;
    prompt: <span class="hljs-built_in">string</span>;
    setPrompt: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">string</span>&gt;&gt;;
    disableBtn: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">interface</span> FormProps {
    handlePostSubmit: <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
    content: <span class="hljs-built_in">string</span>;
    setContent: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">string</span>&gt;&gt;;
    date: <span class="hljs-built_in">string</span>;
    setDate: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">string</span>&gt;&gt;;
    disableBtn: <span class="hljs-built_in">boolean</span>;
    setUseAI: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">boolean</span>&gt;&gt;;
    useAI: <span class="hljs-built_in">boolean</span>;
}
</code></pre>
<p>The <code>types.d.ts</code> file defines all the data structures and type declarations used throughout the application.</p>
<p>Copy the following code snippet into the <code>app/page.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> Nav <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Nav"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> NewPost <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/NewPost"</span>;
<span class="hljs-keyword">import</span> PostsQueue <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PostsQueue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [showPostQueue, setShowPostQueue] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'w-full h-screen'</span>&gt;
            &lt;Nav showPostQueue={showPostQueue} setShowPostQueue={setShowPostQueue} /&gt;
            {showPostQueue ? &lt;PostsQueue /&gt; : &lt;NewPost /&gt;}
        &lt;/div&gt;
    );
}
</code></pre>
<p>The <code>Page</code> component renders the <code>Nav</code> component and uses conditional rendering to display either the <code>PostsQueue</code> or <code>NewPost</code> component based on the value of the <code>showPostQueue</code> state.</p>
<p>Create a <code>components</code> folder to store the page components used in the application.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app
mkdir components &amp;&amp; <span class="hljs-built_in">cd</span> components
touch Nav.tsx NewPost.tsx PostElement.tsx PostsQueue.tsx
</code></pre>
<p>Add the code snippet below to the <code>Nav.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Nav</span>(<span class="hljs-params">{
    showPostQueue,
    setShowPostQueue,
}: {
    showPostQueue: <span class="hljs-built_in">boolean</span>;
    setShowPostQueue: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">boolean</span>&gt;&gt;;
}</span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;nav&gt;
            &lt;h2&gt;Post Scheduler&lt;/h2&gt;

            &lt;button onClick={<span class="hljs-function">() =&gt;</span> setShowPostQueue(!showPostQueue)}&gt;
                {showPostQueue ? <span class="hljs-string">"New Post"</span> : <span class="hljs-string">"Schedule Queue"</span>}
            &lt;/button&gt;
        &lt;/nav&gt;
    );
}
</code></pre>
<p>Copy the following code snippet into the <code>PostsQueue.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useState, useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> PostElement <span class="hljs-keyword">from</span> <span class="hljs-string">"./PostElement"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PostsQueue</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [posts, setPosts] = useState&lt;Post[]&gt;([]);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">true</span>);

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'p-4'</span>&gt;
            &lt;h2 className=<span class="hljs-string">'text-xl font-bold'</span>&gt;Scheduled Posts&lt;/h2&gt;

            {loading ? (
                &lt;p className=<span class="hljs-string">'text-sm'</span>&gt;Loading scheduled posts...&lt;/p&gt;
            ) : (
                &lt;div className=<span class="hljs-string">'mt-4'</span>&gt;
                    {posts.length &gt; <span class="hljs-number">0</span> ? (
                        posts.map(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> &lt;PostElement key={post._id} post={post} /&gt;)
                    ) : (
                        &lt;p&gt;No scheduled posts available.&lt;/p&gt;
                    )}
                &lt;/div&gt;
            )}
        &lt;/div&gt;
    );
}
</code></pre>
<p>The <code>PostsQueue.tsx</code> component displays a list of previously created posts along with their current status, showing whether each post has been published or scheduled for a later time. While the data is being loaded, it shows a loading message, and once loaded, it renders each post using the <code>PostElement</code> component.</p>
<p>Add the following to the <code>PostElement.tsx</code> component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PostElement</span>(<span class="hljs-params">{ post }: { post: Post }</span>) </span>{
    <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> formatReadableTime = <span class="hljs-function">(<span class="hljs-params">isoString: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(isoString); <span class="hljs-comment">// parses UTC automatically</span>
        <span class="hljs-keyword">return</span> date.toLocaleString(<span class="hljs-literal">undefined</span>, {
            year: <span class="hljs-string">"numeric"</span>,
            month: <span class="hljs-string">"short"</span>,
            day: <span class="hljs-string">"numeric"</span>,
            hour: <span class="hljs-string">"2-digit"</span>,
            minute: <span class="hljs-string">"2-digit"</span>,
            second: <span class="hljs-string">"2-digit"</span>,
            hour12: <span class="hljs-literal">true</span>, <span class="hljs-comment">// set to false for 24h format</span>
        });
    };

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'p-4 border flex items-center justify-between  space-x-4 rounded mb-2 hover:bg-gray-100 cursor-pointer'</span>&gt;
            &lt;div&gt;
                &lt;p className=<span class="hljs-string">'font-semibold text-sm'</span>&gt;{post.content.slice(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)}&lt;/p&gt;
                &lt;p className=<span class="hljs-string">'text-blue-400 text-xs'</span>&gt;
                    Scheduled <span class="hljs-keyword">for</span>: {formatReadableTime(post.scheduledFor)}
                &lt;/p&gt;
            &lt;/div&gt;

            &lt;p className=<span class="hljs-string">'text-sm text-red-500'</span>&gt;{post.status}&lt;/p&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>Finally, copy the following code snippet into the <code>NewPost.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewPost</span>(<span class="hljs-params"></span>) </span>{
 <span class="hljs-keyword">const</span> [disableBtn, setDisableBtn] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
 <span class="hljs-keyword">const</span> [useAI, setUseAI] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
 <span class="hljs-keyword">const</span> [content, setContent] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
 <span class="hljs-keyword">const</span> [prompt, setPrompt] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
 <span class="hljs-keyword">const</span> [date, setDate] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

 <span class="hljs-comment">//👇🏻 generates post content</span>
 <span class="hljs-keyword">const</span> handleGeneratePost = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
  e.preventDefault();
  setDisableBtn(<span class="hljs-literal">true</span>);
 };

 <span class="hljs-comment">//👇🏻 create/schedule post</span>
 <span class="hljs-keyword">const</span> handlePostSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
  e.preventDefault();
 };

 <span class="hljs-keyword">return</span> (
  &lt;div className=<span class="hljs-string">'w-full p-4  h-[90vh] flex flex-col items-center justify-center border-t'</span>&gt;
   &lt;h3 className=<span class="hljs-string">'text-xl font-bold'</span>&gt;New Post&lt;/h3&gt;

   {useAI ? (
    &lt;AIPromptForm
     handleGeneratePost={handleGeneratePost}
     useAI={useAI}
     setUseAI={setUseAI}
     prompt={prompt}
     setPrompt={setPrompt}
     disableBtn={disableBtn}
    /&gt;
   ) : (
    &lt;PostForm
     handlePostSubmit={handlePostSubmit}
     content={content}
     setContent={setContent}
     date={date}
     setDate={setDate}
     disableBtn={disableBtn}
     setUseAI={setUseAI}
     useAI={useAI}
    /&gt;
   )}
  &lt;/div&gt;
 );
}
</code></pre>
<p>The <code>NewPost</code> component conditionally renders the <code>AIPromptForm</code> and the <code>PostForm</code>. When a user chooses to generate content using AI, the AIPromptForm component is displayed to collect the prompt. Once the content is generated, the PostForm component is shown, allowing the user to edit, create, or schedule the post.</p>
<p>Add the components below inside the <code>NewPost.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AIPromptForm = <span class="hljs-function">(<span class="hljs-params">{
    handleGeneratePost,
    useAI,
    setUseAI,
    prompt,
    setPrompt,
    disableBtn,
}: AIFormProps</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        &lt;form onSubmit={handleGeneratePost}&gt;
            &lt;p onClick={<span class="hljs-function">() =&gt;</span> setUseAI(!useAI)}&gt;Exit AI &lt;/p&gt;
            &lt;textarea
                rows={<span class="hljs-number">3</span>}
                required
                value={prompt}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setPrompt(e.target.value)}
                placeholder=<span class="hljs-string">'Enter prompt...'</span>
            /&gt;
            &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span> disabled={disableBtn}&gt;
                {disableBtn ? <span class="hljs-string">"Generating..."</span> : <span class="hljs-string">"Generate Post with AI"</span>}
            &lt;/button&gt;
        &lt;/form&gt;
    );
};

<span class="hljs-comment">// 👇🏻 Post Form component</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> PostForm = <span class="hljs-function">(<span class="hljs-params">{
    handlePostSubmit,
    content,
    setContent,
    date,
    setDate,
    disableBtn,
    setUseAI,
    useAI,
}: FormProps</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> getNowForDatetimeLocal = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(now.getTime() - now.getTimezoneOffset() * <span class="hljs-number">60000</span>)
            .toISOString()
            .slice(<span class="hljs-number">0</span>, <span class="hljs-number">16</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;form onSubmit={handlePostSubmit}&gt;
            &lt;p onClick={<span class="hljs-function">() =&gt;</span> setUseAI(!useAI)}&gt;Generate posts <span class="hljs-keyword">with</span> AI &lt;/p&gt;
            &lt;textarea
                value={content}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setContent(e.target.value)}
                rows={<span class="hljs-number">4</span>}
                placeholder=<span class="hljs-string">"What's happening?"</span>
                required
                maxLength={<span class="hljs-number">280</span>}
            /&gt;
            &lt;input
                <span class="hljs-keyword">type</span>=<span class="hljs-string">'datetime-local'</span>
                min={getNowForDatetimeLocal()}
                value={date}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setDate(e.target.value)}
            /&gt;
            &lt;button disabled={disableBtn} <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span>&gt;
                {disableBtn ? <span class="hljs-string">"Posting..."</span> : <span class="hljs-string">"Create post"</span>}
            &lt;/button&gt;
        &lt;/form&gt;
    );
};
</code></pre>
<p>Congratulations! You've completed the application interface.</p>
<h2 id="heading-how-to-integrate-gemini-api-for-post-generation">How to integrate Gemini API for Post Generation</h2>
<p>Here, you will learn how to generate post content from the user's prompt using the Gemini API.</p>
<p>Before we proceed, make sure you have copied your API key from the <a target="_blank" href="https://ai.google.dev/gemini-api/docs/api-key">Google AI Studio</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769615386615/ccbf6a95-fe63-412a-955f-b4fa5e4234fe.png" alt="Create Gemini API key" class="image--center mx-auto" width="2542" height="1401" loading="lazy"></p>
<p>Create an <code>api</code> folder inside the Next.js <code>app</code> directory. This folder will contain the API routes used to generate AI content and create or schedule posts using the Late API.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app &amp;&amp; mkdir api
</code></pre>
<p>Next, create a <code>generate</code> folder inside the <code>api</code> directory and add a <code>route.ts</code> file. Copy the following code into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// 👇🏻 In api/generate/route.ts file</span>
<span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { GoogleGenAI } <span class="hljs-keyword">from</span> <span class="hljs-string">"@google/genai"</span>;

<span class="hljs-keyword">const</span> ai = <span class="hljs-keyword">new</span> GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
    <span class="hljs-keyword">const</span> { prompt } = <span class="hljs-keyword">await</span> req.json();

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> ai.models.generateContent({
            model: <span class="hljs-string">"gemini-3-flash-preview"</span>,
            contents: <span class="hljs-string">`
    You are a social media post generator, very efficient in generating engaging posts for Twitter (X). Given a topic, generate a creative and engaging post that captures attention and encourages interaction. This posts will always be within the character limit of X (Twitter) which is 280 characters, which includes any hashtags or mentions, spaces, punctuation, and emojis.

    The user will provide a topic or theme, and you will generate a post based on that input.
    Here is the instruction from the user:
    "<span class="hljs-subst">${prompt}</span>"`</span>,
        });
        <span class="hljs-keyword">if</span> (!response.text) {
            <span class="hljs-keyword">return</span> NextResponse.json(
                {
                    message: <span class="hljs-string">"Encountered an error generating the post."</span>,
                    success: <span class="hljs-literal">false</span>,
                },
                { status: <span class="hljs-number">400</span> },
            );
        }

        <span class="hljs-keyword">return</span> NextResponse.json(
            { message: response.text, success: <span class="hljs-literal">true</span> },
            { status: <span class="hljs-number">200</span> },
        );
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> NextResponse.json(
            { message: <span class="hljs-string">"Error generating post."</span>, success: <span class="hljs-literal">false</span> },
            { status: <span class="hljs-number">500</span> },
        );
    }
}
</code></pre>
<p>The <code>api/generate</code> endpoint accepts the user's prompt and generates post content using the <a target="_blank" href="https://ai.google.dev/gemini-api/docs/quickstart#javascript_1">Gemini API.</a></p>
<p>Now you can send a request to the newly created <code>/api/generate</code> endpoint from the <code>NewPost</code> component. Update the <code>handleGeneratePost</code> function as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleGeneratePost = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    setDisableBtn(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/generate"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        headers: {
            <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        },
        body: <span class="hljs-built_in">JSON</span>.stringify({ prompt }),
    });

    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> result.json();
    <span class="hljs-keyword">if</span> (data.success) {
        setUseAI(<span class="hljs-literal">false</span>);
        setContent(data.message);
        setPrompt(<span class="hljs-string">""</span>);
    }
    setDisableBtn(<span class="hljs-literal">false</span>);
};
</code></pre>
<p>The <code>handleGeneratePost</code> function accepts the user's prompt and returns the AI-generated content.</p>
<h2 id="heading-how-to-use-late-api-in-nextjs">How to Use Late API in Next.js</h2>
<p><a target="_blank" href="https://docs.getlate.dev/core/posts#create-a-draft-scheduled-or-immediate-post">Late</a> provides API endpoints that let you create, schedule, and manage posts programmatically. This allows you to integrate social media posting directly into your applications or automation workflows.</p>
<p>To get started, copy your Late API key and the account ID of your social media platforms into the <code>.env.local</code> file:</p>
<pre><code class="lang-bash">LATE_API_KEY=&lt;Late_API_key&gt;
ACCOUNT_ID=&lt;social_media_acct_id&gt;

<span class="hljs-comment"># Gemini API key</span>
GEMINI_API_KEY=&lt;gemini_API_key&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769615724826/e9500298-caa6-41a6-8d83-74d144d5c535.png" alt="Connect Twitter (X) account and copy account ID" class="image--center mx-auto" width="2526" height="1412" loading="lazy"></p>
<p><strong>Note:</strong> In this tutorial, we will be using Twitter (X) as the social media platform for scheduling posts. You can adapt the same workflow to other platforms supported by Late API by updating the platform and accountId values in your API requests.</p>
<p>Create an <code>api/post</code> endpoint to accept post content and schedule or publish posts using the Late API.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> api
mkdir post &amp;&amp; <span class="hljs-built_in">cd</span> post
touch route.ts
</code></pre>
<p>Then, add the following POST method to <code>post/route.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> utc <span class="hljs-keyword">from</span> <span class="hljs-string">"dayjs/plugin/utc"</span>;
<span class="hljs-keyword">import</span> dayjs <span class="hljs-keyword">from</span> <span class="hljs-string">"dayjs"</span>;

dayjs.extend(utc);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
    <span class="hljs-keyword">const</span> { content, publishAt } = <span class="hljs-keyword">await</span> req.json();

    <span class="hljs-comment">// Determine if the post should be scheduled or published immediately</span>
    <span class="hljs-keyword">const</span> nowUTC = publishAt ? dayjs(publishAt).utc() : <span class="hljs-literal">null</span>;
    <span class="hljs-keyword">const</span> publishAtUTC = nowUTC ? nowUTC.format(<span class="hljs-string">"YYYY-MM-DDTHH:mm"</span>) : <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://getlate.dev/api/v1/posts"</span>, {
            method: <span class="hljs-string">"POST"</span>,
            headers: {
                Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${process.env.LATE_API_KEY}</span>`</span>,
                <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
            },
            body: <span class="hljs-built_in">JSON</span>.stringify({
                content,
                platforms: [
                    {
                        platform: <span class="hljs-string">"twitter"</span>,
                        accountId: process.env.ACCOUNT_ID!,
                    },
                ],
                publishNow: !publishAt,
                scheduledFor: publishAtUTC,
            }),
        });

        <span class="hljs-keyword">const</span> { post, message } = <span class="hljs-keyword">await</span> response.json();

        <span class="hljs-keyword">if</span> (post?._id) {
            <span class="hljs-keyword">return</span> NextResponse.json({ message, success: <span class="hljs-literal">true</span> }, { status: <span class="hljs-number">201</span> });
        }

        <span class="hljs-keyword">return</span> NextResponse.json({ message: <span class="hljs-string">"Error occurred"</span>, success: <span class="hljs-literal">false</span> }, { status: <span class="hljs-number">500</span> });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> NextResponse.json({ message: <span class="hljs-string">"Error scheduling post."</span>, success: <span class="hljs-literal">false</span> }, { status: <span class="hljs-number">500</span> });
    }
}
</code></pre>
<p>From the code snippet above:</p>
<ul>
<li><p>The <code>api/post</code> endpoint accepts the post’s content and an optional <code>publishAt</code> time.</p>
</li>
<li><p>If <code>publishAt</code> is <code>null</code>, the post is published immediately. Otherwise, the time is converted to UTC for scheduling.</p>
</li>
<li><p>It then sends a request to the Late API using your API key and the account ID to create or schedule the post on the selected social media platform.</p>
</li>
</ul>
<p>You can also add a <strong>GET</strong> method to the <code>/api/post</code> endpoint to retrieve posts that have already been created or scheduled:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
            <span class="hljs-string">"https://getlate.dev/api/v1/posts?platform=twitter"</span>,
            {
                method: <span class="hljs-string">"GET"</span>,
                headers: {
                    Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${process.env.LATE_API_KEY}</span>`</span>,
                    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
                },
            },
        );

        <span class="hljs-keyword">const</span> { posts } = <span class="hljs-keyword">await</span> response.json();

        <span class="hljs-keyword">return</span> NextResponse.json({ posts }, { status: <span class="hljs-number">200</span> });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> NextResponse.json(
            { message: <span class="hljs-string">"Error fetching posts."</span>, success: <span class="hljs-literal">false</span> },
            { status: <span class="hljs-number">500</span> },
        );
    }
}
</code></pre>
<p>Next, update the <code>handlePostSubmit</code> function in <code>NewPost.tsx</code> to send a POST request to <code>/api/post</code>. This will create or schedule the post and notify the user of the result:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handlePostSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    setDisableBtn(<span class="hljs-literal">true</span>);

    <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
    <span class="hljs-keyword">const</span> selected = date ? <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(date) : <span class="hljs-literal">null</span>;
    <span class="hljs-keyword">const</span> publishAt = !selected || selected &lt;= now ? <span class="hljs-literal">null</span> : date;

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/post"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify({ content, publishAt }),
    });

    <span class="hljs-keyword">const</span> { message, success } = <span class="hljs-keyword">await</span> result.json();

    <span class="hljs-keyword">if</span> (success) {
        setContent(<span class="hljs-string">""</span>);
        setDate(<span class="hljs-string">""</span>);
        alert(<span class="hljs-string">"Success: "</span> + message);
    } <span class="hljs-keyword">else</span> {
        alert(<span class="hljs-string">"Error: "</span> + message);
    }

    setDisableBtn(<span class="hljs-literal">false</span>);
};
</code></pre>
<p>Finally, fetch all scheduled or published posts and render them in the <code>PostsQueue</code> component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fetchScheduledPosts = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/post"</span>, {
            method: <span class="hljs-string">"GET"</span>,
            headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        });
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
        setPosts(data.posts);
        setLoading(<span class="hljs-literal">false</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching scheduled posts:"</span>, error);
        setLoading(<span class="hljs-literal">false</span>);
    }
}, []);

useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchScheduledPosts();
}, [fetchScheduledPosts]);
</code></pre>
<p>🎉 Congratulations! You’ve successfully built an AI-powered social media post scheduler using Next.js, Gemini API, and Late API.</p>
<p>The source code for this tutorial is available on <a target="_blank" href="https://github.com/dha-stix/ai-post-scheduler">GitHub</a>.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/pW2GU3r8bTs" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you’ve learnt how to create and schedule social media posts across multiple platforms using a single scheduling platform, Late, and how to generate AI content using the Gemini API.</p>
<p>The <a target="_blank" href="https://getlate.dev/">Late API</a> is a powerful tool for automating social media tasks, posting at specific intervals, managing multiple accounts, and tracking analytics – all from one platform. By combining it with generative AI models like Gemini and automation tools like n8n or Zapier, you can build automated workflows that keep your audience engaged with minimal effort.</p>
<p>The <a target="_blank" href="https://ai.google.dev/gemini-api/docs/quickstart">Gemini API</a> also makes it easy to integrate AI-powered text, images, or code generation directly into your applications, opening up a wide range of creative possibilities.</p>
<p>Thank you for reading! 🎉</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Social Learning Platform using Next.js, Stream, and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ Social media and real-time communication have transformed how people interact, making it easier to share ideas, collaborate, and learn from others, regardless of location. From professional networks to online study groups, these platforms allow vario... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-social-learning-platform-using-nextjs-stream-and-supabase/</link>
                <guid isPermaLink="false">67c5c657e04393229206983b</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Mon, 03 Mar 2025 15:10:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741009946459/dba65929-1b65-4278-9601-4d047042753a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Social media and real-time communication have transformed how people interact, making it easier to share ideas, collaborate, and learn from others, regardless of location. From professional networks to online study groups, these platforms allow various forms of communication such as instant messaging, video calls, and content sharing.</p>
<p>In this tutorial, you'll learn how to build a social learning platform that connects students with professionals across various fields. The platform enables users to:</p>
<ul>
<li><p>Schedule video conferencing sessions that students can join,</p>
</li>
<li><p>Share posts or announcements about trending tools and upcoming sessions, and</p>
</li>
<li><p>Create community channels where students can engage with one another.</p>
</li>
</ul>
<p>The <a target="_blank" href="https://getstream.io/video/sdk/">Stream Video &amp; Audio SDK</a> and <a target="_blank" href="https://getstream.io/chat/sdk/">Stream Chat SDK</a> will enable us to integrate video calls and community channels easily into the application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740662075821/1a004f19-4889-4b57-921b-062cf1927261.gif" alt="Cartoon person with pink hair and green glasses holding a smartphone, which displays heart, poop, and smiley face emojis on the screen." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-app-overview">App Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-server-side-authentication-with-supabase">How to Set up Server-Side Authentication with Supabase</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-supabase-authentication-in-a-nextjs-application">How to Configure Supabase Authentication in a Next.js application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-student-authentication-with-supabase">Student Authentication with Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-instructor-authentication-with-supabase">Instructor Authentication with Supabase</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-the-application-database-design">The Application Database Design</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-access-policy-for-the-announcements-table">Access Policy for the Announcements Table</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-access-policy-for-the-instructors-table">Access Policy for the Instructors Table</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-access-policy-for-the-students-table">Access Policy for the Students Table</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-add-a-video-conferencing-feature-with-stream">How to Add a Video Conferencing Feature with Stream</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-setting-up-stream-video-amp-audio-sdk-in-nextjs">Setting Up Stream Video &amp; Audio SDK in Next.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-and-scheduling-calls-with-stream">Creating and Scheduling Calls with Stream</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-joining-stream-video-calls">Joining Stream Video Calls</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-stream-call-ui-components">Stream Call UI Components</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-a-group-chat-feature-using-stream-chat-messaging">How to Integrate a Group Chat Feature Using Stream Chat Messaging</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-setting-up-the-stream-chat-sdk-in-nextjs">Setting Up the Stream Chat SDK in Next.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-stream-chat-ui-components">Stream Chat UI Components</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
</ul>
<h2 id="heading-app-overview">App Overview</h2>
<p>The application consists of two types of users (students and instructors), each with access to specific features:</p>
<p>Students can do the following:</p>
<ul>
<li><p>View an activity feed with posts from instructors and react to them.</p>
</li>
<li><p>Follow instructors in their field of interest.</p>
</li>
<li><p>Join upcoming video sessions and community channels.</p>
</li>
<li><p>Each student has an interest attribute that helps match them with relevant instructors.</p>
</li>
</ul>
<p>Instructors can also:</p>
<ul>
<li><p>Access a dashboard showing their follower count and post activity.</p>
</li>
<li><p>Schedule video conferences for students to join.</p>
</li>
<li><p>Make announcements or share posts.</p>
</li>
<li><p>Create community channels (if they haven't already).</p>
</li>
<li><p>The platform suggests instructors to students based on shared career interests.</p>
</li>
</ul>
<p>Here is an image showing the various functions that the users can perform:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740723106527/fd8af07a-1919-42f0-9e9a-ed380a0b77cc.png" alt="Flowchart titled &quot;User Functions&quot; showing a hierarchy. &quot;Users&quot; split into &quot;Students&quot; and &quot;Instructors&quot;. Students can &quot;Follow Instructors&quot;, &quot;Join Video Sessions&quot;, &quot;Join Community Channels&quot;, and &quot;Read and React to Posts&quot;. Instructors can &quot;View Dashboard&quot;, &quot;Create Posts&quot;, &quot;Schedule Video Sessions&quot;, and &quot;Create Community Channels&quot;." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To fully understand this tutorial, you need to have a basic understanding of React or Next.js.</p>
<p>We will use the following tools:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase</a>: a Backend-as-a-service platform that makes it easy to integrate authentication, database, real-time communication, file storage, and edge functions within your software applications. It also supports multiple programming languages.</p>
</li>
<li><p><a target="_blank" href="https://getstream.io/chat/docs/sdk/react/">Stream Chat</a> and <a target="_blank" href="https://getstream.io/video/docs/react/">Audio &amp; Video SDK</a>: a real-time communication platform that enables you to add video, chat, and various types of communication to your application.</p>
</li>
<li><p><a target="_blank" href="https://ui.shadcn.com/docs/installation/next">Shadcn UI</a>: a UI component library that provides customizable, beautifully designed, and accessible UI components for your applications.</p>
</li>
</ul>
<p>Create a Next.js project by running the following code snippet:</p>
<pre><code class="lang-bash">npx create-next-app stream-lms
</code></pre>
<p>Install the package dependencies for the project:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js @supabase/ssr @stream-io/node-sdk @stream-io/video-react-sdk stream-chat stream-chat-react @emoji-mart/data @emoji-mart/react
</code></pre>
<p>To install the Shadcn UI library, follow <a target="_blank" href="https://ui.shadcn.com/docs/installation/next">the installation guide.</a></p>
<p>Once everything is set up, your Next.js project is ready. Now, let's start building! 🚀</p>
<h2 id="heading-how-to-set-up-server-side-authentication-with-supabase">How to Set up Server-Side Authentication with Supabase</h2>
<p>Here, you'll learn how to configure Supabase, add server-side authentication, and protect pages from unauthorized users in a Next.js application. You'll also learn how to handle the authentication logic efficiently using <a target="_blank" href="https://nextjs.org/docs/13/app/api-reference/functions/server-actions#with-client-components">Next.js server actions</a>.</p>
<h3 id="heading-how-to-configure-supabase-authentication-in-a-nextjs-application">How to Configure Supabase Authentication in a Next.js application</h3>
<p>First, create a <a target="_blank" href="https://supabase.com/">Supabase account</a> and an organization that will contain your various Supabase projects.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740653729412/35339612-4688-489e-b9d2-2cc825962519.png" alt="Screenshot of a form on the Supabase website to create a new organization. It includes fields for organization name, type, and plan, with options such as &quot;Personal&quot; and &quot;Free - $0/month.&quot; There are buttons for &quot;Cancel&quot; and &quot;Create organization.&quot;" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Add a new Supabase project to the organisation and copy the following credentials on your dashboard into a <code>.env.local</code> file at the root of your project:</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SUPABASE_ANON_KEY=&lt;anon_key_from_Supabase_dashboard&gt;
NEXT_PUBLIC_SUPABASE_URL=&lt;supabase_project_url&gt;
</code></pre>
<p>Create a <code>utils/supabase</code> folder at the root of the Next.js project and add the following files to the folder: <code>client.ts</code>, <code>middleware.ts</code>, and <code>server.ts</code>.</p>
<pre><code class="lang-bash">mkdir utils &amp;&amp; <span class="hljs-built_in">cd</span> utils
mkdir supabase &amp;&amp; <span class="hljs-built_in">cd</span> supabase
touch client.ts middleware.ts server.ts
</code></pre>
<p>Copy the following code into <code>utils/supabase/client.ts</code>. This initializes a Supabase browser client to interact with Supabase on client-side routes:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
}
</code></pre>
<p>Next, copy the following code into <code>utils/supabase/server.ts</code>. This creates a Supabase server client for handling authentication and interacting with Supabase in server-side requests:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();

    <span class="hljs-keyword">return</span> createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
            cookies: {
                getAll() {
                    <span class="hljs-keyword">return</span> cookieStore.getAll();
                },
                setAll(cookiesToSet) {
                    <span class="hljs-keyword">try</span> {
                        cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
                            cookieStore.set(name, value, options)
                        );
                    } <span class="hljs-keyword">catch</span> {
                        <span class="hljs-comment">// The `setAll` method was called from a Server Component.</span>
                        <span class="hljs-comment">// This can be ignored if you have middleware refreshing</span>
                        <span class="hljs-comment">// user sessions.</span>
                    }
                },
            },
        }
    );
}
</code></pre>
<p>Now, copy the following code into <code>utils/supabase/middleware.ts</code>. This middleware creates authentication cookies and protects pages from unauthorized access:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { NextResponse, <span class="hljs-keyword">type</span> NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateSession</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
    <span class="hljs-keyword">let</span> supabaseResponse = NextResponse.next({
        request,
    });
    <span class="hljs-comment">//👇🏻 creates the Supabase cookie functions</span>
    <span class="hljs-keyword">const</span> supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
            cookies: {
                getAll() {
                    <span class="hljs-keyword">return</span> request.cookies.getAll();
                },
                setAll(cookiesToSet) {
                    cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value }</span>) =&gt;</span>
                        request.cookies.set(name, value)
                    );
                    supabaseResponse = NextResponse.next({
                        request,
                    });
                    cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
                        supabaseResponse.cookies.set(name, value, options)
                    );
                },
            },
        }
    );

    <span class="hljs-comment">// 👉🏻 placeholder for protected route controller</span>
}
</code></pre>
<p>To enforce authentication, add the following code inside the placeholder in <code>middleware.ts</code><strong>.</strong> This checks if a user is signed in and redirects unauthenticated users to the login page:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 gets current user</span>
<span class="hljs-keyword">const</span> {
    data: { user },
} = <span class="hljs-keyword">await</span> supabase.auth.getUser();

<span class="hljs-comment">//👇🏻 declares protected routes</span>
<span class="hljs-keyword">if</span> (
    !user &amp;&amp;
    request.nextUrl.pathname !== <span class="hljs-string">"/"</span> &amp;&amp;
    !request.nextUrl.pathname.startsWith(<span class="hljs-string">"/instructor/auth"</span>) &amp;&amp;
    !request.nextUrl.pathname.startsWith(<span class="hljs-string">"/student/auth"</span>)
) {
    <span class="hljs-comment">//👇🏻 Redirect unauthenticated users to the login page</span>
    <span class="hljs-keyword">const</span> url = request.nextUrl.clone();
    url.pathname = <span class="hljs-string">"/student/auth/login"</span>; <span class="hljs-comment">// 👈🏼 redirect page</span>
    <span class="hljs-keyword">return</span> NextResponse.redirect(url);
}
<span class="hljs-comment">//👇🏻 returns Supabase response</span>
<span class="hljs-keyword">return</span> supabaseResponse;
</code></pre>
<p>Add another <code>middleware.ts</code> file to the root of the Next.js project and copy the following code into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { updateSession } <span class="hljs-keyword">from</span> <span class="hljs-string">"./utils/supabase/middleware"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">middleware</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> updateSession(request);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
    matcher: [
        <span class="hljs-comment">/*
         * Match all request paths except for the ones starting with:
         * - _next/static (static files)
         * - _next/image (image optimization files)
         * - favicon.ico (favicon file)
         * Feel free to modify this pattern to include more paths.
         */</span>
        <span class="hljs-string">"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"</span>,
    ],
};
</code></pre>
<p>Finally, create an <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/src/app/auth/confirm/route.ts"><strong>auth/confirm</strong></a> route and <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/src/app/error/page.tsx"><strong>error</strong></a> page within the Next.js app folder.</p>
<p>You've successfully <a target="_blank" href="https://supabase.com/docs/guides/auth/server-side/nextjs">configured authentication in your Next.js project</a> using Supabase.</p>
<h3 id="heading-student-authentication-with-supabase">Student Authentication with Supabase</h3>
<p>In this section, you will learn how to create the signup and login functions for the students within the application.</p>
<p>First, create an <strong>actions</strong> folder in the root of your Next.js project and add an <code>auth.ts</code> file inside it. This file will contain all Supabase authentication functions.</p>
<p>Add the following imports to the top of the <code>auth.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use server"</span>;
<span class="hljs-keyword">import</span> { revalidatePath } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/cache"</span>;
<span class="hljs-keyword">import</span> { redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/supabase/server"</span>;
</code></pre>
<p>Next, you need to create the server functions that accept form data from the client and sign users up or log them in as students.</p>
<p>Copy the following code snippet into the <code>actions/auth.ts</code> file to create the user sign-up function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">studentSignUp</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-keyword">const</span> supabase = <span class="hljs-keyword">await</span> createClient();

    <span class="hljs-comment">//👇🏻 Extract form data</span>
    <span class="hljs-keyword">const</span> credentials = {
        email: formData.get(<span class="hljs-string">"email"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        password: formData.get(<span class="hljs-string">"password"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        interest: formData.get(<span class="hljs-string">"interest"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        name: formData.get(<span class="hljs-string">"name"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
    };

    <span class="hljs-comment">//👇🏻 Supabase sign up function (options attribute :- for user metadata)</span>
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
        email: credentials.email,
        password: credentials.password,
        options: {
            data: {
                interest: credentials.interest,
                name: credentials.name,
            },
        },
    });

    <span class="hljs-comment">//👉🏻 return user or error object</span>
}
</code></pre>
<p>The code snippet above accepts the form credentials such as email, password, interest, and name, and signs the user up as a Supabase user.</p>
<p>Modify the function to return the user or error object.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">studentSignUp</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-comment">//...form inputs and supabase functions</span>

    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">return</span> { error: error.message, status: error.status, user: <span class="hljs-literal">null</span> };
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (data.user?.identities?.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> { error: <span class="hljs-string">"User already exists"</span>, status: <span class="hljs-number">409</span>, user: <span class="hljs-literal">null</span> };
    }

    revalidatePath(<span class="hljs-string">"/"</span>, <span class="hljs-string">"layout"</span>);
    <span class="hljs-keyword">return</span> { error: <span class="hljs-literal">null</span>, status: <span class="hljs-number">200</span>, user: data.user };
}
</code></pre>
<p>Create the student login function as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">studentLogIn</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-keyword">const</span> supabase = <span class="hljs-keyword">await</span> createClient();

    <span class="hljs-keyword">const</span> credentials = {
        email: formData.get(<span class="hljs-string">"email"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        password: formData.get(<span class="hljs-string">"password"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
    };
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword(credentials);

    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">return</span> { error: error.message, status: error.status, user: <span class="hljs-literal">null</span> };
    }
    <span class="hljs-comment">//👇🏻 only instructors have an image attribute</span>
    <span class="hljs-keyword">if</span> (data &amp;&amp; data.user.user_metadata.image) {
        <span class="hljs-keyword">return</span> { error: <span class="hljs-string">"You are not a student"</span>, status: <span class="hljs-number">400</span>, user: <span class="hljs-literal">null</span> };
    }

    <span class="hljs-comment">//👉🏻 create a student row and add to the database</span>

    revalidatePath(<span class="hljs-string">"/"</span>, <span class="hljs-string">"layout"</span>);
    <span class="hljs-keyword">return</span> { error: <span class="hljs-literal">null</span>, status: <span class="hljs-number">200</span>, user: data.user };
}
</code></pre>
<p>The code above takes the student's email and password to log them into the application.</p>
<ul>
<li><p>If an error occurs, it returns an error message.</p>
</li>
<li><p>If the user object includes an image attribute (indicating that they are an instructor), they are prevented from logging in.</p>
</li>
</ul>
<p>Once the student is signed in, you must store their details in a Supabase table. This allows you to add a <code>following_list</code> column that tracks the instructors they follow. The list will be updated whenever the student follows or unfollows an instructor.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">studentLogIn</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-comment">//...other functions</span>

    <span class="hljs-keyword">const</span> { data: existingUser } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">"students"</span>)
        .select()
        .eq(<span class="hljs-string">"email"</span>, credentials.email)
        .single();

    <span class="hljs-comment">//👇🏻 if student doesn't exist</span>
    <span class="hljs-keyword">if</span> (!existingUser) {
        <span class="hljs-keyword">const</span> { error: insertError } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"students"</span>).insert({
            email: credentials.email,
            name: data.user.user_metadata.name,
            interest: data.user.user_metadata.interest,
            id: data.user.id,
            following_list: [] <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>[],
        });

        <span class="hljs-keyword">if</span> (insertError) {
            <span class="hljs-keyword">return</span> { error: insertError.message, status: <span class="hljs-number">500</span>, user: <span class="hljs-literal">null</span> };
        }
    }

    revalidatePath(<span class="hljs-string">"/"</span>, <span class="hljs-string">"layout"</span>);
    <span class="hljs-keyword">return</span> { error: <span class="hljs-literal">null</span>, status: <span class="hljs-number">200</span>, user: data.user };
}
</code></pre>
<p>Every time a student logs in, the code checks if they already exist in the <code>students</code> table.</p>
<ul>
<li><p>If the student is found, no new entry is created.</p>
</li>
<li><p>If the student is not found, a new row with their details is added.</p>
</li>
</ul>
<p>Each student’s data includes two primary keys: <code>id</code> and <code>email</code> and additional columns: <code>interest</code>, <code>name</code>, and <code>following_list</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740654932982/724fcf11-55e3-4163-9a6e-0b2771d0874c.gif" alt="Student registration form with fields for full name, email address, interest, and password. Includes a &quot;Register&quot; button and a sign-in option for existing accounts. The browser tab shows the URL &quot;localhost:3000&quot;." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-instructor-authentication-with-supabase">Instructor Authentication with Supabase</h3>
<p>The instructor's user object is quite different from the student's. It includes data such as email, password, name, interest, occupation, bio, URL, and image.</p>
<p>Add the following function to <code>actions/auth.ts</code> to handle instructor sign-ups:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">instructorSignUp</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-keyword">const</span> supabase = <span class="hljs-keyword">await</span> createClient();

    <span class="hljs-comment">//👇🏻 get user credentials from the form</span>
    <span class="hljs-keyword">const</span> credentials = {
        email: formData.get(<span class="hljs-string">"email"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        password: formData.get(<span class="hljs-string">"password"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        interest: formData.get(<span class="hljs-string">"interest"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        name: formData.get(<span class="hljs-string">"name"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        occupation: formData.get(<span class="hljs-string">"occupation"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        bio: formData.get(<span class="hljs-string">"bio"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        url: formData.get(<span class="hljs-string">"url"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        image: formData.get(<span class="hljs-string">"image"</span>) <span class="hljs-keyword">as</span> File,
    };

    <span class="hljs-comment">//👉🏻 following code snippet below</span>
}
</code></pre>
<p>Next, upload the image to Supabase Storage and retrieve its download URL before signing up the user as an instructor. Update the <code>instructorSignUp</code> function to show this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">instructorSignUp</span>(<span class="hljs-params">formData: FormData</span>) </span>{
    <span class="hljs-comment">//👇🏻 upload instructor's image</span>
    <span class="hljs-keyword">const</span> { data: imageData, error: imageError } = <span class="hljs-keyword">await</span> supabase.storage
        .from(<span class="hljs-string">"headshots"</span>)
        .upload(<span class="hljs-string">`<span class="hljs-subst">${crypto.randomUUID()}</span>/image`</span>, credentials.image);

    <span class="hljs-keyword">if</span> (imageError) {
        <span class="hljs-keyword">return</span> { error: imageError.message, status: <span class="hljs-number">500</span>, user: <span class="hljs-literal">null</span> };
    }
    <span class="hljs-comment">//👇🏻 get the image URL</span>
    <span class="hljs-keyword">const</span> imageURL = <span class="hljs-string">`<span class="hljs-subst">${process.env.STORAGE_URL!}</span><span class="hljs-subst">${imageData.fullPath}</span>`</span>;

    <span class="hljs-comment">//👇🏻 authenticate user as instructor</span>
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
        email: credentials.email,
        password: credentials.password,
        options: {
            data: {
                interest: credentials.interest,
                name: credentials.name,
                occupation: credentials.occupation,
                bio: credentials.bio,
                url: credentials.url,
                image: imageURL,
            },
        },
    });

    <span class="hljs-comment">//👇🏻 return user or error object</span>
    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">return</span> { error: error.message, status: error.status, user: <span class="hljs-literal">null</span> };
    }

    revalidatePath(<span class="hljs-string">"/"</span>, <span class="hljs-string">"layout"</span>);
    <span class="hljs-keyword">return</span> { error: <span class="hljs-literal">null</span>, status: <span class="hljs-number">200</span>, user: data.user };
}
</code></pre>
<p>Finally, an <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/actions/auth.ts">instructor login function</a> that authenticates the user, similar to the student login function, should be created. It should check whether the instructor already exists in the <code>instructors</code> table. If the instructor does not exist, execute the function to add the instructor's user object to the database table.</p>
<p>Here is the Supabase function for adding an instructor to the table:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { error: insertError } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"instructors"</span>).insert({
    email: credentials.email,
    name: data.user.user_metadata.name,
    occupation: data.user.user_metadata.occupation,
    bio: data.user.user_metadata.bio,
    url: data.user.user_metadata.url,
    image: data.user.user_metadata.image,
    id: data.user.id,
    interest: data.user.user_metadata.interest,
    followers: [],
});
</code></pre>
<p>The <code>instructors</code> table includes an additional <code>followers</code> attribute, which stores an array of student IDs following the instructor. You can find the <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/actions/auth.ts">complete code on GitHub</a>.</p>
<p>Additionally, authentication functions like <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/actions/auth.ts"><strong>getUserSession</strong></a> and <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/actions/auth.ts"><strong>logOut</strong></a> must be created. These functions will retrieve the current user's object and allow them to log out when necessary, such as when clicking a logout button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740655472825/4c038412-fe6d-4449-b2a9-d9d3649da2f8.gif" alt="Instructor login page with fields for email and password, a &quot;Sign in&quot; button, and a link to create an account. A sidebar on the left displays &quot;LinkedUp&quot; with a &quot;Student Sign-in&quot; link." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-the-application-database-design">The Application Database Design</h2>
<p>In the previous section, we created two database tables: <code>instructors</code> and <code>students</code>, which store instructors and students separately. Instructors can also upload headshot images to <a target="_blank" href="https://supabase.com/docs/guides/storage/quickstart">Supabase Storage</a>.</p>
<p>In this section, you'll learn how to create these tables, define their access policies, and retrieve or modify data within the tables.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Announcements (data type)</strong></td><td><strong>Instructors (data type)</strong></td><td><strong>Students (data type)</strong></td></tr>
</thead>
<tbody>
<tr>
<td>id (int8)</td><td>id (uuid)</td><td>id (uuid)</td></tr>
<tr>
<td>created_at (timestamptz)</td><td>created_at (timestamptz)</td><td>created_at (timestamptz)</td></tr>
<tr>
<td>author_name (text)</td><td>name (text)</td><td>email (text)</td></tr>
<tr>
<td>interest (text)</td><td>email (text)</td><td>name (text)</td></tr>
<tr>
<td>author_title (text)</td><td>occupation (text)</td><td>interest (text)</td></tr>
<tr>
<td>author_id (uuid)</td><td>bio (text)</td><td>following_list (uuid[])</td></tr>
<tr>
<td>content (text)</td><td>url (text)</td><td></td></tr>
<tr>
<td>likes (uuid [])</td><td>interest (text)</td><td></td></tr>
<tr>
<td>author_image (text)</td><td>image (text)</td><td></td></tr>
<tr>
<td></td><td>followers (uuid[])</td></tr>
</tbody>
</table>
</div><p><strong>Note:</strong> The <code>instructors</code> table includes an <code>image</code> column that stores the instructor's headshot URL. You can obtain this by creating a Supabase bucket named <code>headshot</code> and uploading the image when the instructor signs up.</p>
<p>The <code>instructors</code> and <code>students</code> tables have two primary keys: <code>id</code> and <code>email</code>.</p>
<p>Supabase allows you to define policies for your tables, controlling the operations different users can perform within the application.</p>
<p>Next, let’s create the access policies for each table.</p>
<h3 id="heading-access-policy-for-the-announcements-table">Access Policy for the Announcements Table</h3>
<p>The <code>announcements</code> table has four access policies:</p>
<ul>
<li><p>Enable delete operation for users based on their user ID.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable delete for users based on user_id"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"announcements"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">public</span>
  <span class="hljs-keyword">using</span> (
    (( <span class="hljs-keyword">SELECT</span> auth.uid() <span class="hljs-keyword">AS</span> uid) = author_id)
  );
</code></pre>
</li>
<li><p>Enable insert operation for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable insert for authenticated users only"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"announcements"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
<li><p>Enable read access for all users.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable read access for all users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"announcements"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">public</span>
  <span class="hljs-keyword">using</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
<li><p>Enable update operation for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable update for authenticated users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"announcements"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">using</span> (
    (auth.role() = <span class="hljs-string">'authenticated'</span>::<span class="hljs-built_in">text</span>)
  );
</code></pre>
</li>
</ul>
<h3 id="heading-access-policy-for-the-instructors-table">Access Policy for the Instructors Table</h3>
<p>The <code>instructors</code> table has three policies:</p>
<ul>
<li><p>Allow only authenticated users to update the <code>instructors</code> table.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Allow only authenticated users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"instructors"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">using</span> (
    (auth.role() = <span class="hljs-string">'authenticated'</span>::<span class="hljs-built_in">text</span>)
  );
</code></pre>
</li>
<li><p>Enable insert operation for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable insert for authenticated users only"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"instructors"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
<li><p>Enable read access for all users.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable read access for all users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"instructors"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">public</span>
  <span class="hljs-keyword">using</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
</ul>
<h3 id="heading-access-policy-for-the-students-table">Access Policy for the Students Table</h3>
<p>The <code>students</code> table has three access policies:</p>
<ul>
<li><p>Enable insert operation for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable insert for authenticated users only"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"students"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
<li><p>Enable update operation for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Enable update for only authenticated users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"students"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">using</span> ((auth.role() = <span class="hljs-string">'authenticated'</span>::<span class="hljs-built_in">text</span>))
</code></pre>
</li>
<li><p>Enable read access for authenticated users only.</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">alter</span> <span class="hljs-keyword">policy</span> <span class="hljs-string">"Read access for only authenticated users"</span>
  <span class="hljs-keyword">on</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"students"</span>
  <span class="hljs-keyword">to</span> <span class="hljs-keyword">authenticated</span>
  <span class="hljs-keyword">using</span> (
    <span class="hljs-literal">true</span>
  );
</code></pre>
</li>
</ul>
<h2 id="heading-how-to-add-a-video-conferencing-feature-with-stream">How to Add a Video Conferencing Feature with Stream</h2>
<p>In this section, I'll walk you through adding a video conferencing feature to the application using the <a target="_blank" href="https://getstream.io/video/docs/react/">Stream Audio &amp; Video SDK</a>. This will enable instructors to schedule educational sessions and allow students to join the meetings.</p>
<h3 id="heading-setting-up-stream-video-amp-audio-sdk-in-nextjs">Setting Up Stream Video &amp; Audio SDK in Next.js</h3>
<p>Create a <a target="_blank" href="https://getstream.io/">Stream account</a> and a new organization that holds all your apps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740657831403/ba044353-b0f4-4380-82cf-5abeedd68ac9.png" alt="Form for creating an organization, with fields for organization name, e-mail address, and website URL, and buttons labeled &quot;Cancel&quot; and &quot;Submit&quot;." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Add a new app to the organization and copy the Stream API and Secret key into the <code>.env.local</code> file.</p>
<pre><code class="lang-bash">NEXT_PUBLIC_STREAM_API_KEY=&lt;paste_from_Stream_app_dashboard&gt;
STREAM_SECRET_KEY=&lt;paste_from_Stream_app_dashboard&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740658025617/e8fb8a44-0a16-4730-875a-be3c2255276f.png" alt="Dashboard interface displaying chat overview with key metrics like Monthly Active Users (4 MAUs), Max Concurrent Connections (2), and Message Volume (3). Includes app access keys created on February 17th, 2025." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Create a new file named <code>stream.action.ts</code> inside the <code>actions</code> folder at the root of your Next.js project. This is the same folder where the authentication server actions for Supabase are stored. Then, copy the following code snippet into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use server"</span>;

<span class="hljs-keyword">import</span> { getUserSession } <span class="hljs-keyword">from</span> <span class="hljs-string">"./auth"</span>;
<span class="hljs-keyword">import</span> { StreamClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/node-sdk"</span>;

<span class="hljs-keyword">const</span> STREAM_API_KEY = process.env.NEXT_PUBLIC_STREAM_API_KEY!;
<span class="hljs-keyword">const</span> STREAM_API_SECRET = process.env.STREAM_SECRET_KEY!;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> tokenProvider = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> { user } = <span class="hljs-keyword">await</span> getUserSession();

    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"User is not authenticated"</span>);
    <span class="hljs-keyword">if</span> (!STREAM_API_KEY) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Stream API key secret is missing"</span>);
    <span class="hljs-keyword">if</span> (!STREAM_API_SECRET) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Stream API secret is missing"</span>);

    <span class="hljs-keyword">const</span> streamClient = <span class="hljs-keyword">new</span> StreamClient(STREAM_API_KEY, STREAM_API_SECRET);

    <span class="hljs-keyword">const</span> expirationTime = <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>) + <span class="hljs-number">3600</span>;
    <span class="hljs-keyword">const</span> issuedAt = <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>) - <span class="hljs-number">60</span>;

    <span class="hljs-keyword">const</span> token = streamClient.generateUserToken({
        user_id: user.id,
        exp: expirationTime,
        validity_in_seconds: issuedAt,
    });

    <span class="hljs-keyword">return</span> token;
};
</code></pre>
<ul>
<li><p>From the code snippet above,</p>
<ul>
<li><p>The <strong>getUserSession</strong> function returns the Supabase user object for the current user.</p>
</li>
<li><p>The <strong>tokenProvider</strong> function generates an authentication token for the user, enabling Stream to identify and manage users during real-time communication.</p>
</li>
</ul>
</li>
</ul>
<p>Create a <code>providers</code> folder containing a <code>StreamVideoProvider</code> component within the Next.js app folder and copy the following code snippet into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../utils/supabase/client"</span>;
<span class="hljs-keyword">import</span> { tokenProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../actions/stream.action"</span>;
<span class="hljs-keyword">import</span> { StreamVideo, StreamVideoClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;
<span class="hljs-keyword">import</span> { useState, ReactNode, useEffect, useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Loader2 } <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;

<span class="hljs-keyword">const</span> apiKey = process.env.NEXT_PUBLIC_STREAM_API_KEY!;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> StreamVideoProvider = <span class="hljs-function">(<span class="hljs-params">{ children }: { children: ReactNode }</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> [videoClient, setVideoClient] = useState&lt;StreamVideoClient | <span class="hljs-literal">null</span>&gt;(
        <span class="hljs-literal">null</span>
    );
    <span class="hljs-keyword">const</span> supabase = createClient();

    <span class="hljs-keyword">const</span> getUser = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-comment">//👉🏻 get user object from Supabase</span>
        <span class="hljs-comment">//👉🏻 set Stream user data</span>
        <span class="hljs-comment">// 👉🏻 initialize Stream video client using the Stream API key, Stream user data, and token Provider</span>
    }, [supabase.auth]);

    useEffect(<span class="hljs-function">() =&gt;</span> {
        getUser();
    }, [getUser]);

    <span class="hljs-keyword">if</span> (!videoClient)
        <span class="hljs-keyword">return</span> (
            &lt;div className=<span class="hljs-string">'h-screen flex items-center justify-center'</span>&gt;
                &lt;Loader2 size=<span class="hljs-string">'32'</span> className=<span class="hljs-string">'mx-auto animate-spin'</span> /&gt;
            &lt;/div&gt;
        );

    <span class="hljs-keyword">return</span> &lt;StreamVideo client={videoClient}&gt;{children}&lt;/StreamVideo&gt;;
};
</code></pre>
<p>The <code>StreamVideoProvider</code> component is initialized and manages Stream’s video functionality across the application. It wraps all pages that require access to Stream's real-time video features. This includes:</p>
<ul>
<li><p><code>instructor/[id]</code> – displays an instructor’s upcoming sessions.</p>
</li>
<li><p><code>instructor/dashboard</code> – allows instructors to schedule new video calls.</p>
</li>
</ul>
<p>Update the <code>getUser</code> function as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getUser = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
    <span class="hljs-keyword">const</span> { user } = data;
    <span class="hljs-keyword">if</span> (error || !user || !apiKey) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">if</span> (!tokenProvider) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">let</span> streamUser;

    <span class="hljs-keyword">if</span> (user.user_metadata?.image) {
        streamUser = {
            <span class="hljs-comment">// 👇🏻 user is an instructor</span>
            id: user.id,
            name: user.user_metadata?.name,
            image: user.user_metadata?.image,
        };
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// 👇🏻 user is a student</span>
        streamUser = {
            id: user.id,
            name: user.user_metadata?.name,
        };
    }

    <span class="hljs-comment">//👇🏻 create s Stream video client</span>
    <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> StreamVideoClient({
        apiKey,
        user: streamUser,
        tokenProvider,
    });

    setVideoClient(client);
}, [supabase.auth]);
</code></pre>
<p>The <code>getUser</code> function retrieves the current user's data from Supabase Auth, sets up the Stream user, and initializes a Stream video client using the Stream API key, the user’s object and the token.</p>
<h3 id="heading-creating-and-scheduling-calls-with-stream">Creating and Scheduling Calls with Stream</h3>
<p>Here, you will learn how to allow instructors to schedule calls using the Stream Video &amp; Audio SDK.</p>
<p>Before we proceed, create a <code>hooks</code> folder within the Next.js app folder and add these files:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app &amp;&amp; mkdir hooks
<span class="hljs-built_in">cd</span> hooks
touch useGetCallById.ts useGetCalls.ts
</code></pre>
<p>The <code>useGetCallById</code> file defines a React hook that fetches details of a specific Stream call via its ID, while the <code>useGetCalls</code> hook retrieves all calls created by a particular Stream user.</p>
<p>Let's create these custom React hooks.</p>
<p>Copy the following code snippet into the <code>useGetCallById.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Call, useStreamVideoClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useGetCallById = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span> | <span class="hljs-built_in">string</span>[]</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> [call, setCall] = useState&lt;Call&gt;();
    <span class="hljs-keyword">const</span> [isCallLoading, setIsCallLoading] = useState(<span class="hljs-literal">true</span>);

    <span class="hljs-keyword">const</span> client = useStreamVideoClient();

    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (!client) <span class="hljs-keyword">return</span>;

        <span class="hljs-keyword">const</span> loadCall = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-comment">// https://getstream.io/video/docs/react/guides/querying-calls/#filters</span>
                <span class="hljs-keyword">const</span> { calls } = <span class="hljs-keyword">await</span> client.queryCalls({
                    filter_conditions: { id },
                });

                <span class="hljs-keyword">if</span> (calls.length &gt; <span class="hljs-number">0</span>) setCall(calls[<span class="hljs-number">0</span>]);

                setIsCallLoading(<span class="hljs-literal">false</span>);
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-built_in">console</span>.error(error);
                setIsCallLoading(<span class="hljs-literal">false</span>);
            }
        };

        loadCall();
    }, [client, id]);

    <span class="hljs-keyword">return</span> { call, isCallLoading };
};
</code></pre>
<p>Add the following to the <code>useGetCalls.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Call, useStreamVideoClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;
<span class="hljs-keyword">import</span> { useParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useGetCalls = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> client = useStreamVideoClient();
    <span class="hljs-keyword">const</span> [calls, setCalls] = useState&lt;Call[]&gt;();
    <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> { id } = useParams&lt;{ id: <span class="hljs-built_in">string</span> }&gt;();

    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> loadCalls = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">if</span> (!client || !id) <span class="hljs-keyword">return</span>;

            setIsLoading(<span class="hljs-literal">true</span>);

            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">const</span> { calls } = <span class="hljs-keyword">await</span> client.queryCalls({
                    sort: [{ field: <span class="hljs-string">"starts_at"</span>, direction: <span class="hljs-number">1</span> }],
                    filter_conditions: {
                        starts_at: { $exists: <span class="hljs-literal">true</span> },
                        $or: [{ created_by_user_id: id }, { members: { $in: [id] } }],
                    },
                });

                setCalls(calls);
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-built_in">console</span>.error(error);
            } <span class="hljs-keyword">finally</span> {
                setIsLoading(<span class="hljs-literal">false</span>);
            }
        };

        loadCalls();
    }, [client, id]);

    <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
    <span class="hljs-comment">//👇🏻 upcoming calls</span>
    <span class="hljs-keyword">const</span> upcomingCalls = calls?.filter(<span class="hljs-function">(<span class="hljs-params">{ state: { startsAt } }: Call</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> startsAt &amp;&amp; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(startsAt) &gt; now;
    });
    <span class="hljs-comment">//👇🏻 ongoing calls</span>
    <span class="hljs-keyword">const</span> ongoingCalls = calls?.filter(
        <span class="hljs-function">(<span class="hljs-params">{ state: { startsAt, endedAt } }: Call</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> startsAt &amp;&amp; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(startsAt) &lt; now &amp;&amp; !endedAt;
        }
    );

    <span class="hljs-keyword">return</span> { upcomingCalls, isLoading, ongoingCalls };
};
</code></pre>
<p>The <strong>useGetCalls</strong> hook retrieves all calls where the instructor is either the creator or a participant, returning both current and upcoming calls. It also returns an <strong>isLoading</strong> state to indicate when data is being fetched, allowing for conditional rendering.</p>
<p>Add the function below to the instructor's dashboard to allow instructors to create or schedule calls. This function accepts a call description along with the scheduled date and time.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 imports</span>
<span class="hljs-keyword">import</span> { useStreamVideoClient, Call } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;
<span class="hljs-keyword">const</span> client = useStreamVideoClient();
<span class="hljs-comment">//👇🏻 Form states</span>
<span class="hljs-keyword">const</span> [description, setDescription] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> [dateTime, setDateTime] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

<span class="hljs-keyword">const</span> handleScheduleMeeting = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">if</span> (!client || !user) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> id = crypto.randomUUID();
        <span class="hljs-keyword">const</span> call = client.call(<span class="hljs-string">"default"</span>, id);
        <span class="hljs-keyword">if</span> (!call) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Failed to create meeting"</span>);
    <span class="hljs-comment">//👇🏻 create Stream call</span>
        <span class="hljs-keyword">await</span> call.getOrCreate({
            data: {
                starts_at: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(dateTime).toISOString(),
                custom: {
                    description,
                },
            },
        });

        <span class="hljs-comment">//👇🏻 Call object</span>
        <span class="hljs-built_in">console</span>.log({ call });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(error);
    }
};
</code></pre>
<p>The code snippet above initializes a Stream video call with a default call type. It assigns the call a unique ID, sets the scheduled date and time, and includes a custom description.</p>
<p><strong>Note:</strong> Ensure that the <code>&lt;StreamVideoProvider&gt;</code> component wraps the instructor's dashboard where the video call is being created. You can achieve this by adding a <code>layout.tsx</code> file to the dashboard page and wrapping all child elements with <code>&lt;StreamVideoProvider&gt;</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740658849436/109c53d7-c818-4d79-8bd7-19c3c0ae62a2.gif" alt="Dashboard interface titled &quot;LinkedUp&quot; with sections for followers, announcements, and options to make an announcement, schedule a call, or access the community channel. Logout button is visible at the top right." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-joining-stream-video-calls">Joining Stream Video Calls</h3>
<p>The <code>instructor/[id]</code> page displays detailed information about a specific instructor from Supabase and lists of their current and upcoming calls. This allows students to view scheduled meetings and join them when they start.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740659135003/0c53b3b6-fa91-4d28-81cf-89e8c90b7e02.gif" alt="Screenshot of a profile page for Carl John, a UI Designer, on a platform called LinkedUp. It includes a profile picture, a button for joining a community channel, announcements with delete options, and upcoming meetings with join and copy link options." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To implement this functionality, we will use the <code>MeetingsBox</code> component within the instructor's profile page and create a dedicated <code>calls/[id]</code> page route for joining calls.</p>
<p>First, create a <code>(stream)</code> folder and add a <code>calls/[id]</code> page route. Then, create a <code>layout.tsx</code> file within the <code>(stream)</code> folder and insert the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { StreamVideoProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"../providers/StreamVideoProvider"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
    title: <span class="hljs-string">"Calls &amp; Chat | LinkedUp"</span>,
    description: <span class="hljs-string">"Generated by create next app"</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthLayout</span>(<span class="hljs-params">{
    children,
}: Readonly&lt;{
    children: React.ReactNode;
}&gt;</span>) </span>{
    <span class="hljs-keyword">return</span> &lt;StreamVideoProvider&gt;{children}&lt;/StreamVideoProvider&gt;;
}
</code></pre>
<p>The <code>layout.tsx</code> file ensures that the <code>StreamVideoProvider</code> component wraps all pages inside the <code>(stream)</code> folder, enabling access to Stream's video and audio features across these pages.</p>
<p>Next, render the calls within the <a target="_blank" href="https://github.com/dha-stix/stream-lms/blob/main/src/app/instructor/%5Bid%5D/\(components\)/MeetingsBox.tsx">MeetingsBox component</a> and and let students join meetings.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { formatDateTime } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/utils"</span>;
<span class="hljs-keyword">import</span> { Call } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MeetingsBox</span>(<span class="hljs-params">{
    upcomingCalls,
    isLoading,
    ongoingCalls,
}: {
    upcomingCalls: Call[] | <span class="hljs-literal">undefined</span>;
    isLoading: <span class="hljs-built_in">boolean</span>;
    ongoingCalls: Call[] | <span class="hljs-literal">undefined</span>;
}</span>) </span>{
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">if</span> (isLoading || !upcomingCalls || !ongoingCalls) {
        <span class="hljs-keyword">return</span> &lt;p className=<span class="hljs-string">'text-xs opacity-60'</span>&gt;Fetching calls...&lt;/p&gt;;
    }

    <span class="hljs-keyword">if</span> (upcomingCalls.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> &lt;p className=<span class="hljs-string">'text-xs  opacity-60'</span>&gt;No upcoming meetings&lt;/p&gt;;
    }

    <span class="hljs-keyword">return</span> {
        <span class="hljs-comment">// --- upcoming and ongoing calls display elements ---</span>
    };
}
</code></pre>
<p>Return the following UI elements from the component to allow everyone to see the instructor's current and upcoming meetings.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">'space-y-4'</span>&gt;
        <span class="hljs-comment">// --- ongoing calls ---</span>
        {ongoingCalls.map(<span class="hljs-function">(<span class="hljs-params">call</span>) =&gt;</span> (
            &lt;div className=<span class="hljs-string">'bg-white p-2 rounded-md'</span> key={call.id}&gt;
                &lt;h3 className=<span class="hljs-string">'text-sm font-bold text-gray-500 mb-2'</span>&gt;
                    {call.state.custom.description}
                &lt;/h3&gt;
                &lt;p className=<span class="hljs-string">'text-xs'</span>&gt;
                    Started: {formatDateTime(call.state?.startsAt?.toLocaleString())}
                &lt;/p&gt;
                &lt;div className=<span class="hljs-string">'flex items-center space-x-4'</span>&gt;
                    &lt;button
                        className=<span class="hljs-string">'bg-blue-500 text-white px-4 py-2 text-xs rounded-md mt-2'</span>
                        onClick={<span class="hljs-function">() =&gt;</span> handleJoinCall(call)}
                    &gt;
                        Join In
                    &lt;/button&gt;

                    &lt;button
                        className=<span class="hljs-string">'bg-gray-500 text-white px-4 py-2 text-xs rounded-md mt-2'</span>
                        onClick={<span class="hljs-function">() =&gt;</span> handleCopyLink(call)}
                    &gt;
                        Copy Link
                    &lt;/button&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        ))}
        <span class="hljs-comment">// --- upcoming calls ---</span>
        {upcomingCalls.map(<span class="hljs-function">(<span class="hljs-params">call</span>) =&gt;</span> (
            &lt;div className=<span class="hljs-string">'bg-white p-2 rounded-md'</span> key={call.id}&gt;
                &lt;h3 className=<span class="hljs-string">'text-sm font-bold text-gray-500 mb-2'</span>&gt;
                    {call.state.custom.description}
                &lt;/h3&gt;

                &lt;div className=<span class="hljs-string">'flex items-center space-x-4'</span>&gt;
                    &lt;button
                        className=<span class="hljs-string">'bg-blue-500 text-white px-4 py-2 text-xs rounded-md mt-2'</span>
                        disabled={<span class="hljs-literal">true</span>}
                    &gt;
                        {formatDateTime(call.state?.startsAt?.toLocaleString())}
                    &lt;/button&gt;

                    &lt;button
                        className=<span class="hljs-string">'bg-gray-500 text-white px-4 py-2 text-xs rounded-md mt-2'</span>
                        onClick={<span class="hljs-function">() =&gt;</span> handleCopyLink(call)}
                    &gt;
                        Copy Link
                    &lt;/button&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        ))}
    &lt;/div&gt;
);
</code></pre>
<p>The <code>MeetingsBox</code> component renders the instructor's current and upcoming calls, allowing users to copy the call link and join meetings.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740659373366/9150c744-7bb0-4e1f-99f5-b6b89d2bc488.png" alt="Profile page for Carl John, a UI Designer, featuring announcements and upcoming meetings. Links to &quot;Join my Community Channel&quot; and meeting details are shown." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Execute the <code>handleJoinCall</code> function to redirect the user to the call page. This allows them to confirm the action before joining the call. The <code>handleCopyLink</code> function copies the call link to the clipboard.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleJoinCall = <span class="hljs-function">(<span class="hljs-params">call: Call</span>) =&gt;</span> {
    router.push(<span class="hljs-string">`/call/<span class="hljs-subst">${call.id}</span>`</span>);
};

<span class="hljs-keyword">const</span> handleCopyLink = <span class="hljs-function">(<span class="hljs-params">call: Call</span>) =&gt;</span> {
    navigator.clipboard.writeText(
        <span class="hljs-string">`<span class="hljs-subst">${process.env.NEXT_PUBLIC_PAGE_URL!}</span>/call/<span class="hljs-subst">${call.id}</span>`</span>
    );
    <span class="hljs-built_in">console</span>.log({
        title: <span class="hljs-string">"Link copied to clipboard"</span>,
        description: <span class="hljs-string">"You can now share the link with interested participants"</span>,
    });
};
</code></pre>
<p>Now, create the <code>call/[id]/page.tsx</code> component and copy the following code into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> { useEffect, useState, useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../../../utils/supabase/client"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CallPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { id } = useParams&lt;{ id: <span class="hljs-built_in">string</span> }&gt;();
    <span class="hljs-keyword">const</span> [user, setUser] = useState&lt;User | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">const</span> authenticateUser = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">const</span> supabase = createClient();
        <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
        <span class="hljs-keyword">const</span> userData = data.user;
        <span class="hljs-keyword">if</span> (!userData) {
            <span class="hljs-keyword">return</span> router.push(<span class="hljs-string">"/student/auth/login"</span>);
        }
        setUser(userData);
    }, [router, call, camMicEnabled]);

    useEffect(<span class="hljs-function">() =&gt;</span> {
        authenticateUser();
    }, [authenticateUser]);

    <span class="hljs-keyword">return</span> {
        <span class="hljs-comment">// -- Conditionally render Stream Call component --</span>
    };
}
</code></pre>
<p>The code snippet authenticates the user to ensure they are signed in.</p>
<p>Next, fetch the call details using the call ID from the page route via the <code>useParams</code> hook.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-comment">//..other imports</span>
<span class="hljs-keyword">import</span> { useGetCallById } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/hooks/useGetCallById"</span>;
<span class="hljs-keyword">import</span> { StreamCall, StreamTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CallPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">//..other states</span>
    <span class="hljs-keyword">const</span> { call, isCallLoading } = useGetCallById(id);
    <span class="hljs-keyword">const</span> [confirmJoin, setConfirmJoin] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [camMicEnabled, setCamMicEnabled] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);

    <span class="hljs-keyword">const</span> handleJoin = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">//👇🏻 Stream join call function</span>
        call?.join();
        setConfirmJoin(<span class="hljs-literal">true</span>);
    };

    <span class="hljs-keyword">if</span> (isCallLoading) <span class="hljs-keyword">return</span> &lt;p&gt;Loading...&lt;/p&gt;;

    <span class="hljs-keyword">if</span> (!call) <span class="hljs-keyword">return</span> &lt;p&gt;Call not found&lt;/p&gt;;

    <span class="hljs-keyword">return</span> (
        &lt;main className=<span class="hljs-string">'min-h-screen w-full items-center justify-center'</span>&gt;
            &lt;StreamCall call={call}&gt;
                &lt;StreamTheme&gt;
                    {confirmJoin ? (
                        &lt;MeetingRoom call={call} /&gt;
                    ) : (
                        &lt;div className=<span class="hljs-string">'flex flex-col items-center justify-center gap-5 h-screen w-full'</span>&gt;
                            &lt;h1 className=<span class="hljs-string">'text-3xl font-bold'</span>&gt;Join Call&lt;/h1&gt;
                            &lt;p className=<span class="hljs-string">'text-lg'</span>&gt;
                                Are you sure you want to join <span class="hljs-built_in">this</span> call?
                            &lt;/p&gt;
                            &lt;div className=<span class="hljs-string">'flex gap-5'</span>&gt;
                                &lt;button
                                    onClick={handleJoin}
                                    className=<span class="hljs-string">'px-4 py-3 bg-blue-600 text-blue-50'</span>
                                &gt;
                                    Join
                                &lt;/button&gt;
                                &lt;button
                                    onClick={<span class="hljs-function">() =&gt;</span> router.back()}
                                    className=<span class="hljs-string">'px-4 py-3 bg-red-600 text-red-50'</span>
                                &gt;
                                    Cancel
                                &lt;/button&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    )}
                &lt;/StreamTheme&gt;
            &lt;/StreamCall&gt;
        &lt;/main&gt;
    );
}
</code></pre>
<p>In the code snippet above,</p>
<ul>
<li><p>The <a target="_blank" href="https://getstream.io/video/docs/react/ui-components/core/stream-call/"><strong>StreamCall</strong> component</a> wraps the entire call page, allowing access to various audio and video calling features. It accepts the <strong>call object</strong> as a prop.</p>
</li>
<li><p>The <a target="_blank" href="https://getstream.io/video/docs/react/ui-components/video-theme/"><strong>StreamTheme</strong> component</a> provides UI styling for the call, enabling you to use different themes.</p>
</li>
<li><p>The <code>confirmJoin</code> state is initially set to <code>false</code>. When the user clicks the <strong>Join</strong> button, it triggers the <code>handleJoin</code> function, which joins the call and updates <code>confirmJoin</code> to <code>true</code>.</p>
</li>
<li><p>When <code>confirmJoin</code> is <code>true</code>, the component renders the <code>MeetingRoom</code> component, which includes all prebuilt and customizable UI elements for the call provided by Stream.</p>
</li>
</ul>
<p>Finally, update the <code>authenticateUser</code> function to prompt the Stream user to enable or disable the camera and microphone immediately after joining a call.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 call &amp; camera disable/enable state</span>
<span class="hljs-keyword">const</span> [camMicEnabled, setCamMicEnabled] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> authenticateUser = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> supabase = createClient();
    <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
    <span class="hljs-keyword">const</span> userData = data.user;
    <span class="hljs-keyword">if</span> (!userData) {
        <span class="hljs-keyword">return</span> router.push(<span class="hljs-string">"/student/auth/login"</span>);
    }
    setUser(userData);
    <span class="hljs-comment">//👇🏻 Enable camera and microphone</span>
    <span class="hljs-keyword">if</span> (camMicEnabled) {
        call?.camera.enable();
        call?.microphone.enable();
    } <span class="hljs-keyword">else</span> {
        call?.camera.disable();
        call?.microphone.disable();
    }
}, [router, call, camMicEnabled]);

useEffect(<span class="hljs-function">() =&gt;</span> {
    authenticateUser();
}, [authenticateUser]);
</code></pre>
<h3 id="heading-stream-call-ui-components">Stream Call UI Components</h3>
<p>Stream makes setting up a call page easy using minimal UI components. It provides two prebuilt <a target="_blank" href="https://getstream.io/video/docs/react/ui-components/core/call-layout/">call layouts</a> (<strong>PaginatedGridLayout</strong> and <strong>SpeakerLayout)</strong> and a customizable <a target="_blank" href="https://getstream.io/video/docs/react/ui-cookbook/replacing-call-controls/"><strong>CallControls</strong> component.</a></p>
<ul>
<li><p>PaginatedGridLayout and SpeakerLayout define how call participants are displayed on the call page.</p>
</li>
<li><p>CallControls provides essential call functionalities such as toggling video and audio, sharing the screen, leaving the call, and more.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740659807590/b3e24561-60fc-4e0d-8054-a02d78a745a2.gif" alt="Video call interface with a person in a small window. The microphone is muted, indicated by a crossed-out microphone icon. Other controls and a red &quot;End Call for Everyone&quot; button are visible at the bottom." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Create the <strong>MeetingRoom</strong> component as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> MeetingRoom = <span class="hljs-function">(<span class="hljs-params">{call} : {call: Call}</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> [layout, setLayout] = useState&lt;CallLayoutType&gt;(<span class="hljs-string">"grid"</span>);
    <span class="hljs-keyword">const</span> router = useRouter();

<span class="hljs-comment">//👇🏻 allows members to leave the call</span>
    <span class="hljs-keyword">const</span> handleLeave = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (confirm(<span class="hljs-string">"Are you sure you want to leave the call?"</span>)) {
            router.push(<span class="hljs-string">"/"</span>);
        }
    };

<span class="hljs-comment">//👇🏻 describes the call layout</span>
    <span class="hljs-keyword">const</span> CallLayout = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">switch</span> (layout) {
            <span class="hljs-keyword">case</span> <span class="hljs-string">"grid"</span>:
                <span class="hljs-keyword">return</span> &lt;PaginatedGridLayout /&gt;;
            <span class="hljs-keyword">case</span> <span class="hljs-string">"speaker-right"</span>:
                <span class="hljs-keyword">return</span> &lt;SpeakerLayout participantsBarPosition=<span class="hljs-string">'left'</span> /&gt;;
            <span class="hljs-keyword">default</span>:
                <span class="hljs-keyword">return</span> &lt;SpeakerLayout participantsBarPosition=<span class="hljs-string">'right'</span> /&gt;;
        }
    };

  <span class="hljs-keyword">return</span> (
    <span class="hljs-comment">//  -- Stream call UI component--</span>
  )
}
</code></pre>
<p>The <code>handleLeave</code> function enables call participants to leave the call and the <code>CallLayout</code> component determines how they are laid out on the screen.</p>
<p>Return the following from the <code>MeetingRoom</code> component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> (
    &lt;section className=<span class="hljs-string">'relative min-h-screen w-full overflow-hidden pt-4'</span>&gt;
        &lt;div className=<span class="hljs-string">'relative flex size-full items-center justify-center'</span>&gt;
            &lt;div className=<span class="hljs-string">'flex size-full max-w-[1000px] items-center'</span>&gt;
                &lt;CallLayout /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">'fixed bottom-0 flex w-full items-center justify-center gap-5'</span>&gt;
                &lt;CallControls onLeave={handleLeave} /&gt;
            &lt;/div&gt;

            &lt;div className=<span class="hljs-string">'fixed bottom-0 right-0 flex items-center justify-center gap-5 p-5'</span>&gt;
                &lt;EndCallButton call={call} /&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/section&gt;
);
</code></pre>
<p>The CallLayout and CallControls components are rendered on the page, allowing users to communicate, share their screen, turn their camera on or off, and engage in conversations through reactions.</p>
<p>Finally, create the <strong>EndCallButton</strong> component to enable the host (instructor) to end the call for everyone.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 Stream call hook</span>
<span class="hljs-keyword">import</span> { useCallStateHooks } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stream-io/video-react-sdk"</span>;

<span class="hljs-keyword">const</span> EndCallButton = <span class="hljs-function">(<span class="hljs-params">{ call }: { call: Call }</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { useLocalParticipant } = useCallStateHooks();
    <span class="hljs-keyword">const</span> localParticipant = useLocalParticipant();
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">const</span> participantIsHost =
        localParticipant &amp;&amp;
        call.state.createdBy &amp;&amp;
        localParticipant.userId === call.state.createdBy.id;

    <span class="hljs-keyword">if</span> (!participantIsHost) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">const</span> handleEndCall = <span class="hljs-function">() =&gt;</span> {
        call.endCall();
        <span class="hljs-built_in">console</span>.log({
            title: <span class="hljs-string">"Call Ended"</span>,
            description: <span class="hljs-string">"The call has been ended for everyone"</span>,
        });
        router.push(<span class="hljs-string">"/"</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;button
            className=<span class="hljs-string">'bg-red-500 text-white px-4 py-2 rounded-md mt-2'</span>
            onClick={handleEndCall}
        &gt;
            End Call <span class="hljs-keyword">for</span> Everyone
        &lt;/button&gt;
    );
};
</code></pre>
<p>The code snippet above ensures that only the call host can end the call for all participants. It first checks if the current user is the host before displaying the <a target="_blank" href="https://getstream.io/video/docs/api/calls/#ending-calls">"End Call for Everyone" button</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740660039466/1851aba0-6961-4b54-87cf-54a3a98bb61a.gif" alt="Profile page of a UI Designer named Carl John. The page includes an announcement section with posts, an upcoming meetings section with join and copy link options, and a log out button. A button to join a community channel is also present." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-integrate-a-group-chat-feature-using-stream-chat-messaging">How to Integrate a Group Chat Feature Using Stream Chat Messaging</h2>
<p>In this section, you will learn how to integrate a community chat feature into the application. Each instructor will create a group chat for their followers (students). The chat will allow students to interact with one another and share documents, video links, text, images, and so on using the <a target="_blank" href="https://getstream.io/chat/docs/sdk/react/">Stream Chat Messaging SDK</a>.</p>
<h3 id="heading-setting-up-the-stream-chat-sdk-in-nextjs">Setting Up the Stream Chat SDK in Next.js</h3>
<p>Add the following code snippet to the <code>stream.action.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { StreamChat } <span class="hljs-keyword">from</span> <span class="hljs-string">"stream-chat"</span>;
<span class="hljs-keyword">import</span> { getUserSession } <span class="hljs-keyword">from</span> <span class="hljs-string">"./auth"</span>;

<span class="hljs-comment">//👇🏻 creates a StreamChat instance</span>
<span class="hljs-keyword">const</span> serverClient = StreamChat.getInstance(STREAM_API_KEY, STREAM_API_SECRET);

<span class="hljs-comment">//👇🏻 creates a token</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createToken</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">string</span>&gt; </span>{
    <span class="hljs-keyword">const</span> { user } = <span class="hljs-keyword">await</span> getUserSession();
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"User is not authenticated"</span>);
    <span class="hljs-keyword">return</span> serverClient.createToken(user.id);
}
</code></pre>
<p>The code snippet above initializes a Stream Chat instance using its API key and secret key. It also includes a function that generates and returns a token based on the current user's ID.</p>
<p>To ensure that only instructors can create a community channel, follow these steps:</p>
<ol>
<li><p>Retrieve all the channels where the instructor is a member.</p>
</li>
<li><p>If no channels are found (i.e., the returned array is empty), the instructor can create a new channel.</p>
</li>
<li><p>An error message is displayed if a channel already exists, informing the instructor that they can only have one community channel.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createChannel</span>(<span class="hljs-params">{
     userId,
     data,
 }: {
     userId: <span class="hljs-built_in">string</span>;
     data: { name: <span class="hljs-built_in">string</span>; imageUrl: <span class="hljs-built_in">string</span> };
 }</span>) </span>{
     <span class="hljs-keyword">try</span> {
         <span class="hljs-comment">//👇🏻 retrieve channel list</span>
         <span class="hljs-keyword">const</span> channels = <span class="hljs-keyword">await</span> serverClient.queryChannels(
             {
                 members: { $in: [userId] },
                 <span class="hljs-keyword">type</span>: <span class="hljs-string">"messaging"</span>,
             },
             { last_message_at: <span class="hljs-number">-1</span> }
         );
         <span class="hljs-comment">//👇🏻 instructor already has a channel</span>
         <span class="hljs-keyword">if</span> (channels.length &gt; <span class="hljs-number">0</span>) {
             <span class="hljs-keyword">return</span> {
                 success: <span class="hljs-literal">false</span>,
                 error: <span class="hljs-string">"You already have an existing channel"</span>,
                 id: channels[<span class="hljs-number">0</span>].id,
             };
         }
         <span class="hljs-comment">//👇🏻 declare channel type</span>
         <span class="hljs-keyword">const</span> channel = serverClient.channel(<span class="hljs-string">"messaging"</span>, <span class="hljs-string">`channel-<span class="hljs-subst">${userId}</span>`</span>, {
             name: data.name,
             image: data.imageUrl,
             members: [userId],
             created_by_id: userId,
         });
         <span class="hljs-comment">//👇🏻 create a channel</span>
         <span class="hljs-keyword">await</span> channel.create();
         <span class="hljs-keyword">return</span> { success: <span class="hljs-literal">true</span>, error: <span class="hljs-literal">null</span>, id: channel.id };
     } <span class="hljs-keyword">catch</span> (err) {
         <span class="hljs-keyword">return</span> { success: <span class="hljs-literal">false</span>, error: <span class="hljs-string">"Failed to create channel"</span>, id: <span class="hljs-literal">null</span> };
     }
 }
</code></pre>
</li>
</ol>
<p>The code snippet above <a target="_blank" href="https://getstream.io/chat/docs/react/creating_channels/">creates a public channel</a>, meaning anyone can join at any time. Also, the channel name is linked to the instructor's ID, ensuring it remains unique to that instructor.</p>
<p>To retrieve the instructor's channel link, add a function inside the <code>stream.action.ts</code> file. This function should return the channel URL (channel ID), allowing members to access the channel whenever needed. Then, you can display this link on the instructor's profile for easy access.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getInstructorChannel</span>(<span class="hljs-params">userId: <span class="hljs-built_in">string</span></span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> channels = <span class="hljs-keyword">await</span> serverClient.queryChannels(
            {
                members: { $in: [userId] },
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"messaging"</span>,
            },
            { last_message_at: <span class="hljs-number">-1</span> }
        );
        <span class="hljs-keyword">return</span> <span class="hljs-string">`/chat/<span class="hljs-subst">${channels[<span class="hljs-number">0</span>].id}</span>`</span>;
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
}
</code></pre>
<p>Finally, to grant users access to the channel page, check if the user is already a member. If not, add the student as a member before rendering the chat page. This ensures that only authorized users can participate in the conversation.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addUserToChannel</span>(<span class="hljs-params">channelId: <span class="hljs-built_in">string</span>, userId: <span class="hljs-built_in">string</span></span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">//👇🏻 check if student is already a member</span>
        <span class="hljs-keyword">const</span> channels = <span class="hljs-keyword">await</span> serverClient.queryChannels(
            {
                members: { $in: [userId] },
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"messaging"</span>,
                id: channelId,
            },
            { last_message_at: <span class="hljs-number">-1</span> }
        );
        <span class="hljs-comment">//👇🏻 student already a member (success - show chat page)</span>
        <span class="hljs-keyword">if</span> (channels.length &gt; <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">true</span>,
                message: <span class="hljs-string">"Already a member"</span>,
                id: channels[<span class="hljs-number">0</span>].id,
                error: <span class="hljs-literal">null</span>,
            };
        }
        <span class="hljs-comment">//👇🏻 get channel by ID (student not a member)</span>
        <span class="hljs-keyword">const</span> channel = serverClient.channel(<span class="hljs-string">"messaging"</span>, channelId);
        <span class="hljs-comment">//👇🏻 add student to channel as a member</span>
        <span class="hljs-keyword">await</span> channel.addMembers([userId]);
        <span class="hljs-comment">//👇🏻 student now a member (success - show chat page)</span>
        <span class="hljs-keyword">return</span> {
            success: <span class="hljs-literal">true</span>,
            error: <span class="hljs-literal">null</span>,
            id: channel.id,
            message: <span class="hljs-string">"Member just added"</span>,
        };
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error adding user to channel:"</span>, error);
        <span class="hljs-keyword">return</span> {
            success: <span class="hljs-literal">false</span>,
            error: <span class="hljs-string">"Failed to add user to channel"</span>,
            id: <span class="hljs-literal">null</span>,
            message: <span class="hljs-literal">null</span>,
        };
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740660564180/474b342a-7d07-415e-b1d7-a213ed807b67.gif" alt="The Stream Chat page using the Stream Call UI components" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-stream-chat-ui-components">Stream Chat UI Components</h3>
<p>Inside the <code>(stream)</code> folder, create a <code>chat/[id]/page.tsx</code> file. This page retrieves the channel ID from the page route and checks whether the user is already a channel member. If not, the user is automatically added as a member before displaying the chat interface.</p>
<p>Copy the following code snippet into the <code>chat/[id]/page.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useCallback, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> StreamChat <span class="hljs-keyword">from</span> <span class="hljs-string">"./../(components)/StreamChat"</span>;
<span class="hljs-keyword">import</span> { useParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [userData, setUserData] = useState&lt;UserData | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> [joinChannel, setJoinChannel] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> params = useParams&lt;{ id: <span class="hljs-built_in">string</span> }&gt;();
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">const</span> fetchUserData = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-comment">// 👉🏻 get user object &amp; channel id from useParams</span>
        <span class="hljs-comment">// 👉🏻 execute the addUserToChannel() function declared in the previous section</span>
        <span class="hljs-comment">// 👉🏻 update the joinChannel React state</span>
    }, [params.id, router]);

    useEffect(<span class="hljs-function">() =&gt;</span> {
        fetchUserData();
    }, [fetchUserData]);

    <span class="hljs-keyword">if</span> (!userData) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">return</span> (
        &lt;&gt;{joinChannel ? &lt;StreamChat user={userData} /&gt; : &lt;ConfirmMember /&gt;}&lt;/&gt;
    );
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ConfirmMember</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'flex flex-col items-center justify-center h-screen'</span>&gt;
            &lt;h1 className=<span class="hljs-string">'text-2xl font-bold mb-4 text-blue-500'</span>&gt;
                You are not a member <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span> channel
            &lt;/h1&gt;
            &lt;p className=<span class="hljs-string">'text-lg mb-4'</span>&gt;
                Please wait <span class="hljs-keyword">while</span> we add you to the channel
            &lt;/p&gt;

            &lt;div className=<span class="hljs-string">'loader'</span>&gt;
                &lt;Loader2 size={<span class="hljs-number">48</span>} className=<span class="hljs-string">'animate-spin'</span> /&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>This code snippet ensures that a user is either already a member of the channel or is added before displaying the chat interface. The <strong>StreamChat</strong> component is a custom React component that contains all the Stream Chat UI elements. The <strong>ConfirmMember</strong> component shows a loading message while the user is added to the channel.</p>
<p>Create a StreamChat component and add the following imports to the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-comment">//👇🏻 -- Stream chat UI components</span>
<span class="hljs-keyword">import</span> {
    Chat,
    Channel,
    ChannelList,
    Window,
    ChannelHeader,
    MessageList,
    MessageInput,
    useCreateChatClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"stream-chat-react"</span>;
<span class="hljs-comment">// -- end of Stream chat UI components</span>

<span class="hljs-comment">//👇🏻 -- allows members to send emoji within the chat</span>
<span class="hljs-keyword">import</span> { EmojiPicker } <span class="hljs-keyword">from</span> <span class="hljs-string">"stream-chat-react/emojis"</span>;
<span class="hljs-keyword">import</span> { init, SearchIndex } <span class="hljs-keyword">from</span> <span class="hljs-string">"emoji-mart"</span>;
<span class="hljs-keyword">import</span> data <span class="hljs-keyword">from</span> <span class="hljs-string">"@emoji-mart/data"</span>;
init({ data });
<span class="hljs-comment">// -- end of emoji imports</span>
<span class="hljs-comment">//👇🏻 -- create token server action</span>
<span class="hljs-keyword">import</span> { createToken } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../../../actions/stream.action"</span>;
</code></pre>
<p>Declare the StreamChat component as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">StreamChat</span>(<span class="hljs-params">{ user }: { user: UserData }</span>) </span>{

    <span class="hljs-keyword">const</span> tokenProvider = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> createToken();
    }, []);

    <span class="hljs-keyword">const</span> filters = { members: { $in: [user.id] }, <span class="hljs-keyword">type</span>: <span class="hljs-string">"messaging"</span> };
    <span class="hljs-keyword">const</span> options = { presence: <span class="hljs-literal">true</span>, state: <span class="hljs-literal">true</span> };

    <span class="hljs-keyword">const</span> client = useCreateChatClient({
        apiKey: process.env.NEXT_PUBLIC_STREAM_API_KEY!,
        tokenOrProvider: tokenProvider,
        userData: { id: user.id, name: user.name, image: user.image },
    });

    <span class="hljs-keyword">if</span> (!client) <span class="hljs-keyword">return</span> &lt;div&gt;Loading...&lt;/div&gt;;

  <span class="hljs-keyword">return</span> (
    <span class="hljs-comment">// -- Stream Chat UI components --</span>
  )
</code></pre>
<p>The <strong>useCreateChatClient</strong> hook creates a Stream chat client using the Stream API key, the user's data, and the token created using the <code>createToken()</code> function declared earlier in this section.</p>
<p>Finally, return the chat UI from the StreamChat component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> (
    &lt;Chat client={client}&gt;
        &lt;div className=<span class="hljs-string">'chat-container'</span>&gt;
            {<span class="hljs-comment">/* -- Channel List -- */</span>}
            &lt;div className=<span class="hljs-string">'channel-list'</span>&gt;
                &lt;ChannelList
                    sort={{ last_message_at: <span class="hljs-number">-1</span> }}
                    filters={filters}
                    options={options}
                /&gt;
            &lt;/div&gt;

            {<span class="hljs-comment">/* -- Messages Panel -- */</span>}
            &lt;div className=<span class="hljs-string">'chat-panel'</span>&gt;
                &lt;Channel EmojiPicker={EmojiPicker} emojiSearchIndex={SearchIndex}&gt;
                    &lt;Window&gt;
                        &lt;ChannelHeader /&gt;
                        &lt;MessageList /&gt;
                        &lt;MessageInput /&gt;
                    &lt;/Window&gt;
                &lt;/Channel&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/Chat&gt;
);
</code></pre>
<ul>
<li><p>From the code snippet above:</p>
<ul>
<li><p><a target="_blank" href="https://getstream.io/chat/docs/sdk/react/components/core-components/chat/"><strong>Chat</strong> component</a> initializes the Stream Chat client and wraps the entire Chat page.</p>
</li>
<li><p><a target="_blank" href="https://getstream.io/chat/docs/sdk/react/components/core-components/channel_list/"><strong>ChannelList</strong></a> shows available chat channels.</p>
</li>
<li><p><a target="_blank" href="https://getstream.io/chat/docs/sdk/react/components/core-components/channel/"><strong>Channel</strong></a> sets up an active chat session.</p>
</li>
<li><p><strong>Window</strong> contains the message display and input areas.</p>
</li>
<li><p><strong>ChannelHeader</strong>, <a target="_blank" href="https://getstream.io/chat/docs/sdk/react/components/core-components/message_list/"><strong>MessageList</strong></a>, and <strong>MessageInput</strong> provide a fully functional chat interface.</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740660921805/65b237a8-584a-4d71-88ed-fe7edc68b737.png" alt="A screenshot of a group chat named &quot;UI Design Students&quot; with 2 members, showing a brief conversation with messages &quot;Hello&quot; and &quot;Hi,&quot; timestamped at 11:33 AM." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Congratulations! You've completed this tutorial. The <a target="_blank" href="https://github.com/dha-stix/stream-lms">source code for this article is also available on GitHub</a>.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>So far, you've learned how to build a full-stack social learning platform using Stream and Supabase. This platform enables users to interact with one another through real-time chat powered by Stream.</p>
<p>Stream helps you build engaging apps that scale to millions with performant and flexible Chat, Video, Voice, Feeds, and Moderation APIs and SDKs powered by a global edge network and enterprise-grade infrastructure.</p>
<p>Here are some useful resources to help you get started:</p>
<ul>
<li><p><a target="_blank" href="https://getstream.io/chat/docs/">Stream Chat Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://getstream.io/video/">Stream Video and Audio Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://getstream.io/activity-feeds/">Stream Activity Feeds</a></p>
</li>
</ul>
<p>Thank you for reading! 🎉</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create and Send Email Templates Using React Email and Resend in Next.js ]]>
                </title>
                <description>
                    <![CDATA[ Modern software applications often rely on email communication to engage with users. They may send authentication codes during sign in attempts, marketing emails, or newsletters, for example. This means that email notifications are typically the most... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-and-send-email-templates-using-react-email-and-resend-in-nextjs/</link>
                <guid isPermaLink="false">675b1981cc99415a548dbe6c</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Thu, 12 Dec 2024 17:12:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733995060570/3450a9b3-e740-4362-b0ab-0269646e725c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Modern software applications often rely on email communication to engage with users. They may send authentication codes during sign in attempts, marketing emails, or newsletters, for example. This means that email notifications are typically the most common means of communication with users.</p>
<p>In this tutorial, you'll learn how to design stunning email templates with <a target="_blank" href="https://react.email/docs/introduction">React Email</a> and send them using <a target="_blank" href="https://resend.com/docs/send-with-nextjs">Resend</a> – a simple and powerful email API platform.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this tutorial, you should have a basic understanding of React or Next.js.</p>
<p>We'll also make use of the following tools:</p>
<ul>
<li><p>React Email: A library that lets you create beautifully designed email templates using React components.</p>
</li>
<li><p>Resend: A simple and powerful API platform for sending emails from your applications.</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-application-with-nextjs">How to Build the Application with Next.js</h2>
<p>In this section, you'll create a simple customer support application. The app will include a form for users to submit their queries, which triggers an email notification confirming that a support ticket has been created.</p>
<p>To get started, we'll first set up the user interface and an API endpoint.</p>
<p>Run the following command to create a new Next.js TypeScript project:</p>
<pre><code class="lang-bash">npx create-next-app react-email-resend
</code></pre>
<p>Update the <code>app/page.tsx</code> file to render a form that collects the customer's details, including their full name, email address, the subject of the ticket, and a detailed message describing the issue. When the form is submitted, the input data is logged to the console using the <code>handleSubmit</code> function.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> support <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/images/support.jpg"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">//👇🏻 input states</span>
    <span class="hljs-keyword">const</span> [name, setName] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [subject, setSubject] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [content, setContent] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent) =&gt; {
        e.preventDefault();
        <span class="hljs-comment">//👇🏻 log the user's input</span>
        <span class="hljs-built_in">console</span>.log({ name, email, subject, content });
    };
<span class="hljs-keyword">return</span> ({<span class="hljs-comment">/** -- UI elements -- */</span>})
}
</code></pre>
<p>Return the form UI elements that accept the user’s full name, email address, ticket subject, and a detailed message describing the issue.</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">return</span> (
        &lt;main className=<span class="hljs-string">'w-full min-h-screen flex items-center justify-between'</span>&gt;
                &lt;form className=<span class="hljs-string">'w-full'</span> onSubmit={handleSubmit}&gt;
                    &lt;label htmlFor=<span class="hljs-string">'name'</span> className=<span class="hljs-string">'opacity-60'</span>&gt;
                        Full Name
                    &lt;/label&gt;
                    &lt;input
                        <span class="hljs-keyword">type</span>=<span class="hljs-string">'text'</span>
                        className=<span class="hljs-string">'w-full px-4 py-3 border-[1px] mb-3 border-gray-300 rounded-sm'</span>
                        id=<span class="hljs-string">'name'</span>
                        required
                        value={name}
                        onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setName(e.target.value)}
                    /&gt;

                    &lt;label htmlFor=<span class="hljs-string">'email'</span> className=<span class="hljs-string">'opacity-60'</span>&gt;
                        Email Address
                    &lt;/label&gt;
                    &lt;input
                        <span class="hljs-keyword">type</span>=<span class="hljs-string">'email'</span>
                        className=<span class="hljs-string">'w-full px-4 py-3 border-[1px] mb-3 border-gray-300 rounded-sm'</span>
                        id=<span class="hljs-string">'email'</span>
                        value={email}
                        onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setEmail(e.target.value)}
                        required
                    /&gt;

                    &lt;label htmlFor=<span class="hljs-string">'subject'</span> className=<span class="hljs-string">'opacity-60'</span>&gt;
                        Subject
                    &lt;/label&gt;
                    &lt;input
                        <span class="hljs-keyword">type</span>=<span class="hljs-string">'text'</span>
                        className=<span class="hljs-string">'w-full px-4 py-3 border-[1px] mb-3 border-gray-300 rounded-sm'</span>
                        id=<span class="hljs-string">'subject'</span>
                        value={subject}
                        onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setSubject(e.target.value)}
                        required
                    /&gt;

                    &lt;label htmlFor=<span class="hljs-string">'message'</span> className=<span class="hljs-string">'opacity-60'</span>&gt;
                        Message
                    &lt;/label&gt;
                    &lt;textarea
                        rows={<span class="hljs-number">7</span>}
                        className=<span class="hljs-string">'w-full px-4 py-3 border-[1px] mb-3 border-gray-300 rounded-sm'</span>
                        id=<span class="hljs-string">'message'</span>
                        required
                        value={content}
                        onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setContent(e.target.value)}
                    /&gt;

                    &lt;button className=<span class="hljs-string">'w-full bg-blue-500 py-4 px-3 rounded-md font-bold text-blue-50'</span>&gt;
                        SEND MESSAGE
                    &lt;/button&gt;
                &lt;/form&gt;
            &lt;/div&gt;
        &lt;/main&gt;
    );
</code></pre>
<p>Here is the resulting page from the component:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733748196715/703e7e5b-f868-45e6-b62f-64a2d6dd279e.png" alt="The Page component renders a form that accepts the user's input" class="image--center mx-auto" width="2560" height="1420" loading="lazy"></p>
<p>Next, create an API endpoint (<code>/api/route.ts</code>) that accepts the customer’s input.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app
mkdir api &amp;&amp; <span class="hljs-built_in">cd</span> api
touch route.ts
</code></pre>
<p>Copy the following code into the <code>api/route.ts</code> file. The API endpoint logs the customer's input to the console upon receiving it.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
    <span class="hljs-keyword">const</span> { name, email, subject, content } = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-comment">//👇🏻 log the contents</span>
    <span class="hljs-built_in">console</span>.log({ name, email, subject, content });
    <span class="hljs-keyword">return</span> NextResponse.json({
        message: <span class="hljs-string">"Email sent successfully"</span>,
        data,
 });
}
</code></pre>
<p>Update the <code>handleSubmit</code> function to send the customer's data to the API endpoint and return the JSON response:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api"</span>, {
            method: <span class="hljs-string">"POST"</span>,
            headers: {
                <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
            },
            body: <span class="hljs-built_in">JSON</span>.stringify({ name, email, subject, content }),
        });
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
        alert(data.message);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(error);
        alert(<span class="hljs-string">"An error occurred, please try again later"</span>);
    }
    setName(<span class="hljs-string">""</span>);
    setEmail(<span class="hljs-string">""</span>);
    setSubject(<span class="hljs-string">""</span>);
    setContent(<span class="hljs-string">""</span>);
};
</code></pre>
<p>Congratulations! You've set up the data collection and submission. In the upcoming sections, I'll walk you through creating and sending email templates with React Email and Resend.</p>
<h2 id="heading-how-to-create-email-templates-using-react-email">How to Create Email Templates using React Email</h2>
<p>React Email lets you build and send email components using React and TypeScript. It supports multiple email clients, including Gmail, Yahoo Mail, Outlook, and Apple Mail.</p>
<p>React Email also provides multiple <a target="_blank" href="https://react.email/components">UI components</a> that enables you to customize the email templates according to your preferred layout using React JSX/TSX components.</p>
<p>Install the React Email package and its components by running the code snippet below:</p>
<pre><code class="lang-bash">npm install react-email -D -E
npm install @react-email/components -E
</code></pre>
<p>Include this script in your <code>package.json</code> file. It directs React Email to where the email templates are located in your project.</p>
<pre><code class="lang-json">  <span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"email"</span>: <span class="hljs-string">"email dev --dir src/emails"</span>
  },
</code></pre>
<p>One of the features of React Email is the ability to preview your email template in your browser during development, allowing you to see how it will appear in the recipient's email.</p>
<p>So next, create an <code>emails</code> folder containing a <code>TicketCreated.tsx</code> file within the Next.js <code>src</code> folder and copy the following code snippet into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
    Body,
    Container,
    Head,
    Heading,
    Hr,
    Html,
    Link,
    Preview,
    Text,
    Tailwind,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@react-email/components"</span>;

<span class="hljs-keyword">interface</span> TicketCreatedProps {
    username: <span class="hljs-built_in">string</span>;
    ticketID: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">const</span> baseUrl = process.env.VERCEL_URL || <span class="hljs-string">"http://localhost:3000"</span>;
</code></pre>
<p>In the code snippet above, we imported the components necessary for building the email template.</p>
<p>Next, add the <code>TicketCreated</code> component to the file to render the email template using <a target="_blank" href="https://react.email/components">React Email components</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TicketCreated = <span class="hljs-function">(<span class="hljs-params">{ username, ticketID }: TicketCreatedProps</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        &lt;Html&gt;
            &lt;Head /&gt;
            &lt;Preview&gt;Support Ticket Confirmation Email 🎉&lt;/Preview&gt;
            &lt;Tailwind&gt;
                &lt;Body className=<span class="hljs-string">'bg-white my-auto mx-auto font-sans px-2'</span>&gt;
                    &lt;Container className=<span class="hljs-string">'border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]'</span>&gt;
                        &lt;Heading className=<span class="hljs-string">'text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0'</span>&gt;
                            Your Ticket has been created
                        &lt;/Heading&gt;
                        &lt;Text className=<span class="hljs-string">'text-black text-[14px] leading-[24px]'</span>&gt;
                            Hello {username},
                        &lt;/Text&gt;
                        &lt;Text className=<span class="hljs-string">'text-black text-[14px] leading-[24px]'</span>&gt;
                            &lt;strong&gt;Support Ticket&lt;/strong&gt; (
                            &lt;Link
                                href={<span class="hljs-string">`<span class="hljs-subst">${baseUrl}</span>/ticket/<span class="hljs-subst">${ticketID}</span>`</span>}
                                className=<span class="hljs-string">'text-blue-600 no-underline'</span>
                            &gt;
                                {<span class="hljs-string">`#<span class="hljs-subst">${ticketID}</span>`</span>}
                            &lt;/Link&gt;
                            ) has been created successfully.
                        &lt;/Text&gt;

                        &lt;Text className=<span class="hljs-string">'text-black text-[14px] leading-[24px]'</span>&gt;
                            The Support team will review your ticket and get back to you
                            shortly.
                        &lt;/Text&gt;

                        &lt;Hr className=<span class="hljs-string">'border border-solid border-[#eaeaea] my-[26px] mx-0 w-full'</span> /&gt;
                        &lt;Text className=<span class="hljs-string">'text-[#666666] text-[12px] leading-[24px]'</span>&gt;
                            This message was intended <span class="hljs-keyword">for</span>{<span class="hljs-string">" "</span>}
                            &lt;span className=<span class="hljs-string">'text-black'</span>&gt;{username}&lt;/span&gt;. If you did not
                            create <span class="hljs-built_in">this</span> ticket, please ignore <span class="hljs-built_in">this</span> email.
                        &lt;/Text&gt;
                    &lt;/Container&gt;
                &lt;/Body&gt;
            &lt;/Tailwind&gt;
        &lt;/Html&gt;
    );
};
</code></pre>
<p>Finally, export it and add a default values for the props:</p>
<pre><code class="lang-typescript">TicketCreated.PreviewProps = {
    username: <span class="hljs-string">"alanturing"</span>,
    ticketID: <span class="hljs-string">"9083475"</span>,
} <span class="hljs-keyword">as</span> TicketCreatedProps;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> TicketCreated;
</code></pre>
<p>Run <code>npm run email</code> in your terminal to preview the email template.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733752824765/6c5e1518-fc85-4d79-bd05-f4a2c2381976.png" alt="6c5e1518-fc85-4d79-bd05-f4a2c2381976" class="image--center mx-auto" width="2553" height="1276" loading="lazy"></p>
<p>This email template notifies customers that their support ticket has been created and that someone from the support team will reach out to them.</p>
<p>React Email offers a variety of pre-designed email templates, making it easy to craft beautifully styled emails for different purposes. You can check out the <a target="_blank" href="https://demo.react.email/preview/notifications/vercel-invite-user">available demo to see examples</a> of what’s possible.</p>
<h2 id="heading-how-to-send-emails-with-resend">How to Send Emails with Resend</h2>
<p>Resend is a simple email API that enables you to send emails within your software application. It supports a range of programming languages, including JavaScript (Next.js, Express, Node.js), Python, PHP, Go, and Rust, among others.</p>
<p>Resend and React Email can be easily integrated together since the co-founder of Resend, <a target="_blank" href="https://github.com/bukinoshita">Bu Kinoshita</a>, is also the creator of React Email.</p>
<p>Create an account on <a target="_blank" href="https://resend.com/docs/send-with-nextjs">Resend</a>. Once you're signed in, navigate to the API keys section on your dashboard and copy your API key into a <code>.env.local</code> file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733753483127/4a289abc-b7e5-4c81-b7ee-e8f084354fae.png" alt="4a289abc-b7e5-4c81-b7ee-e8f084354fae" class="image--center mx-auto" width="2560" height="1384" loading="lazy"></p>
<pre><code class="lang-bash">//👇🏻 .env.local file
RESEND_API_KEY=&lt;RESEND_API_KEY&gt;
</code></pre>
<p>Update the API endpoint to send an email using the React Email template, as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-comment">//👇🏻 ticket ID generator function</span>
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> generateID } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-comment">//👇🏻 imports the email template</span>
<span class="hljs-keyword">import</span> TicketCreated <span class="hljs-keyword">from</span> <span class="hljs-string">"@/emails/TicketCreated"</span>;
<span class="hljs-comment">//👇🏻 imports Resend</span>
<span class="hljs-keyword">import</span> { Resend } <span class="hljs-keyword">from</span> <span class="hljs-string">"resend"</span>;
<span class="hljs-keyword">const</span> resend = <span class="hljs-keyword">new</span> Resend(process.env.RESEND_API_KEY);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
    <span class="hljs-comment">//👇🏻 accepts customer's input from frontend</span>
    <span class="hljs-keyword">const</span> { name, email, subject, content } = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-comment">//👇🏻 logs them</span>
    <span class="hljs-built_in">console</span>.log({ name, email, subject, content });
    <span class="hljs-comment">//👇🏻 send an email using the email template</span>
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> resend.emails.send({
        <span class="hljs-keyword">from</span>: <span class="hljs-string">"Acme &lt;onboarding@resend.dev&gt;"</span>,
        to: [email],
        subject: <span class="hljs-string">"Ticket Confirmation Email 🎉"</span>,
        react: TicketCreated({ username: name, ticketID: generateID() }),
    });

    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">return</span> NextResponse.json(
            { message: <span class="hljs-string">"Error sending email"</span> },
            { status: <span class="hljs-number">500</span> }
        );
    }

    <span class="hljs-keyword">return</span> NextResponse.json({
        message: <span class="hljs-string">"Email sent successfully"</span>,
        data,
    });
}
</code></pre>
<p>Congratulations!🥳 You've completed this tutorial.</p>
<p>Here is a brief demo of the application:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/RsNAtwDjAEg" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>In this tutorial, you learned how to create email templates with React Email and send them using Resend. Both packages enable you to integrate email communication easily within your applications.</p>
<p>Whether it’s simple email notifications, newsletters, or marketing campaigns, React Email and Resend offers an efficient and customizable solution to meet your needs.</p>
<p>Some helpful resources includes:</p>
<ul>
<li><p><a target="_blank" href="https://react.email/components">React Email Pre-built Components</a></p>
</li>
<li><p><a target="_blank" href="https://react.email/docs/introduction">React Email Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://resend.com/docs/send-with-nextjs">How to install Resend in Next.js applications</a></p>
</li>
</ul>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Mobile Quiz App with React Native, ChatGPT and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores.  The application leverages some of Supabase's features, such as authentication and databa... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-mobile-quiz-app/</link>
                <guid isPermaLink="false">66b8fc7a33470f39c663c1a4</guid>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Thu, 29 Feb 2024 17:30:26 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Building-a-mobile-quiz-app-with-React-Native--ChatGPT-and-Supabase--1--1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores. </p>
<p>The application leverages some of Supabase's features, such as authentication and database storage, to build a secured full-stack mobile application.</p>
<p>Additionally, you'll learn how to create React Native applications with Expo, generate a set of questions and answers from ChatGPT, and perform CRUD operations and user authentication with Supabase.</p>
<p>To fully understand this tutorial, you'll need to have a basic knowledge of React Native and data fetching in React applications.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-mobile-application-demo">Mobile Application Demo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to set up a React Native application with Expo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to style the React Native application with Tailwind CSS</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-application-screens">How to build the application screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-authentication-screens">How to build the authentication screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-tab-screens">How to build the tab screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-stack-screens">How to build the stack screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to generate quiz questions and answers from ChatGPT</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-to-react-native">How to add Supabase to React Native</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-authentication-to-react-native-applications">How to add Supabase authentication to React Native applications</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-up-new-users">How to sign up new users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-in-existing-users">How to sign in existing users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-log-users-out-of-the-application">How to log users out of the application</a></li>
<li><a class="post-section-overview" href="#heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-interact-with-the-supabase-database">How to interact with the Supabase database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-save-the-users-score-to-the-database">How to save user's score to the database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-mobile-application-demo">Mobile Application Demo</h2>
<p>To preview the application, download <a target="_blank" href="https://expo.dev/client">Expo Go</a> and paste the links below into the app URL field:</p>
<p><strong>Android:</strong> <code>exp://u.expo.dev/update/a4774250-e156-4d34-bcfc-a4f2549c2e1d</code><br><strong>iOS:</strong> <code>exp://u.expo.dev/update/7e5f8ba5-89c4-4c1d-b219-a613ace642df</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/app-demo.png" alt="Image" width="600" height="400" loading="lazy">
<em>Scan the QR code to preview the mobile quiz application within the Expo Go application</em></p>
<h2 id="heading-how-to-set-up-a-react-native-application-with-expo">How to Set Up a React Native Application with Expo</h2>
<p>Expo is an open-source platform that allows you to create cross-platform applications easily with JavaScript. It saves us from the complex configurations required to create a native application with the React Native CLI, making it the easiest and fastest way to build and publish React Native apps.</p>
<p>Execute the code snippet below to create a new <a target="_blank" href="https://expo.dev/">Expo</a> project that uses <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> for navigating between screens.</p>
<pre><code class="lang-bash">npx create-expo-app@latest --template tabs@50
</code></pre>
<p><a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> is an open-source file-based routing system that enables users to navigate between screens easily. It is similar to Next.js, where each file name represents its route name.</p>
<p>Start the development server to ensure that the app is working as expected.</p>
<pre><code class="lang-bash">npx expo start
</code></pre>
<h3 id="heading-how-to-style-the-react-native-application-with-tailwind-css">How to style the React Native application with Tailwind CSS</h3>
<p>Tailwind CSS is a CSS framework that lets you create modern and stunning applications easily. </p>
<p>However, to style Expo applications using Tailwind CSS, you need to install <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">NativeWind</a> – a library that uses Tailwind CSS as its scripting language.</p>
<p>Run the code snippet below to install NativeWind and its dependencies:</p>
<pre><code class="lang-bash">npx expo install nativewind@^4.0.1 react-native-reanimated tailwindcss
</code></pre>
<p>Execute <code>npx tailwindcss init</code> within your terminal to create a <code>tailwind.config.js</code> file. Update the file with the code snippet below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [<span class="hljs-string">"./app/**/*.{js,jsx,ts,tsx}"</span>],
    <span class="hljs-attr">presets</span>: [<span class="hljs-built_in">require</span>(<span class="hljs-string">"nativewind/preset"</span>)],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {},
    },
    <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<p>Create a <code>globals.css</code> file within the root of your project and add the Tailwind directives below:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>Update the <code>babel.config.js</code> file with the code below:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">api</span>) </span>{
  api.cache(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">presets</span>: [
      [<span class="hljs-string">"babel-preset-expo"</span>, { <span class="hljs-attr">jsxImportSource</span>: <span class="hljs-string">"nativewind"</span> }],
      <span class="hljs-string">"nativewind/babel"</span>,
    ],
  };
};
</code></pre>
<p>Create a <code>metro.config.js</code> file within the root of your project and paste the code snippet below into the file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { getDefaultConfig } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"expo/metro-config"</span>);
<span class="hljs-keyword">const</span> { withNativeWind } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'nativewind/metro'</span>);

<span class="hljs-keyword">const</span> config = getDefaultConfig(__dirname)

<span class="hljs-built_in">module</span>.exports = withNativeWind(config, { <span class="hljs-attr">input</span>: <span class="hljs-string">'./globals.css'</span> })
</code></pre>
<p>Finally, import the <code>./globals.css</code> file into the <code>app/_layout.tsx</code> file to enable you to style your application with Tailwind CSS:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👉🏻 Within ./app/_layout.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"../globals.css"</span>;
</code></pre>
<p>Great job on creating the React Native project with Expo! Now, you're ready to add some style using Tailwind CSS. If you encounter any problems while installing NativeWind, check out the <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">documentation</a> for a step-by-step guide.</p>
<h2 id="heading-how-to-build-the-application-screens">How to Build the Application Screens</h2>
<p>Here, I'll guide you through building the application screens. They are divided into three categories:</p>
<ul>
<li>The Authentication screens – the register and login screens.</li>
<li>The Tab layout screens – the dashboard, leaderboard, and profile screens.</li>
<li>The Stack screens – the test and test completion screens.</li>
</ul>
<p>The application prompts new users to create an account and log in before allowing access to the Tab layout screens. </p>
<p>On the dashboard screen, users can take tests on various topics. The leaderboard screen showcases the top ten users. Users can log out or preview their previous attempts on the profile page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/application-demo.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Application Demo</em></p>
<h3 id="heading-how-to-build-the-authentication-screens">How to Build the Authentication Screens</h3>
<p>The authentication screens accept the user's email and password and ensure the credentials are valid before creating an account or granting access to the application.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/auth-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Authentication Screens</em></p>
<p>Create an <code>index.tsx</code> and a <code>register.tsx</code> file within the <code>app</code> folder and a component that accepts the user's email and password using the <a target="_blank" href="https://reactnative.dev/docs/textinput">React Native TextInput</a> component.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, View, TextInput, Pressable, Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { Link, useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [password, setPassword] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-comment">//👇🏻 triggered when the user submits the email &amp; password</span>
    <span class="hljs-keyword">const</span> handleLogin = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
            <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
        setLoading(<span class="hljs-literal">true</span>);
        <span class="hljs-built_in">console</span>.log({
            email,
            password,
        });
        router.replace(<span class="hljs-string">"/(tabs)/"</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;View&gt;
        {<span class="hljs-comment">/** -- user interface--*/</span>}
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet stores the user's email and password in states using the React useState hook. The <code>handleLogin</code> function accepts the user's email and password when the form is submitted and ensures that they are not empty before logging them to the console and redirecting the user to the Dashboard page.</p>
<p>You can create the user interface using the code snippet below. It displays the input fields for the user's credentials and an interactive Sign-in button that executes the <code>handleLogin</code> function. Additionally, the <code>loading</code> state ensures that the button is only pressed once.</p>
<pre><code class="lang-typescript">&lt;View className=<span class="hljs-string">' flex-1'</span>&gt;
    &lt;View className=<span class="hljs-string">'w-full px-4'</span>&gt;
        &lt;Text className=<span class="hljs-string">'text-3xl mb-4 font-bold text-white text-center'</span>&gt;
            Log <span class="hljs-keyword">in</span>
        &lt;/Text&gt;

        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Email Address&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            value={email}
            onChangeText={setEmail}
        /&gt;
        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Password&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            secureTextEntry
            value={password}
            onChangeText={setPassword}
        /&gt;
        &lt;Pressable
            className={<span class="hljs-string">`w-full <span class="hljs-subst">${
                loading ? <span class="hljs-string">"bg-orange-200"</span> : <span class="hljs-string">"bg-orange-600"</span>
            }</span> rounded-xl p-4 border-[1px] border-orange-200`</span>}
            disabled={loading}
            onPress={<span class="hljs-function">() =&gt;</span> handleLogin()}
        &gt;
            &lt;Text className=<span class="hljs-string">'text-white text-center font-bold text-xl'</span>&gt;
                {loading ? <span class="hljs-string">"Authenticating..."</span> : <span class="hljs-string">"Sign in"</span>}
            &lt;/Text&gt;
        &lt;/Pressable&gt;
        &lt;Text className=<span class="hljs-string">'text-center mt-2 text-orange-200'</span>&gt;
            Don<span class="hljs-string">'t have an account?{" "}
            &lt;Link href='</span>/register<span class="hljs-string">'&gt;
                &lt;Text className='</span>text-white<span class="hljs-string">'&gt;Register&lt;/Text&gt;
            &lt;/Link&gt;
        &lt;/Text&gt;
    &lt;/View&gt;
&lt;/View&gt;</span>
</code></pre>
<p>For instance, the <code>loading</code> state becomes true when a user clicks the Sign-in button. The Pressable component (button) has a <code>disabled</code> attribute set to the <code>loading</code> state to ensure that the user does not press the button multiple times. Additionally, you can use the loading state to notify the user that the request is processing.</p>
<p>The <code>register.tsx</code> file is also similar to the <code>login.tsx</code> file. You only need to change the words from Login to Register.</p>
<h3 id="heading-how-to-build-the-tab-screens">How to Build the Tab Screens</h3>
<p>The Tab Screens consist of the Dashboard, Leaderboard, and Profile screens.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/tab-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Tab Screens</em></p>
<p>Create a <code>(tabs)</code> folder containing <code>index.tsx</code>, <code>leaderboard.tsx</code>, <code>profile.tsx</code>, and <code>_layout.tsx</code> files within the <code>app</code> folder.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app
mkdir (tabs)
<span class="hljs-built_in">cd</span> (tabs)
touch index.tsx leaderboard.tsx profile.tsx _layout.tsx
</code></pre>
<p>After creating the <code>_layout.tsx</code> file within the (tabs) folder, update the <code>_layout.tsx</code> to specify Tab screen navigation for the newly created screens. The screens use icons from the <a target="_blank" href="https://icons.expo.fyi/Index">Expo Vector Icons library</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { Ionicons, MaterialIcons, FontAwesome5 } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Tabs
            screenOptions={{
                tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                tabBarShowLabel: <span class="hljs-literal">false</span>,
                headerShown: <span class="hljs-literal">false</span>,
                tabBarStyle: {
                    backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                    borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                },
            }}
        &gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'index'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;Ionicons name=<span class="hljs-string">'home'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'leaderboard'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;MaterialIcons name=<span class="hljs-string">'leaderboard'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'profile'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;FontAwesome5 name=<span class="hljs-string">'user-alt'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
        &lt;/Tabs&gt;
    );
}
</code></pre>
<p>Next, update the <code>RootLayoutNav</code> component within the <code>_app/layout.tsx</code> file to render all the screens within the application.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
        &lt;/Stack&gt;
    );
}
</code></pre>
<h4 id="heading-the-dashboard-screen">The Dashboard Screen</h4>
<p>Update the component to allow users to select four categories from a list of categories.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomeScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> greet = getGreeting();
    <span class="hljs-keyword">const</span> router = useRouter();
    <span class="hljs-keyword">const</span> { session } = useAuth();
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [userCategories, setUserCategories] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> fetchQuestions = <span class="hljs-keyword">async</span> () =&gt; {};

    <span class="hljs-keyword">const</span> handleStartTest = <span class="hljs-keyword">async</span> () =&gt; {
        Alert.alert(<span class="hljs-string">"Start Test"</span>, <span class="hljs-string">"Are you sure you want to start the test?"</span>, [
            {
                text: <span class="hljs-string">"Cancel"</span>,
                style: <span class="hljs-string">"destructive"</span>,
            },
            {
                text: <span class="hljs-string">"Yes"</span>,
                onPress: <span class="hljs-function">() =&gt;</span> fetchQuestions(),
            },
        ]);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 px-4 py-2'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex flex-row items-center justify-between mb-2'</span>&gt;
                &lt;View&gt;
                    &lt;Text className=<span class="hljs-string">'font-bold text-2xl mb-[1px]'</span>&gt;
                        Good morning
                        &lt;Ionicons name=<span class="hljs-string">'partly-sunny-sharp'</span> size={<span class="hljs-number">24</span>} color=<span class="hljs-string">'orange'</span> /&gt;
                    &lt;/Text&gt;

                    &lt;Text className=<span class="hljs-string">'text-lg'</span>&gt;Welcome User&lt;/Text&gt;
                &lt;/View&gt;
            &lt;/View&gt;
            {userCategories.length === <span class="hljs-number">4</span> &amp;&amp; (
                &lt;Pressable
                    className={<span class="hljs-string">`w-full h-[70px] flex items-center justify-center <span class="hljs-subst">${
                        loading ? <span class="hljs-string">"bg-orange-300"</span> : <span class="hljs-string">"bg-orange-500"</span>
                    }</span> rounded-xl mb-2`</span>}
                    disabled={loading}
                    onPress={<span class="hljs-function">() =&gt;</span> handleStartTest()}
                &gt;
                    &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-50'</span>&gt;
                        {loading ? <span class="hljs-string">"Loading questions..."</span> : <span class="hljs-string">"START TEST"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            )}

            &lt;View className=<span class="hljs-string">'w-full flex-1'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-500 mb-4'</span>&gt;
                    Available Categories
                &lt;/Text&gt;
                &lt;FlatList
                    data={categories}
                    numColumns={<span class="hljs-number">2</span>}
                    contentContainerStyle={{ width: <span class="hljs-string">"100%"</span>, gap: <span class="hljs-number">10</span> }}
                    columnWrapperStyle={{ gap: <span class="hljs-number">10</span> }}
                    renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> (
                        &lt;Categories
                            item={item}
                            userCategories={userCategories}
                            setUserCategories={setUserCategories}
                        /&gt;
                    )}
                    showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
                    keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.id}
                /&gt;
            &lt;/View&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a list of categories where users can select only four categories to answer questions on and start the quiz.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/dashboard-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Dashboard Screen</em></p>
<h4 id="heading-the-leaderboard-screen">The Leaderboard Screen</h4>
<p>The Leaderboard screen displays the top ten users ranked in descending order.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, FlatList, SafeAreaView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> Board <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Board"</span>;

<span class="hljs-keyword">interface</span> Props {
    total_score: <span class="hljs-built_in">number</span>;
    user_id: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LeaderboardScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [leaderboard, setLeaderboard] = useState&lt;Props[]&gt;([]);

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;Text className=<span class="hljs-string">'text-2xl font-bold text-gray-500 text-center mb-6'</span>&gt;
                Leaderboard
            &lt;/Text&gt;

            &lt;FlatList
                data={leaderboard}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Board item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.user_id}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a FlatList with ten items. You can create an array containing ten users and pass it into the FlatList for now.</p>
<h4 id="heading-the-profile-screen">The Profile Screen</h4>
<p>The Profile Screen displays the user's image, recent attempts, and a log-out button that enables the user to sign out of the application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProfileScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [total_score, setTotalScore] = useState&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">const</span> [attempts, setAttempts] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
        setLoading(<span class="hljs-literal">true</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex items-center justify-center mb-6'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-[1px]'</span>&gt;
                    &lt;FontAwesome name=<span class="hljs-string">'star'</span> size={<span class="hljs-number">20</span>} color=<span class="hljs-string">'red'</span> /&gt;
                    &lt;Text&gt;<span class="hljs-number">45</span>&lt;/Text&gt;
                &lt;/Text&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-2'</span>&gt;<span class="hljs-meta">@dhastix</span>&lt;/Text&gt;

                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> handleSignOut()} disabled={loading}&gt;
                    &lt;Text className=<span class="hljs-string">'text-red-500'</span>&gt;
                        {loading ? <span class="hljs-string">"Logging out..."</span> : <span class="hljs-string">"Log out"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            &lt;/View&gt;

            &lt;Text className=<span class="hljs-string">'font-bold text-xl text-gray-700 mb-3 px-4'</span>&gt;
                Recent Attempts
            &lt;/Text&gt;

            &lt;FlatList
                data={attempts}
                contentContainerStyle={{ padding: <span class="hljs-number">15</span> }}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Attempts item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> index.toString()}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above displays the user's image, the sign-out button, and all the user's attempts. You can create an array of items for testing purposes.</p>
<h3 id="heading-how-to-build-the-stack-screens">How to Build the Stack Screens</h3>
<p>The Stack Screens comprise two screens – the quiz screen and the screen that displays the user's score after completing a quiz session.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screens-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Stack Screens</em></p>
<h4 id="heading-the-quiz-screen">The Quiz Screen</h4>
<p>The Quiz Screen displays a timer that countdowns from 15 seconds before moving to the next question. It shows the question, its category, available options, the Skip and Next buttons, and a cancel icon.</p>
<p>Create a similar screen to the one shown below. You can use <a target="_blank" href="https://github.com/dha-stix/techtest-app/blob/main/app/(stack)/test.tsx">this</a> as a guide.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Quiz Screen</em></p>
<h4 id="heading-the-quiz-completion-screen">The Quiz Completion Screen</h4>
<p>It displays the user's score after completing a test.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    SafeAreaView,
    Text,
    Pressable,
    View,
    ImageBackground,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { MaterialIcons } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { useLocalSearchParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CompletedScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { score } = useLocalSearchParams();

    <span class="hljs-keyword">return</span> (
        &lt;View className=<span class="hljs-string">'flex flex-1 bg-orange-400'</span>&gt;
            &lt;ImageBackground
                source={{ uri: <span class="hljs-string">"https://source.unsplash.com/NAP14GEjvh8"</span> }}
                className=<span class="hljs-string">'flex-1 p-4'</span>
            &gt;
                &lt;SafeAreaView /&gt;
                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> router.replace(<span class="hljs-string">"/(tabs)/"</span>)}&gt;
                    &lt;MaterialIcons name=<span class="hljs-string">'cancel'</span> size={<span class="hljs-number">60</span>} color=<span class="hljs-string">'white'</span> /&gt;
                &lt;/Pressable&gt;

                &lt;View className=<span class="hljs-string">'flex-1 flex items-center justify-center'</span>&gt;
                    &lt;View className=<span class="hljs-string">'bg-orange-50 w-full py-[50px] rounded-xl p-4 flex items-center justify-center shadow-lg shadow-orange-500'</span>&gt;
                        &lt;Text className=<span class="hljs-string">'text-3xl text-orange-600 font-bold mb-4'</span>&gt;
                            {<span class="hljs-built_in">Number</span>(score) &gt; <span class="hljs-number">20</span> ? <span class="hljs-string">"Congratulations🥳"</span> : <span class="hljs-string">"Sorry! You lose 🥲"</span>}
                        &lt;/Text&gt;
                        &lt;Text className=<span class="hljs-string">'font-bold text-xl'</span>&gt;You scored {score}!&lt;/Text&gt;
                    &lt;/View&gt;
                &lt;/View&gt;
            &lt;/ImageBackground&gt;
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet above accepts the user's score as a parameter after completing the quiz and displays the score to the user.</p>
<h2 id="heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to Generate Quiz Questions and Answers from ChatGPT</h2>
<p>When building a quiz application, the first question is: how do you get the questions and options for the application? You can either create a list of questions or search for a suitable public API.</p>
<p>However, I'll guide you through creating a list of questions and options in JSON format using ChatGPT. Use this prompt to generate questions and answers from ChatGPT:</p>
<blockquote>
<p><em>Generate 25 distinct questions on  and ensure they are in JSON format containing an id, category which is , a question attribute containing the question, an options array of 3 options, and an answer property.</em></p>
</blockquote>
<p>The prompt returns a JSON result containing the questions and answers. You can host them on GitHub or save them to a database.</p>
<p>The questions and answers I'm using in this mobile application are available on <a target="_blank" href="https://github.com/dha-stix/trivia-app/tree/main/questions">GitHub</a>. Feel free to clone or copy the files.</p>
<p>Once your questions and answers are ready, you can connect the application to Supabase.</p>
<h2 id="heading-how-to-add-supabase-to-react-native">How to Add Supabase to React Native</h2>
<p>Supabase is an open-source Firebase alternative that enables you to create secured and scalable software applications within a few minutes.</p>
<p>It provides a secured Postgres database, a complete user management system that handles various forms of authentication (including email and password, email sign-in, and social authentication), a file storage system that lets you store and serve files of any size, real-time communication, and many other features.</p>
<p>In this tutorial, I'll walk you through the following:</p>
<ul>
<li>How to authenticate users and control access to some application screens with Supabase.</li>
<li>How to save the users' scores to the database to enable you to rank them based on their scores.</li>
</ul>
<p>First, you need to install Supabase and its required dependencies. You can do that with the following commands:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js 
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
</code></pre>
<p>Create a <code>supabase.ts</code> file within your project and copy the code snippet below into the file to initiate Supabase:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">"react-native-url-polyfill/auto"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-secure-store"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;

<span class="hljs-keyword">const</span> ExpoSecureStoreAdapter = {
    getItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> SecureStore.getItemAsync(key);
    },
    setItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.setItemAsync(key, value);
    },
    removeItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.deleteItemAsync(key);
    },
};

<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_URL"</span>;
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_ANON_KEY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> supabase = createClient(supabaseUrl, supabaseAnonKey, {
    auth: {
        storage: ExpoSecureStoreAdapter <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>,
        autoRefreshToken: <span class="hljs-literal">true</span>,
        persistSession: <span class="hljs-literal">true</span>,
        detectSessionInUrl: <span class="hljs-literal">false</span>,
    },
});
</code></pre>
<p>Next, visit the <a target="_blank" href="https://supabase.com">Supabase homepage</a>, sign in, and create a new organization and project.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/project.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a new Supabase project</em></p>
<p>Click the Settings icon on the sidebar and select API to copy the project URL and the public API key.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-17.50.34.png" alt="Image" width="600" height="400" loading="lazy">
<em>Supabase API settings containing project credentials</em></p>
<p>Create a <code>.env.local</code> file and copy the credentials into the variables. Update the <code>supabase.ts</code> file to use the Supabase URL and API key.</p>
<pre><code class="lang-txt">EXPO_PUBLIC_API_URL=&lt;YOUR_SUPABASE_URL&gt;
EXPO_PUBLIC_API_KEY=&lt;YOUR_SUPABASE_API_KEY&gt;
</code></pre>
<p>Congratulations! You can now interact with Supabase from your application and access various features such as authentication, database, file storage, and so on.</p>
<h2 id="heading-how-to-add-supabase-authentication-to-react-native-applications">How to Add Supabase Authentication to React Native Applications</h2>
<p>Supabase offers various forms of authentication. But we only need the email and password method of authentication for this application.</p>
<h3 id="heading-how-to-sign-up-new-users">How to sign up new users</h3>
<p>The code snippet below accepts an email and a password and creates an account for the user. Otherwise, it returns an error if any of the credentials is invalid.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign up function</span>
<span class="hljs-keyword">const</span> handleRegister = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/"</span>);
};
</code></pre>
<p>With the <code>supabase.auth.signUp()</code> function, Supabase handles the authentication process. If successful, the user is redirected to the login page. Otherwise, it displays an error message.</p>
<h3 id="heading-how-to-sign-in-existing-users">How to sign in existing users</h3>
<p>This function allows existing users to access the application. It accepts the user's email and password and logs the user into the application.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 register function</span>
<span class="hljs-keyword">const</span> handleLogin = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/(tabs)/"</span>);
};
</code></pre>
<p>The <code>supabase.auth.signInWithPassword()</code> function validates the user's email and password and redirects the user to the Dashboard screen. Otherwise, it returns the necessary authentication error.</p>
<h3 id="heading-how-to-log-users-out-of-the-application">How to log users out of the application</h3>
<p>Supabase also allows users to sign out of the application. You can execute this function when the user clicks a button within the Profile page.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign out function</span>
<span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
    }
};
</code></pre>
<h3 id="heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</h3>
<p>You've been able to add the sign-up, sign-in, and log-out functionalities to the React Native application. But the Dashboard and other screens containing sensitive data are still accessible to unauthenticated users.</p>
<p>How do we fix this?</p>
<p>In this section, I'll walk you through how to protect screens from unauthorized users using the <a target="_blank" href="https://react.dev/reference/react/createContext">React Context API</a>.</p>
<p>The React Context API allows us to pass data through the component tree without needing to pass props down manually at every level.</p>
<p>Create an <code>AuthProvider.tsx</code> file. This is where the data to be passed down the application screens is stored. Copy the code snippet below into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;
<span class="hljs-keyword">import</span> { Session } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
<span class="hljs-keyword">import</span> {
    PropsWithChildren,
    createContext,
    useContext,
    useEffect,
    useState,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">type</span> AuthData = {
    session: Session | <span class="hljs-literal">null</span>;
    loading: <span class="hljs-built_in">boolean</span>;
};

<span class="hljs-comment">//👇🏻 data to be passed down the components</span>
<span class="hljs-keyword">const</span> AuthContext = createContext&lt;AuthData&gt;({
    session: <span class="hljs-literal">null</span>,
    loading: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthProvider</span>(<span class="hljs-params">{ children }: PropsWithChildren</span>) </span>{
    <span class="hljs-keyword">const</span> [session, setSession] = useState&lt;Session | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);

    <span class="hljs-comment">//👇🏻 fetches the current user's session</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> fetchSession = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">const</span> {
                data: { session },
            } = <span class="hljs-keyword">await</span> supabase.auth.getSession();
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        };

        fetchSession();
        supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">_event, session</span>) =&gt;</span> {
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        });
    }, []);

    <span class="hljs-keyword">return</span> (
        &lt;AuthContext.Provider value={{ session, loading }}&gt;
            {children}
        &lt;/AuthContext.Provider&gt;
    );
}
<span class="hljs-comment">//👇🏻 custom hook for using the context (data)</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useAuth = <span class="hljs-function">() =&gt;</span> useContext(AuthContext);
</code></pre>
<p>The code snippet retrieves the current user's session. If the user is signed in, the session and loading state variables are updated to show that the user is active, and they are passed into other components within the application.</p>
<p>The <code>useAuth</code> custom hook allows you to access the state variables (session and loading) within the application screens.</p>
<p>To access the context (data) available within the application screens, wrap the entire application with the <code>AuthProvider</code>. So now, update the <code>RootLayoutNav</code> component within the <code>app/_layout.tsx</code> file as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> AuthProvider <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/AuthProvider"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;AuthProvider&gt;
            &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
            &lt;/Stack&gt;
        &lt;/AuthProvider&gt;
    );
}
</code></pre>
<p>Congratulations! You've successfully set up the context. Next, how do we read the context and ensure that only authenticated users can view some of the application screens?</p>
<p>You can do this using the custom <code>useAuth</code> hook. For example, you can protect the Tabs screens via the <code>(tabs)/_layout.tsx</code> file.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs, Redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/AuthProvider"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { session, loading } = useAuth();

    <span class="hljs-keyword">if</span> (!session) {
        <span class="hljs-keyword">return</span> &lt;Redirect href=<span class="hljs-string">'/'</span> /&gt;;
    }

    <span class="hljs-keyword">if</span> (loading) {
        <span class="hljs-keyword">return</span> &lt;ActivityIndicator size=<span class="hljs-string">'large'</span> color=<span class="hljs-string">'#f97316'</span> /&gt;;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> (
            &lt;Tabs
                screenOptions={{
                    tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                    tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                    tabBarShowLabel: <span class="hljs-literal">false</span>,
                    headerShown: <span class="hljs-literal">false</span>,
                    tabBarStyle: {
                        backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                        borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                    },
                }}
            &gt;
                {<span class="hljs-comment">/**-- screens--*/</span>}
            &lt;/Tabs&gt;
        );
    }
}
</code></pre>
<p>The code snippet above checks if there is a session for the current user. If null, the application redirects the user to the login screen. If the application is yet to determine the user's status, it displays a loading icon.</p>
<h2 id="heading-how-to-interact-with-the-supabase-database">How to Interact with the Supabase Database</h2>
<p>In this section, I'll walk you through creating the database for the mobile application. You'll learn how to store and retrieve the user's scores and rank them based on their total score.</p>
<p>Before we proceed, note that the application calculates the user's score after answering each question on the test screen. Upon completion, the user's score is retrieved and displayed on the test completion screen.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleSave = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">//👇🏻 checks if the user has not completed the test</span>
    <span class="hljs-keyword">if</span> (count &lt; questions.length - <span class="hljs-number">1</span>) {
        <span class="hljs-comment">//👇🏻 updates the user's score if the selected answer is correct</span>
        <span class="hljs-keyword">if</span> (questions[count].answer === userAnswer) {
            setUserScore(<span class="hljs-function">(<span class="hljs-params">userScore</span>) =&gt;</span> userScore + <span class="hljs-number">1</span>);
        }
        <span class="hljs-comment">//👇🏻 change the question, refresh the selected answer and time</span>
        setCount(<span class="hljs-function">(<span class="hljs-params">count</span>) =&gt;</span> count + <span class="hljs-number">1</span>);
        setSelectedBox(<span class="hljs-literal">null</span>);
        setTime(<span class="hljs-number">15</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">//👇🏻 test completed</span>
        router.push({
            pathname: <span class="hljs-string">"/(stack)/completed"</span>,
            params: { score: userScore },
        });
    }
};
</code></pre>
<p>Within your Supabase project, select Table Editor from the sidebar menu and create a new table containing the following columns:</p>
<ul>
<li><code>id</code> – contains a unique ID for each row of data.</li>
<li><code>created_at</code> – represents the time the data was created.</li>
<li><code>attempts</code> – a text array containing the score and date attributes.</li>
<li><code>total_score</code> – represents a user's cumulative score. We'll rank users using this score</li>
<li><code>user_id</code> – a unique ID used to identify each user's data.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.05.55.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Columns</em></p>
<p>Finally, you can add a Row Level Security that allows only authenticated users interact with the database.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.20.51.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Row Level Security Policy</em></p>
<h3 id="heading-how-to-save-the-users-score-to-the-database">How to save the user's score to the database</h3>
<p>Before you can save a user's score to the database, you need to check if the user's data already exists – meaning the user has taken a test before. If true, you need to update the user's score with the latest test score. Otherwise, add the data to the database.</p>
<p>The code snippet below accepts the user's score and user's ID (from the session data) and saves the user's score to Supabase.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> saveScore = <span class="hljs-keyword">async</span> (userScore: <span class="hljs-built_in">number</span>, userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">//👇🏻 check if the user data exists</span>
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select()
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;

        <span class="hljs-comment">//👇🏻 if the user data does not exist, insert a new one</span>
        <span class="hljs-keyword">if</span> (error || !data.length) {
            <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .insert({
                    attempts: [{ score: userScore, date: getCurrentDate() }],
                    total_score: userScore,
                    user_id: userID,
                })
                .single();
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">//👇🏻 if the user data exists, update the attempts and total_score</span>
            <span class="hljs-keyword">const</span> { data: updateData, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .update({
                    attempts: [
                        ...data[<span class="hljs-number">0</span>].attempts,
                        { score: userScore, date: getCurrentDate() },
                    ],
                    total_score: data[<span class="hljs-number">0</span>].total_score + userScore,
                })
                .eq(<span class="hljs-string">"user_id"</span>, userID);
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(err);
    }
};
</code></pre>
<h3 id="heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</h3>
<p>Recall that you need to rank the users based on their scores on the Leaderboard screen and retrieve the user's attempts on the Profile screen.</p>
<p>The code snippet below accepts a user's ID and retrieves the attempts and total score from the database.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getUserAttempts = <span class="hljs-keyword">async</span> (userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"attempts, total_score"</span>)
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> { attempts: data[<span class="hljs-number">0</span>].attempts, total_score: data[<span class="hljs-number">0</span>].total_score };
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> { attempts: <span class="hljs-string">""</span>, total_score: <span class="hljs-number">0</span> };
    }
};
</code></pre>
<p>The code snippet below retrieves the top ten users from the database based on their score.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getLeaderBoard = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"total_score, user_id"</span>)
            .order(<span class="hljs-string">"total_score"</span>, { ascending: <span class="hljs-literal">false</span> })
            .limit(<span class="hljs-number">10</span>);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> data;
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
};
</code></pre>
<p>Congratulations! You've successfully completed the project for this tutorial.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you’ve learned how to:</p>
<ul>
<li>build React Native mobile applications with Expo,</li>
<li>style your mobile applications with <a target="_blank" href="https://www.nativewind.dev/">Tailwind CSS</a>,</li>
<li>create stack and tab screen navigations using <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a>,</li>
<li>use Supabase and leverage its authentication and database features to build full-stack applications.</li>
</ul>
<p>Supabase is an amazing tool that enables you to build a full-stack software application with no hassle. If you are looking forward to shipping great software products or side projects faster, consider using Supabase.</p>
<p>Expo also saves us from the complexities of setting up and developing mobile applications using the <a target="_blank" href="https://reactnative.dev/docs/environment-setup">React Native CLI</a>. It enables you to focus more on building your applications while it handles the necessary configurations, including deployment.</p>
<p>Feel free to customise the application using <a target="_blank" href="https://chat.openai.com/">ChatGPT</a> to generate questions and answers tailored to any niche or topic.</p>
<p>The source code for this tutorial is available in this <a target="_blank" href="https://github.com/dha-stix/techtest-app">GitHub repository</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
