<?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[ Strapi - 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[ Strapi - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 30 May 2026 16:31:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/strapi/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Multilingual Social Recipe Application with Flutter and Strapi ]]>
                </title>
                <description>
                    <![CDATA[ Hey there! In this project, you will build a multilingual social recipe application using Flutter and Strapi. Flutter is an open-source UI software development kit created by Google. It allows you to build beautiful and highly interactive user interf... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-multilingual-social-recipe-app-with-flutter-and-strapi/</link>
                <guid isPermaLink="false">67f59a4c27d15057ec14c438</guid>
                
                    <category>
                        <![CDATA[ Recipe Apps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Strapi ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ multilingual ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kevine Nzapdi ]]>
                </dc:creator>
                <pubDate>Tue, 08 Apr 2025 21:51:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743509325302/fd7d5d6c-9a48-4037-9cc2-3b35a92b6006.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hey there!</p>
<p>In this project, you will build a multilingual social recipe application using Flutter and Strapi.</p>
<p>Flutter is an open-source UI software development kit created by Google. It allows you to build beautiful and highly interactive user interfaces for mobile, web, and desktop from a single codebase.</p>
<p>Strapi, on the other hand, is a headless CMS that makes it easy to create, manage and distribute content anywhere you need – all from one place.</p>
<p>The multilingual feature of the application will allow users from different parts of the world to interact with the app in their native language, making it more user-friendly and accessible. This feature is particularly beneficial for a social recipe application where users share recipes from different cuisines and cultures.</p>
<p>In this application, users will be able to view recipes, request a specific recipe, share their favorite recipes, and like or comment on recipes.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-demo">Demo</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-models">Create Models</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-languages-and-enable-internationalization-in-strapi">Add Languages and Enable Internationalization in Strapi</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-content">Add Recipe Content</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-add-recipe-english-content">Add Recipe English Content</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-french-content">Add Recipe French Content</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-recipe-japanese-content">Add Recipe Japanese Content</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-generate-api-token-and-set-permissions">Generate API Token and Set permissions</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-set-user-roles-and-permissions">Set User Roles and Permissions</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-flutter">Set up Flutter</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-install-packages">Install Packages</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-add-assets">Add Assets</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-taking-a-look-at-maindart">Taking a look at main.dart</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-add-environment-variables">Add Environment Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-models-1">Create Models</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-reciperequest">1. RecipeRequest</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-step">2. Step</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-description">3. Description</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-textcontent">4. TextContent</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-comment">5. Comment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-recipe">6. Recipe</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-create-services">Create Services</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-class-variables">1. Class Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-helper-methods">2. Helper Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-user-operations">3. User Operations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-data-fetching-and-manipulation">4. Data Fetching and Manipulation</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-authorization-and-authentication">Authorization and Authentication</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-registration">Registration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-login">Login</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-build-app-components">Build App Components</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-drawer">Drawer</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-appbar">AppBar</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-fetch-recipes">Fetch Recipes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-view-recipe">View Recipe</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-request-recipe-screen">Create Request Recipe Screen</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-user-profile-screen">Create User Profile Screen</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-the-app">Test the App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, make sure you have:</p>
<ul>
<li><p><a target="_blank" href="https://nodejs.org/en">Node.js</a> installed.</p>
</li>
<li><p>Basic knowledge of <a target="_blank" href="https://flutter.dev/">Flutter</a></p>
</li>
<li><p>Basic understanding of Strapi with this <a target="_blank" href="https://docs.strapi.io/dev-docs/quick-start">quick guide</a></p>
</li>
</ul>
<h2 id="heading-demo">Demo</h2>
<p>Here’s what you will be building in the tutorial:</p>
<ol>
<li><p>Authentication and Authorization: <a target="_blank" href="https://drive.google.com/file/d/1cjnnRD38wQsj_sYHl5EG5uM3AyHJUWdf/view?usp=sharing">Demo</a></p>
</li>
<li><p>Comment and Likes: <a target="_blank" href="https://drive.google.com/file/d/1wM0xQ2R7inL90gAkiYjLcGV5df4AmzH1/view?usp=sharing">Demo</a></p>
</li>
<li><p>Request recipe: <a target="_blank" href="https://drive.google.com/file/d/1xlxSFD2qU2rOE4kICiX-py_JxvgrphqK/view?usp=sharing">Demo</a></p>
</li>
<li><p>Language Switch: <a target="_blank" href="https://drive.google.com/file/d/14lmBCIgX4VIKOFmS9pG71cIHH7HLaW1J/view?usp=sharing">Demo</a></p>
</li>
</ol>
<p>You can get the full code of the application from <a target="_blank" href="https://github.com/Gunkev/flutter_strapi_multilingual_app">this GitHub repository</a>.</p>
<h2 id="heading-create-models">Create Models</h2>
<p>Once you have set up a Strapi project with <a target="_blank" href="https://docs.strapi.io/dev-docs/installation/cli">this quick guide</a>, create two models, Recipe and RecipeRequest, in the Strapi admin panel.</p>
<p>A recipe typically has the following elements:</p>
<ul>
<li><p>Title: <code>text</code> which represents the title of the recipe</p>
</li>
<li><p>Ingredients: <code>text</code> which represent the of ingredients of the recipe</p>
</li>
<li><p>Likes: <code>int</code> which represent the number of likes</p>
</li>
<li><p>Author: <code>relation</code> which represent the author of the recipe</p>
</li>
<li><p>Comments: <code>relation</code> which represent the list of comments of a specific recipe</p>
</li>
<li><p>Steps: <code>rich text</code> which represents the main content of the recipe</p>
</li>
<li><p>Description: <code>rich text</code> which represents a description of what the recipe is like</p>
</li>
<li><p>Comment Count: <code>int</code> which represents the number of comment a recipe has</p>
</li>
<li><p>Cover Image: <code>media</code> which represents the cover image of the recipe</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743504946186/e1be7d98-fff8-4e2e-b446-1ddbf541d1c0.png" alt="recipe model" class="image--center mx-auto" width="1360" height="834" loading="lazy"></p>
<p>Make sure to enable internationalization for Recipe Content Type when you create it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743504992503/73842540-4b8d-4412-9c51-1c55e095e83e.png" alt="enable internationalization" class="image--center mx-auto" width="827" height="311" loading="lazy"></p>
<p>A recipe request typically has:</p>
<ul>
<li><p>Title, which is <code>text</code> that represents the title of the request</p>
</li>
<li><p>Description, which is <code>rich text</code> that represents the content of the request</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505019316/6d172672-af58-4a6d-b0a3-cb713ee32dd2.png" alt="recipe request model" class="image--center mx-auto" width="1383" height="496" loading="lazy"></p>
<p>A comment typical has:</p>
<ul>
<li><p>Author, which is a <code>relation</code> that represents the author of the comment</p>
</li>
<li><p>Content, which is <code>text</code> that represents the content of the comments</p>
</li>
<li><p>Date, which is a <code>date</code> that represents the published date of the comment</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505036935/92d02ecb-9a86-43f9-99a9-a2a534aab871.png" alt="comment model" class="image--center mx-auto" width="1367" height="435" loading="lazy"></p>
<p>The user will also have 4 new fields:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505060587/cda0be86-298b-4053-b8ae-8c894e07a592.png" alt="additional user fields" class="image--center mx-auto" width="1346" height="199" loading="lazy"></p>
<h2 id="heading-add-languages-and-enable-internationalization-in-strapi">Add Languages and Enable Internationalization in Strapi</h2>
<p>The application will support three different languages (English, French, and Japanese). English is the default language, so you need to add the two others. In the Strapi panel, you’ll need to navigate to Settings and then Internationalization and add French and Japanese. I will explain the process in detail in the next sections.</p>
<h2 id="heading-add-recipe-content">Add Recipe Content</h2>
<p>Next, you will populate some recipe data in English, French, and Japanese.</p>
<h3 id="heading-add-recipe-english-content">Add Recipe English Content</h3>
<p>Since English is the default language, go to Content manager, then select Recipe, and then select <strong>Create new entry</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505111608/3fb2d615-d649-4c22-8a73-87cbcbd38bdb.png" alt="list of added recipes" class="image--center mx-auto" width="1368" height="441" loading="lazy"></p>
<h3 id="heading-add-recipe-french-content">Add Recipe French Content</h3>
<p>For French, navigate to Settings, select Internationalization, and then under global settings click on <strong>Add new locale.</strong> Here you will add the French language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505140738/a8e5b0d0-0871-46b1-8fb0-2921c84b913a.png" alt="french language config" class="image--center mx-auto" width="840" height="403" loading="lazy"></p>
<p>Back to the Content manager, click on recipe and select the French language in the top right corner. Then choose the <strong>Create recipe entry</strong> in French.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505164770/2ad75e5a-a20d-496d-9fe3-75fdc3cf64b1.png" alt="french model version" class="image--center mx-auto" width="1356" height="420" loading="lazy"></p>
<h3 id="heading-add-recipe-japanese-content">Add Recipe Japanese Content</h3>
<p>Navigate back to Settings and Internationalization, and under global settings again click on <strong>Add new locale.</strong> Now you will add the Japanese language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505187987/91251e4e-4172-4ce5-9e53-78ca12352af4.png" alt="japanese language config" class="image--center mx-auto" width="833" height="400" loading="lazy"></p>
<p>Back to the Content manager, click on recipe and select the Japanese language in the top right corner. Then select <strong>Create new entry</strong> in Japanese.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505218903/0e7b7025-8473-4012-ab54-130fe5b63164.png" alt="Japenese recipe list" class="image--center mx-auto" width="1367" height="446" loading="lazy"></p>
<h2 id="heading-generate-api-token-and-set-permissions">Generate API Token and Set permissions</h2>
<p>Once you’ve added the content for the various languages, it’s time to create your API and set the necessary permissions.</p>
<p>To do this, navigate to Settings, then API Tokens, and then Create API Token. Add the details of your key there.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505239235/5a183f54-6469-4d4e-aa62-d81f4dccf8ae.png" alt="API token creation" class="image--center mx-auto" width="1433" height="830" loading="lazy"></p>
<ul>
<li><p>Token duration: choose Unlimited</p>
</li>
<li><p>Token Type: Custom. The custom type allows you to specify permission for certain entities.</p>
</li>
</ul>
<p>Next, still in the Create API Token screen, scroll down to the permission section and set the permission to “Select all” for Comments, and RecipeRequest, upload, email, content type, i18n, and User permissions like in the screenshot below for Recipe-request:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505260256/84f6f009-4c7a-4136-8497-6c22b9fa87de.png" alt="enable permission for recipe request" class="image--center mx-auto" width="690" height="332" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744116611459/f5518d2e-5200-40b3-9b74-ed0b0adeeabb.png" alt="f5518d2e-5200-40b3-9b74-ed0b0adeeabb" class="image--center mx-auto" width="474" height="660" loading="lazy"></p>
<p>Then click on the Save button in the top right corner to generate your API key. Copy and save the key in your PC as you won’t be able to see it again</p>
<h3 id="heading-set-user-roles-and-permissions">Set User Roles and Permissions</h3>
<p>You’ll also need to set the user roles and permissions using the <a target="_blank" href="https://docs.strapi.io/dev-docs/plugins/users-permissions">User and Permission Plugin</a>. It allows you to manage what both authenticated and non-authenticated users can do in your application.</p>
<p>Head to the Settings section of the dashboard and go to Roles under the User and Permission plugin.</p>
<p>We have two types of users:</p>
<ul>
<li><p>Authenticated users</p>
</li>
<li><p>Public users</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744117848867/8023d7c4-c07b-43dc-ba00-89a958bc0672.png" alt="8023d7c4-c07b-43dc-ba00-89a958bc0672" class="image--center mx-auto" width="1890" height="762" loading="lazy"></p>
<p>Select the authenticated users and give them the following permissions for:</p>
<p>Comment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505301527/3939448a-48f4-44fc-baa9-a528a78e73c7.png" alt="enable permission for comments" class="image--center mx-auto" width="628" height="666" loading="lazy"></p>
<p>Recipe:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505327113/f9224713-105d-4cdb-9a5b-4846d1789b07.png" alt="enable authorized user to perdorm action on recipe model" class="image--center mx-auto" width="615" height="735" loading="lazy"></p>
<p>Request-recipe:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505346092/d328c629-4ea9-40a0-baa6-90a96ae364ec.png" alt="enable permission for recipe request model" class="image--center mx-auto" width="616" height="734" loading="lazy"></p>
<p>Also select all for Content-type builder, i18n, and Upload and then save.</p>
<p>Public users can only read recipes and comments:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505362706/4d776b8f-84f9-4a41-a1d4-73b1a2fd6a4c.png" alt="limit comment operation for public users" class="image--center mx-auto" width="688" height="347" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505369235/54ed5f73-9841-43bf-a088-0079358b6b05.png" alt="limit recipe operations for public user" class="image--center mx-auto" width="684" height="349" loading="lazy"></p>
<h2 id="heading-set-up-flutter">Set Up Flutter</h2>
<p>Once you have <a target="_blank" href="https://docs.flutter.dev/get-started/install/windows/desktop">set up Flutte</a><a target="_blank" href="https://docs.flutter.dev/get-started/install/windows/desktop">r</a> in your environment, run the following command to bootstrap a new application in your favorite directory:</p>
<pre><code class="lang-bash">flutter create flutter_recipe_app
</code></pre>
<p>To see your app in action, you need to run it on a mobile device. You can either:</p>
<ul>
<li><p>Use an <strong>emulator</strong> (a virtual Android or iOS device that runs on your computer), or</p>
</li>
<li><p>Connect a <strong>physical device</strong> (like your smartphone) to your computer with a USB cable.</p>
</li>
</ul>
<p>Once your emulator or device is ready, navigate into the newly created project folder:</p>
<pre><code class="lang-bash">flutter run
</code></pre>
<p>This command builds the app and starts it on your connected device or emulator.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743505498936/6e1e461d-9fee-4e19-81e0-65d25ddebd63.png" alt="flutter starter app" class="image--center mx-auto" width="1080" height="2220" loading="lazy"></p>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Now let's look at the file structure of the project:</p>
<pre><code class="lang-bash">flutter_recipe_app/
|
|-- .dart_tool/
|-- .idea/
|-- android/ [flutter_recipe_app_android]
|   |-- assets/
|   |   |-- images/
|   |   |-- translations/
|
|-- build/
|-- ios/
|-- lib/
|   |-- components/
|   |   |-- appBar.dart
|   |   |-- drawer.dart
|   |
|   |-- models/
|   |   |-- recipe.dart
|   |
|   |-- screens/
|   |   |-- detail.dart
|   |   |-- home.dart
|   |   |-- login.dart
|   |   |-- profile.dart
|   |   |-- requestRecipe.dart
|   |   |-- signUp.dart
|   |
|   |-- utils/
|       |-- server2.dart
|
|-- main.dart
|-- <span class="hljs-built_in">test</span>/
|-- .env
</code></pre>
<p>The structure is organized as follows:</p>
<ul>
<li><p><code>.dart_tool/</code>: Contains Dart tools and build outputs.</p>
</li>
<li><p><code>.idea/</code>: IDE-specific settings.</p>
</li>
<li><p><code>android/</code>: Android-specific project files, including custom assets like images and translations.</p>
</li>
<li><p><code>build/</code>: Generated files from the build process.</p>
</li>
<li><p><code>ios/</code>: iOS-specific project files.</p>
</li>
<li><p><code>lib/</code>: The main source directory for Dart code, which includes:</p>
<ul>
<li><p><code>components/</code>: Reusable widgets or UI components like <code>appBar</code> and <code>drawer</code>.</p>
</li>
<li><p><code>models/</code>: Data models for your application, like <code>recipe</code>.</p>
</li>
<li><p><code>screens/</code>: Individual screens of the app, such as the <code>recipe details</code>, <code>home</code>, <code>login</code>, <code>profile</code>, <code>request recipe</code> and <code>signUp</code> screens of the app</p>
</li>
<li><p><code>utils/</code>: Utilities and helper functions, like <code>server2.dart</code> for the server communication logic.</p>
</li>
</ul>
</li>
<li><p><code>main.dart</code>: The entry point of the Flutter application.</p>
</li>
<li><p><code>test/</code>: Directory for test files.</p>
</li>
<li><p><code>.env</code>: Environment-specific variables file.</p>
</li>
</ul>
<p>This setup is typical for a moderately complex Flutter application, segregating functionality into manageable, logical sections for better organization and maintainability.</p>
<h2 id="heading-install-packages">Install Packages</h2>
<p>In this tutorial, we’re using five main packages:</p>
<ul>
<li><p><a target="_blank" href="https://pub.dev/packages/flutter_dotenv">flutter_dotenv</a>: to manage environment variables</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/http">http</a>: to handle HTTP requests and interact with <a target="_blank" href="https://docs.strapi.io/dev-docs/api/rest">Strapi REST API</a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/shared_preferences">shared_preferences</a>: persists key-value data on the device like user login tokens</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/provider">provider</a>: for state management and updating your UI reactively when the underlying state changes</p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/easy_localization">easy_localization</a>: for managing translations and locale data. It supports both JSON and YAML file formats for defining translations.</p>
</li>
</ul>
<p>In your <code>pubspec.yaml</code> file, add the following lines:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-string">...</span>
  <span class="hljs-attr">flutter_dotenv:</span> <span class="hljs-string">^5.1.0</span>
  <span class="hljs-attr">http:</span> <span class="hljs-string">^1.1.0</span>
  <span class="hljs-attr">shared_preferences:</span> <span class="hljs-string">^2.2.2</span>
  <span class="hljs-attr">provider:</span> <span class="hljs-string">^6.1.2</span>
  <span class="hljs-attr">easy_localization:</span> <span class="hljs-string">^3.0.7</span>
</code></pre>
<p>Then run the command below to install the packages:</p>
<pre><code class="lang-bash">flutter pub get
</code></pre>
<h3 id="heading-add-assets">Add Assets</h3>
<p>Add the path to your assets in your <code>pubspec.yaml</code> file found at the root of your project:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter:</span>
  <span class="hljs-attr">uses-material-design:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">assets:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/translations/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/images/</span>
</code></pre>
<p>The translations folder contains the list of your translations while the images folder hosts the photos of your application.</p>
<h3 id="heading-taking-a-look-at-maindart">Taking a look at main.dart</h3>
<p>In the <code>main.dart</code> file, you need to set up your localization, load environment variables, and a list of providers for dependency injection:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/home.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/login.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/requestRecipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/signUp.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/utils/server.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;

Future&lt;<span class="hljs-keyword">void</span>&gt; main() <span class="hljs-keyword">async</span>{
  <span class="hljs-comment">// Ensure all bindings are initialized</span>
  WidgetsFlutterBinding.ensureInitialized();
  <span class="hljs-keyword">await</span> EasyLocalization.ensureInitialized();

  <span class="hljs-comment">// Load environment variables</span>
  <span class="hljs-keyword">await</span> dotenv.load(fileName: <span class="hljs-string">".env"</span>);
  runApp(EasyLocalization(
    supportedLocales: <span class="hljs-keyword">const</span> [
      Locale(<span class="hljs-string">'en'</span>),
      Locale(<span class="hljs-string">'fr'</span>, <span class="hljs-string">'FR'</span>),
      Locale(<span class="hljs-string">'ja'</span>, <span class="hljs-string">'JP'</span>)],
    path: <span class="hljs-string">'assets/translations'</span>, <span class="hljs-comment">//</span>
    fallbackLocale: Locale(<span class="hljs-string">'en'</span>),
    child: MyApp(),
  ));
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MultiProvider(
      providers: [
        Provider(create: (_) =&gt; ApiService()),
      ],
      child: MaterialApp(
        title: tr(<span class="hljs-string">'app_description'</span>),
        localizationsDelegates: context.localizationDelegates,
        supportedLocales: context.supportedLocales,
        locale: context.locale,
        initialRoute: <span class="hljs-string">'/home'</span>,
        routes: {
          <span class="hljs-string">'/request'</span>: (context) =&gt; RecipeRequestScreen(),
          <span class="hljs-string">'/login'</span>: (context) =&gt; LoginScreen(),
          <span class="hljs-string">'/register'</span>: (context) =&gt; RegisterScreen(),
          <span class="hljs-string">'/home'</span>: (context) =&gt; HomeScreen(), <span class="hljs-comment">// Implement HomeScreen</span>
        },
      ),
    );
  }
}
</code></pre>
<p>From the code snippet above, the <code>WidgetsFlutterBinding.ensureInitialized()</code> ensures that all Flutter bindings are initialized before any other operations and the <code>EasyLocalization.ensureInitialized()</code> initializes the EasyLocalization package to handle translations.</p>
<p>Load the environment variables with <code>dotenv.load(fileName: ".env")</code> to read variables from the <code>.env</code> file. The <code>runApp</code> function wraps the <code>MyApp</code> widget with the <code>EasyLocalization</code> widget, which is configured to support English (<code>en</code>), French (<code>fr_FR</code>), and Japanese (<code>ja_JP</code>) locales. The path for translation files is set to <code>'assets/translations'</code>, and the fallback locale is set to English.</p>
<p>It also creates the main routes of the recipe application and sets <code>home</code> as the initial route.</p>
<h2 id="heading-add-environment-variables">Add Environment Variables</h2>
<p>You will store configuration data such as API keys, environment-specific URLs (base URL, recipe endpoints, comments endpoints), and other sensitive or configurable data outside your codebase using the <code>flutter_dotenv</code> package you installed earlier. Create an <code>.env</code> file in your root directory and add your environment variables:</p>
<pre><code class="lang-bash">BASE_URL=your-base-url
USERS_ENDPOINT=/auth/<span class="hljs-built_in">local</span>
USERS_ENDPOINT_REG=/auth/<span class="hljs-built_in">local</span>/register
ACCESS_TOKEN=your-api-key
RECIPE_ENDPOINT=/recipes
COMMENT_ENDPOINT=/comments
R_REQUEST_ENDPOINT=/recipe-requests
</code></pre>
<ul>
<li><p><code>BASE_URL</code>: This is the base URL for your Strapi backend server. The <code>/api</code> means that all API endpoints are accessed via this base path. This URL is used to construct full URLs for all API requests by appending specific endpoints to it.</p>
</li>
<li><p><code>USERS_ENDPOINT</code>: This endpoint typically handles login operations where existing users authenticate by submitting their credentials.</p>
</li>
<li><p><code>USERS_ENDPOINT_REG</code>: This is the registration endpoint for new users.</p>
</li>
<li><p><code>ACCESS_TOKEN</code>: This is the API token you created earlier which is used for authenticating API requests.</p>
</li>
<li><p><code>RECIPE_ENDPOINT</code>: This endpoint is used to fetch a list of recipes or a single recipe. You can also use it to post new recipes, or update or delete a recipe.</p>
</li>
<li><p><code>COMMENT_ENDPOINT</code>: This endpoint manages comments related to recipes.</p>
</li>
<li><p><code>R_REQUEST_ENDPOINT</code>: This endpoint manages requests related to recipes.</p>
</li>
</ul>
<h2 id="heading-create-models-1">Create Models</h2>
<p>Here you will create the different models of the app. You can create all the models in a single file or create them in individual files. In this tutorial, we’ll create all the models in a single file which is <code>lib/models/recipe.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;

<span class="hljs-comment">// models recipe_ request</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeRequest</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Description&gt; description

  RecipeRequest({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.description,
  });

  <span class="hljs-keyword">factory</span> RecipeRequest.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">var</span> attr = json[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-keyword">var</span> attributes = json[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-built_in">List</span>&lt;Description&gt; descriptionList = (attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [])
        .map((desc) =&gt; Description.fromJson(desc)).toList();

    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Parsed Recipe: <span class="hljs-subst">${json[<span class="hljs-string">'id'</span>]}</span> - Descriptions: <span class="hljs-subst">${descriptionList.length}</span>"</span>);

    <span class="hljs-keyword">return</span> RecipeRequest(
      id: json[<span class="hljs-string">'id'</span>] ?? <span class="hljs-number">0</span>,
      title: attr[<span class="hljs-string">'title'</span>] ?? <span class="hljs-string">'No title'</span>,
      description: descriptionList,
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'title'</span>: title,
      <span class="hljs-string">'description'</span>: description.map((desc) =&gt; desc.toJson()).toList(),
      <span class="hljs-comment">// 'id': id</span>
    };
  }
}

<span class="hljs-comment">// step model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Step</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;TextContent&gt; children;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int?</span> level;

  Step({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.children, <span class="hljs-keyword">this</span>.level});

  <span class="hljs-keyword">factory</span> Step.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">var</span> childrenList = json[<span class="hljs-string">'children'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [];
    <span class="hljs-built_in">List</span>&lt;TextContent&gt; parsedChildren = childrenList.map((child) =&gt; TextContent.fromJson(child)).toList();
    <span class="hljs-keyword">return</span> Step(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      children: parsedChildren,
      level: json[<span class="hljs-string">'level'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'children'</span>: children.map((child) =&gt; child.toJson()).toList(),
      <span class="hljs-string">'level'</span>: level,
    };
  }
}

<span class="hljs-comment">// description model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Description</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;TextContent&gt; children;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int?</span> level;

  Description({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.children, <span class="hljs-keyword">this</span>.level});

  <span class="hljs-keyword">factory</span> Description.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">var</span> childrenList = json[<span class="hljs-string">'children'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List?</span> ?? [];
    <span class="hljs-built_in">List</span>&lt;TextContent&gt; parsedChildren = childrenList.map((child) =&gt; TextContent.fromJson(child)).toList();
    <span class="hljs-keyword">return</span> Description(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      children: parsedChildren,
      level: json[<span class="hljs-string">'level'</span>],
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'children'</span>: children.map((child) =&gt; child.toJson()).toList(),
      <span class="hljs-string">'level'</span>: level,
    };
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TextContent</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> type;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> text;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool?</span> bold;

  TextContent({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.type, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.text, <span class="hljs-keyword">this</span>.bold});

  <span class="hljs-keyword">factory</span> TextContent.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">return</span> TextContent(
      type: json[<span class="hljs-string">'type'</span>] ?? <span class="hljs-string">''</span>,
      text: json[<span class="hljs-string">'text'</span>] ?? <span class="hljs-string">''</span>,
      bold: json[<span class="hljs-string">'bold'</span>] ?? <span class="hljs-keyword">false</span>,
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'type'</span>: type,
      <span class="hljs-string">'text'</span>: text,
      <span class="hljs-string">'bold'</span>: bold,
    };
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comment</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> content;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> author;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> createdAt;

  Comment({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.content,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.author,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.createdAt,
  });

  <span class="hljs-keyword">factory</span> Comment.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">var</span> attributes = json[<span class="hljs-string">'attributes'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; ?? {};
    <span class="hljs-keyword">var</span> authorData = attributes[<span class="hljs-string">'comment_author'</span>]?[<span class="hljs-string">'data'</span>]?[<span class="hljs-string">'attributes'</span>] ?? {};
    <span class="hljs-keyword">return</span> Comment(
      content: attributes[<span class="hljs-string">'content'</span>] ?? <span class="hljs-string">'No content'</span>,
      author: authorData[<span class="hljs-string">'username'</span>] ?? <span class="hljs-string">'Unknown'</span>,
      createdAt: <span class="hljs-built_in">DateTime</span>.parse(attributes[<span class="hljs-string">'createdAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toString()),
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'content'</span>: content,
      <span class="hljs-string">'author'</span>: author,
      <span class="hljs-string">'createdAt'</span>: createdAt.toIso8601String(),
    };
  }
}

<span class="hljs-comment">//recipe model</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Recipe</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> id;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Description&gt; description;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> ingredients;
  <span class="hljs-keyword">late</span> <span class="hljs-built_in">int</span> likes;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> createdAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> updatedAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">DateTime</span> publishedAt;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Step&gt; steps;
  <span class="hljs-keyword">late</span> <span class="hljs-built_in">int</span> commentCount;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Comment&gt; comments;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> coverImageUrl;

  Recipe({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.id,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.description,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.ingredients,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.likes,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.createdAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.updatedAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.publishedAt,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.steps,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.commentCount,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.comments,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.coverImageUrl
  });

  <span class="hljs-keyword">factory</span> Recipe.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">var</span> attr = json[<span class="hljs-string">'attributes'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; ?? {};

    <span class="hljs-comment">// Parse descriptions</span>
    <span class="hljs-built_in">List</span>&lt;Description&gt; descriptionList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'description'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      descriptionList = (attr[<span class="hljs-string">'description'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((desc) =&gt; Description.fromJson(desc)).toList();
    }

    <span class="hljs-comment">// Parse steps</span>
    <span class="hljs-built_in">List</span>&lt;Step&gt; stepsList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'steps'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'steps'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      stepsList = (attr[<span class="hljs-string">'steps'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((step) =&gt; Step.fromJson(step)).toList();
    }

    <span class="hljs-comment">// Parse comments</span>
    <span class="hljs-built_in">List</span>&lt;Comment&gt; commentList = [];
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'comments'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] <span class="hljs-keyword">is</span> <span class="hljs-built_in">List</span>) {
      commentList = (attr[<span class="hljs-string">'comments'</span>][<span class="hljs-string">'data'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">List</span>).map((comment) =&gt; Comment.fromJson(comment)).toList();
    }

    <span class="hljs-comment">// var attr = json['attributes'] as Map&lt;String, dynamic&gt;;</span>
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> baseUrl = dotenv.env[<span class="hljs-string">'BASE_URL'</span>]!;

    <span class="hljs-comment">// Ensure image URL is correctly prefixed</span>
    <span class="hljs-built_in">String</span> coverImageUrl = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">if</span> (attr[<span class="hljs-string">'cover'</span>] != <span class="hljs-keyword">null</span> &amp;&amp; attr[<span class="hljs-string">'cover'</span>][<span class="hljs-string">'data'</span>] != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">var</span> imageUrl = attr[<span class="hljs-string">'cover'</span>][<span class="hljs-string">'data'</span>][<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'url'</span>];
      coverImageUrl = imageUrl.startsWith(<span class="hljs-string">'http'</span>)
          ? imageUrl
          : baseUrl + imageUrl; 
    }

    <span class="hljs-keyword">return</span> Recipe(
        id: json[<span class="hljs-string">'id'</span>] ?? <span class="hljs-number">0</span>,
        title: attr[<span class="hljs-string">'title'</span>] ?? <span class="hljs-string">'No title'</span>,
        description: descriptionList,
        ingredients: attr[<span class="hljs-string">'ingredients'</span>] ?? <span class="hljs-string">'No ingredients'</span>,
        likes: attr[<span class="hljs-string">'likes'</span>] ?? <span class="hljs-number">0</span>,
        createdAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'createdAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        updatedAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'updatedAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        publishedAt: <span class="hljs-built_in">DateTime</span>.tryParse(attr[<span class="hljs-string">'publishedAt'</span>] ?? <span class="hljs-built_in">DateTime</span>.now().toIso8601String()) ?? <span class="hljs-built_in">DateTime</span>.now(),
        steps: stepsList,
        commentCount: commentList.length,
        comments: commentList,
        coverImageUrl: coverImageUrl
    );
  }

  <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; toJson() {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'id'</span>: id,
      <span class="hljs-string">'title'</span>: title,
      <span class="hljs-string">'description'</span>: description.map((desc) =&gt; desc.toJson()).toList(),
      <span class="hljs-string">'ingredients'</span>: ingredients,
      <span class="hljs-string">'likes'</span>: likes,
      <span class="hljs-string">'createdAt'</span>: createdAt.toIso8601String(),
      <span class="hljs-string">'updatedAt'</span>: updatedAt.toIso8601String(),
      <span class="hljs-string">'publishedAt'</span>: publishedAt.toIso8601String(),
      <span class="hljs-string">'steps'</span>: steps.map((step) =&gt; step.toJson()).toList(),
      <span class="hljs-string">'commentCount'</span>: commentCount,
      <span class="hljs-string">'comments'</span>: comments.map((comment) =&gt; comment.toJson()).toList(),
      <span class="hljs-string">'cover'</span>: coverImageUrl
    };
  }
}
</code></pre>
<p>Let’s go over this code piece by piece, as it’s a lot:</p>
<h3 id="heading-1-reciperequest">1. <strong>RecipeRequest</strong></h3>
<p>The <code>RecipeRequest</code> class represents the class that allows a user to request a recipe. It has three properties (<code>id</code>, <code>title</code>, and a list of <code>Description</code> objects as defined in the Strapi backend) with 2 methods:</p>
<ul>
<li><p><code>fromJson</code>: to convert JSON data into a <code>RecipeRequest</code> object, including parsing a list of descriptions.</p>
</li>
<li><p><code>toJson</code>: to convert a <code>RecipeRequest</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-2-step">2. <strong>Step</strong></h3>
<p>Represents the cooking steps in a recipe. It contains a list of <code>Textcontent</code> objects, and each Step object has a type, level, and children as it is a richtext type. It also has two methods:</p>
<ul>
<li><p><code>fromJson</code>: to parse JSON to create a <code>Step</code> object.</p>
</li>
<li><p><code>toJson</code>: to convert a <code>Step</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-3-description">3. <strong>Description</strong></h3>
<p>This class also contains a list of <code>TextContent</code> objects (<code>children</code>). Each <code>Description</code> object also has a <code>type</code> and an optional <code>level</code> to indicate hierarchical structure. It has two methods, too:</p>
<ul>
<li><p><code>fromJson</code>: to convert JSON into a <code>Description</code> object.</p>
</li>
<li><p><code>toJson</code>: to serialise a <code>Description</code> object to JSON.</p>
</li>
</ul>
<h3 id="heading-4-textcontent">4. <strong>TextContent</strong></h3>
<p>This class is designed to represent individual pieces of text within larger structures. Each <code>TextContent</code> object can contain a string of text (<code>text</code>), the type of text (<code>type</code>), and an optional boolean to indicate whether the text is bold (<code>bold</code>)</p>
<ul>
<li><p><code>fromJson</code>: Parses JSON into a <code>TextContent</code> object.</p>
</li>
<li><p><code>toJson</code>: Converts a <code>TextContent</code> object back to JSON.</p>
</li>
</ul>
<h3 id="heading-5-comment">5. <strong>Comment</strong></h3>
<p>As the name indicates, this represents a comment written by a use. It has three properties: the comment <code>content</code>, <code>author</code>, and <code>createdAt</code>. Like others, it also includes two methods:</p>
<ul>
<li><p><code>fromJson</code>: to extract and construct a <code>Comment</code> object from JSON, including parsing author data.</p>
</li>
<li><p><code>toJson</code>: to serializes a <code>Comment</code> object to JSON.</p>
</li>
</ul>
<h3 id="heading-6-recipe">6. <strong>Recipe</strong></h3>
<p>Finally, there is the <code>Recipe</code> class which is the main recipe object. It contains various details about a recipe, including id, title, descriptions, ingredients, likes, timestamps, steps, comment count, comment list, and a cover image URL. We have the:</p>
<ul>
<li><p><code>fromJson</code>: to build a <code>Recipe</code> object from JSON data. This includes parsing lists of descriptions, steps, and comments. It also adjusts the image URL to ensure it is absolute.</p>
</li>
<li><p><code>toJson</code>: to convert the <code>Recipe</code> object to JSON format.</p>
</li>
</ul>
<p>As you can see, each class is designed to handle specific parts of the recipe data, with <code>fromJson</code> methods to parse JSON into Dart objects and <code>toJson</code> methods to serialize Dart objects back to JSON.</p>
<h2 id="heading-create-services">Create Services</h2>
<p>Now that your environment variables are set up, you can create different services for communicating with the server. In your <code>lib/utils/server.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:convert'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:http/http.dart'</span> <span class="hljs-keyword">as</span> http;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiService</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> baseUrl = dotenv.env[<span class="hljs-string">'BASE_URL'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> registerEndpoint = dotenv.env[<span class="hljs-string">'USERS_ENDPOINT_REG'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> loginEndpoint = dotenv.env[<span class="hljs-string">'USERS_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> accessToken = dotenv.env[<span class="hljs-string">'ACCESS_TOKEN'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> recipeEndpoint = dotenv.env[<span class="hljs-string">'RECIPE_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> commentEndpoint = dotenv.env[<span class="hljs-string">'COMMENT_ENDPOINT'</span>]!;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> requestEndpoint = dotenv.env[<span class="hljs-string">'R_REQUEST_ENDPOINT'</span>]!;

  <span class="hljs-comment">// Helper method to get headers with optional JWT token</span>
  Future&lt;<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt;&gt; _getHeaders({<span class="hljs-built_in">bool</span> includeJwt = <span class="hljs-keyword">false</span>}) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> headers = {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">"Bearer <span class="hljs-subst">$accessToken</span>"</span>,
    };
    <span class="hljs-keyword">if</span> (includeJwt) {
      <span class="hljs-keyword">final</span> jwt = <span class="hljs-keyword">await</span> getJwt();
      <span class="hljs-keyword">if</span> (jwt != <span class="hljs-keyword">null</span>) {
        headers[<span class="hljs-string">"Authorization"</span>] = <span class="hljs-string">"Bearer <span class="hljs-subst">$jwt</span>"</span>;
      }
    }
    <span class="hljs-keyword">return</span> headers;
  }

  <span class="hljs-comment">// Get JWT</span>
  Future&lt;<span class="hljs-built_in">String?</span>&gt; getJwt() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">return</span> prefs.getString(<span class="hljs-string">'jwt'</span>);
  }

  <span class="hljs-comment">// Set JWT</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; setJwt(<span class="hljs-built_in">String</span> jwt) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">await</span> prefs.setString(<span class="hljs-string">'jwt'</span>, jwt);
  }

  <span class="hljs-comment">// Remove JWT</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; removeJwt() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">await</span> prefs.remove(<span class="hljs-string">'jwt'</span>);
  }

  <span class="hljs-comment">// Set User Data</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; setUserData(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; data) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">await</span> prefs.setString(<span class="hljs-string">'userId'</span>, data[<span class="hljs-string">'user'</span>][<span class="hljs-string">'id'</span>].toString());
    <span class="hljs-keyword">await</span> prefs.setString(<span class="hljs-string">'username'</span>, data[<span class="hljs-string">'user'</span>][<span class="hljs-string">'username'</span>]);
  }

  <span class="hljs-comment">// Remove User Data</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; removeUserData() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">await</span> prefs.remove(<span class="hljs-string">'userId'</span>);
    <span class="hljs-keyword">await</span> prefs.remove(<span class="hljs-string">'username'</span>);
  }

  <span class="hljs-comment">// User Registration</span>
  Future&lt;http.Response&gt; register(<span class="hljs-built_in">String</span> username, <span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$registerEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"username"</span>: username,
          <span class="hljs-string">"email"</span>: email,
          <span class="hljs-string">"password"</span>: password,
        }),
      );
      <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error registering user: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// User Login</span>
  Future&lt;http.Response&gt; login(<span class="hljs-built_in">String</span> email, <span class="hljs-built_in">String</span> password) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$loginEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"identifier"</span>: email,
          <span class="hljs-string">"password"</span>: password,
        }),
      );

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">final</span> data = json.decode(response.body);
        <span class="hljs-keyword">await</span> setJwt(data[<span class="hljs-string">'jwt'</span>]);
        <span class="hljs-keyword">await</span> setUserData(data);
      }

      <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error logging in user: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// User Logout</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> removeJwt();
    <span class="hljs-keyword">await</span> removeUserData();
  }

  <span class="hljs-comment">// Fetch Recipes</span>
  Future&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt; fetchRecipes(BuildContext context) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> localeCode = context.locale.toString().replaceAll(<span class="hljs-string">'_'</span>, <span class="hljs-string">'-'</span>);
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> lang = localeCode == <span class="hljs-string">'en'</span> ? <span class="hljs-string">'en'</span> : localeCode;
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>?locale=<span class="hljs-subst">$lang</span>&amp;populate=*'</span>);
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(url);

    <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
      <span class="hljs-keyword">var</span> jsonResponse = jsonDecode(response.body);
      <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; dataList = jsonResponse[<span class="hljs-string">'data'</span>];
      <span class="hljs-built_in">List</span>&lt;Recipe&gt; recipes = [];

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> dataList) {
        <span class="hljs-keyword">try</span> {
          recipes.add(Recipe.fromJson(item));
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Failed to parse item: <span class="hljs-subst">$e</span>'</span>);
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Item data: <span class="hljs-subst">$item</span>'</span>);
        }
      }

      <span class="hljs-keyword">return</span> recipes;
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to load recipes: HTTP <span class="hljs-subst">${response.statusCode}</span>'</span>);
    }
  }

  <span class="hljs-comment">// Fetch Comments</span>
    Future&lt;<span class="hljs-built_in">List</span>&lt;Comment&gt;&gt; fetchComments(<span class="hljs-built_in">int</span> recipeId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$commentEndpoint</span>?filters[recipe][id][\$eq]=<span class="hljs-subst">$recipeId</span>&amp;populate=comment_author'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(url, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response fetch status: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response fetch body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> jsonData = jsonDecode(response.body);
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Parsed JSON: <span class="hljs-subst">$jsonData</span>"</span>);

        <span class="hljs-keyword">if</span> (jsonData != <span class="hljs-keyword">null</span> &amp;&amp; jsonData.containsKey(<span class="hljs-string">'data'</span>)) {
          <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; data = jsonData[<span class="hljs-string">'data'</span>];
          <span class="hljs-keyword">return</span> data.map&lt;Comment&gt;((json) {
            <span class="hljs-keyword">if</span> (json == <span class="hljs-keyword">null</span> || json[<span class="hljs-string">'attributes'</span>] == <span class="hljs-keyword">null</span>) {
              <span class="hljs-built_in">print</span>(<span class="hljs-string">'json or json[\'attributes\'] is null'</span>);
              <span class="hljs-keyword">return</span> Comment(content: <span class="hljs-string">'Invalid'</span>, author: <span class="hljs-string">'Invalid'</span>, createdAt: <span class="hljs-built_in">DateTime</span>.now());
            }
            <span class="hljs-keyword">return</span> Comment.fromJson(json);
          }).toList();
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-built_in">print</span>(<span class="hljs-string">'Data field is missing or null in the response'</span>);
          <span class="hljs-keyword">return</span> [];
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Failed to load comments with status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
        <span class="hljs-keyword">return</span> [];
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error server fetching comments: <span class="hljs-subst">$e</span>'</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error fetching comments: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  Future&lt;Comment&gt; postComment(<span class="hljs-built_in">String</span> content, <span class="hljs-built_in">int</span> recipeId, <span class="hljs-built_in">String</span> authorId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$commentEndpoint</span>?populate=comment_author'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(),
        body: json.encode({
          <span class="hljs-string">"data"</span>: {
            <span class="hljs-string">"content"</span>: content,
            <span class="hljs-string">"recipe"</span>: recipeId,
            <span class="hljs-string">"comment_author"</span>: authorId,
          },
        }),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Post comment response status: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Post comment response body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span> || response.statusCode == <span class="hljs-number">201</span>) {
        <span class="hljs-keyword">var</span> jsonData = jsonDecode(response.body);
        <span class="hljs-keyword">return</span> Comment.fromJson(jsonData[<span class="hljs-string">'data'</span>]);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to post comment'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error posting comment: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; updateCommentCount(<span class="hljs-built_in">int</span> recipeId, {<span class="hljs-keyword">required</span> <span class="hljs-built_in">bool</span> increment}) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> recipeUrl = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>/<span class="hljs-subst">$recipeId</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Fetch the current recipe data</span>
      <span class="hljs-keyword">final</span> recipeResponse = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(recipeUrl, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Fetch recipe response status: <span class="hljs-subst">${recipeResponse.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Fetch recipe response body: <span class="hljs-subst">${recipeResponse.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (recipeResponse.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> recipeData = jsonDecode(recipeResponse.body)[<span class="hljs-string">'data'</span>];
        <span class="hljs-built_in">int</span> currentComments = recipeData[<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'comments'</span>] ?? <span class="hljs-number">0</span>;
        <span class="hljs-built_in">int</span> updatedComments = increment ? currentComments + <span class="hljs-number">1</span> : currentComments - <span class="hljs-number">1</span>;

        <span class="hljs-comment">// Ensure updatedComments is not negative</span>
        <span class="hljs-keyword">if</span> (updatedComments &lt; <span class="hljs-number">0</span>) {
          updatedComments = <span class="hljs-number">0</span>;
        }

        <span class="hljs-comment">// Update the recipe with the new comment count</span>
        <span class="hljs-keyword">final</span> updateResponse = <span class="hljs-keyword">await</span> http.put(
          recipeUrl,
          headers: <span class="hljs-keyword">await</span> _getHeaders(),
          body: json.encode({
            <span class="hljs-string">"data"</span>: {
              <span class="hljs-string">"comments"</span>: updatedComments,
            },
          }),
        );

        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Update recipe response status: <span class="hljs-subst">${updateResponse.statusCode}</span>'</span>);
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Update recipe response body: <span class="hljs-subst">${updateResponse.body}</span>'</span>);

        <span class="hljs-keyword">if</span> (updateResponse.statusCode != <span class="hljs-number">200</span>) {
          <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to update comment count'</span>);
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to fetch recipe data'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error updating comment count: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error updating comment count: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  <span class="hljs-comment">// Like Recipe</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; likeRecipe(<span class="hljs-built_in">int</span> recipeId) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> recipeUrl = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$recipeEndpoint</span>/<span class="hljs-subst">$recipeId</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Fetch the current recipe data</span>
      <span class="hljs-keyword">final</span> recipeResponse = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(recipeUrl, headers: <span class="hljs-keyword">await</span> _getHeaders());
      <span class="hljs-keyword">if</span> (recipeResponse.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> recipeData = jsonDecode(recipeResponse.body)[<span class="hljs-string">'data'</span>];
        <span class="hljs-built_in">int</span> currentLikes = recipeData[<span class="hljs-string">'attributes'</span>][<span class="hljs-string">'likes'</span>] ?? <span class="hljs-number">0</span>;
        <span class="hljs-built_in">int</span> updatedLikes = currentLikes + <span class="hljs-number">1</span>;

        <span class="hljs-comment">// Update the recipe with the new likes count</span>
        <span class="hljs-keyword">final</span> updateResponse = <span class="hljs-keyword">await</span> http.put(
          recipeUrl,
          headers: <span class="hljs-keyword">await</span> _getHeaders(),
          body: json.encode({
            <span class="hljs-string">"data"</span>: {
              <span class="hljs-string">"likes"</span>: updatedLikes,
            },
          }),
        );

        <span class="hljs-keyword">if</span> (updateResponse.statusCode != <span class="hljs-number">200</span>) {
          <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to update likes count'</span>);
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to fetch recipe data'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error liking recipe: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Error liking recipe: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  <span class="hljs-comment">// Submit Recipe Request</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; submitRecipeRequest(RecipeRequest r_request) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$requestEndpoint</span>'</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.post(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(includeJwt: <span class="hljs-keyword">true</span>),
        body: jsonEncode({
          <span class="hljs-string">'data'</span>: r_request.toJson(), <span class="hljs-comment">// Wrap the request in a 'data' object</span>
        }),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response body: <span class="hljs-subst">${response.body}</span>'</span>);
      <span class="hljs-keyword">if</span> (response.statusCode != <span class="hljs-number">200</span> &amp;&amp; response.statusCode != <span class="hljs-number">201</span>) {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to submit recipe request'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error submitting recipe request: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }

  <span class="hljs-comment">// Fetch User Requested Recipes</span>
  Future&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt; fetchUserRequestedRecipes() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> url = <span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'<span class="hljs-subst">$baseUrl</span><span class="hljs-subst">$requestEndpoint</span>'</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(
        url,
        headers: <span class="hljs-keyword">await</span> _getHeaders(includeJwt: <span class="hljs-keyword">true</span>),
      );
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response status code: <span class="hljs-subst">${response.statusCode}</span>'</span>);
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Response body: <span class="hljs-subst">${response.body}</span>'</span>);

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-keyword">var</span> jsonResponse = jsonDecode(response.body);
        <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">dynamic</span>&gt; data = jsonResponse[<span class="hljs-string">'data'</span>];
        <span class="hljs-keyword">return</span> data.map((json) =&gt; RecipeRequest.fromJson(json)).toList();
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Failed to load user requested recipes'</span>);
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error fetching user requested recipes: <span class="hljs-subst">$e</span>"</span>);
      <span class="hljs-keyword">rethrow</span>;
    }
  }
}
</code></pre>
<p>The <code>ApiService</code> class from the code above is a utility for handling various operations related to user authentication and data fetching from a backend server. This service uses HTTP requests to communicate with the Strapi server.</p>
<p>There are four main entities:</p>
<h3 id="heading-1-class-variables">1. Class Variables</h3>
<ul>
<li><p><code>baseUrl</code> is the base URL.</p>
</li>
<li><p><code>registerEndpoint</code>, <code>loginEndpoint</code>, <code>recipeEndpoint</code>, <code>commentEndpoint</code>, <code>requestEndpoint</code> are the specific endpoints for registration, login, recipes, comments, and requests.</p>
</li>
<li><p><code>accessToken</code> is the token used for API authentication.</p>
</li>
</ul>
<h3 id="heading-2-helper-methods">2. Helper Methods</h3>
<ul>
<li><p><code>_getHeaders</code> prepares the headers for HTTP requests and it optionally includes a JWT token if <code>includeJwt</code> is true.</p>
</li>
<li><p><code>getJwt</code> retrieves the JWT token from shared preferences.</p>
</li>
<li><p><code>setJwt</code> and <code>setUserData</code> store the JWT token and user data (ID and username) in shared preferences once the user logs in.</p>
</li>
<li><p><code>removeJwt</code> and <code>removeUserData</code> remove the JWT token and user data from shared preferences, respectively, and log the user out.</p>
</li>
</ul>
<h3 id="heading-3-user-operations">3. User Operations</h3>
<ul>
<li><p><code>register</code> registers a new user with the given username, email, and password. It sends a POST request to the registration endpoint with the user details.</p>
</li>
<li><p><code>login</code> logs in a user with the given email and password. If successful, it stores the received JWT token and user data.</p>
</li>
<li><p><code>logout</code> logs out the user by removing the JWT token and user data from shared preferences.</p>
</li>
</ul>
<h3 id="heading-4-data-fetching-and-manipulation">4. Data Fetching and Manipulation</h3>
<ul>
<li><p><code>fetchRecipes</code> fetches a list of recipes based on the current locale (language) from the backend. It handles parsing the JSON response into a list of <code>Recipe</code> objects.</p>
</li>
<li><p><code>fetchComments</code> fetches comments for a specific recipe by its ID. It populates the <code>comment_author</code> field and returns a list of <code>Comment</code> objects.</p>
</li>
<li><p><code>postComment</code> posts a new comment on a specific recipe. It sends the comment content, recipe ID, and author ID to the backend.</p>
</li>
<li><p><code>updateCommentCount</code> updates the comment count for a specific recipe. It first fetches the current count, modifies it, and then updates it on the backend.</p>
</li>
<li><p><code>likeRecipe</code>: Increments the like count for a specific recipe by fetching the current count, adding one, and updating the backend.</p>
</li>
<li><p><code>submitRecipeRequest</code> submits a new recipe request to the backend. It sends the request data wrapped in a <code>data</code> object.</p>
</li>
<li><p><code>fetchUserRequestedRecipes</code> fetches a list of recipes requested by a specific user from the backend.</p>
</li>
</ul>
<h2 id="heading-authorization-and-authentication">Authorization and Authentication</h2>
<p>Authorization is what allows a user to access a particular resource and determines if a user can perform certain actions within the application like commenting on a recipe, liking a recipe, or requesting a recipe.</p>
<p>On the other hand, authentication is the process of validating and verifying a user.</p>
<p>There are many Authorization and Authentication methods, but in this tutorial we’ll use password-based authentication and an API Key for authorization.</p>
<h3 id="heading-registration">Registration</h3>
<p>In the <code>lib/screen/signUp.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'login.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RegisterScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _RegisterScreenState createState() =&gt; _RegisterScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RegisterScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RegisterScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> TextEditingController usernameController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController emailController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController passwordController = TextEditingController();
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    usernameController.dispose();
    emailController.dispose();
    passwordController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _register() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = <span class="hljs-keyword">true</span>;
      });

      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> Provider.of&lt;ApiService&gt;(context, listen: <span class="hljs-keyword">false</span>)
          .register(usernameController.text, emailController.text, passwordController.text);

      setState(() {
        _isLoading = <span class="hljs-keyword">false</span>;
      });

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-comment">// Navigate to the login screen after successful registration</span>
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (_) =&gt; LoginScreen()),
        );
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Handle error</span>
        showDialog(
          context: context,
          builder: (context) =&gt; AlertDialog(
            title: Text(tr(<span class="hljs-string">'register_fail'</span>)),
            content: Text(tr(<span class="hljs-string">'register_error'</span>)),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text(tr(<span class="hljs-string">'ok'</span>)),
              ),
            ],
          ),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(tr(<span class="hljs-string">'register'</span>))),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: usernameController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'username'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'username_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: emailController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'email'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_required'</span>);
                  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[^@]+@[^@]+\.[^@]+'</span>).hasMatch(value)) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_invalid'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: passwordController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'password'</span>)),
                obscureText: <span class="hljs-keyword">true</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'password_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              _isLoading
                  ? CircularProgressIndicator()
                  : ElevatedButton(
                onPressed: _register,
                child: Text(tr(<span class="hljs-string">'register'</span>)),
              ),
              TextButton(
                onPressed: () {
                  <span class="hljs-comment">// Navigate to the login screen</span>
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(builder: (_) =&gt; LoginScreen()),
                  );
                },
                child: Text(
                  tr(<span class="hljs-string">"have_account"</span>),
                  style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">16</span>),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>This code provides a user-friendly registration interface for the recipe application. The <code>RegisterScreen</code> class is a stateful widget that manages the registration process.</p>
<p>The <code>_register</code> method validates the form and calls the <code>register</code> method from the <code>ApiService</code>. If the registration is successful (indicated by a 200 HTTP status code), it redirects to the login screen. If it fails, an error dialog is displayed with a message.</p>
<p>The code above also employs form validation to ensure that users enter valid information. The username and password fields must not be empty, and the email field must follow a proper email format.</p>
<p>Upon submission, the form displays a loading indicator while the app communicates with the server to register the user.</p>
<p>The form's state is managed using a GlobalKey, and controllers for the text fields are properly disposed of to free up resources when the widget is removed from the tree.</p>
<h3 id="heading-login">Login</h3>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:provider/provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'signUp.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _LoginScreenState createState() =&gt; _LoginScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_LoginScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">LoginScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> TextEditingController emailController = TextEditingController();
  <span class="hljs-keyword">final</span> TextEditingController passwordController = TextEditingController();
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    emailController.dispose();
    passwordController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _login() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = <span class="hljs-keyword">true</span>;
      });

      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> Provider.of&lt;ApiService&gt;(context, listen: <span class="hljs-keyword">false</span>)
          .login(emailController.text, passwordController.text);

      setState(() {
        _isLoading = <span class="hljs-keyword">false</span>;
      });

      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        Navigator.pushReplacementNamed(context, <span class="hljs-string">'/home'</span>);
      } <span class="hljs-keyword">else</span> {
        showDialog(
          context: context,
          builder: (context) =&gt; AlertDialog(
            title: Text(tr(<span class="hljs-string">'login_failed'</span>)),
            content: Text(tr(<span class="hljs-string">'invalid_email_password'</span>)),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text(tr(<span class="hljs-string">'ok'</span>)),
              ),
            ],
          ),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: Text(tr(<span class="hljs-string">'login'</span>))),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: emailController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'email'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_required'</span>);
                  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'^[^@]+@[^@]+\.[^@]+'</span>).hasMatch(value)) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'email_invalid'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: passwordController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'password'</span>)),
                obscureText: <span class="hljs-keyword">true</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'password_required'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              _isLoading
                  ? CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _login,
                      child: Text(tr(<span class="hljs-string">'login'</span>)),
                    ),
              TextButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (_) =&gt; RegisterScreen()),
                  );
                },
                child: Text(
                  tr(<span class="hljs-string">"dont_have_account"</span>),
                  style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">16</span>),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>LoginScreen</code> contains two input fields for the user's email and password, and it validates the inputs before attempting to log in. When the user submits the form, the app checks if the input is valid. If valid, it sets a loading indicator and sends a login request to the backend API.</p>
<p>If the login is successful, the app navigates to the home screen, whereas if the login fails, an alert dialog is displayed to inform the user of the invalid email or password. The form also uses a <code>GlobalKey</code> to manage its state and ensures that the text controllers are properly disposed of when the widget is removed from the tree.</p>
<h2 id="heading-build-app-components">Build App Components</h2>
<h3 id="heading-drawer">Drawer</h3>
<p>The Drawer is a side panel that slides in from the left (by default) and provides navigation options for the user. It’s a great way to organize your app’s sections without crowding the main screen.</p>
<p>In our app, the drawer will include links to the Request recipe screen, Profile, Logout, and languages for authenticated users.</p>
<p>In the <code>lib/components/drawer.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../screens/profile.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../screens/requestRecipe.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomDrawer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _CustomDrawerState createState() =&gt; _CustomDrawerState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_CustomDrawerState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">CustomDrawer</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _username;
  <span class="hljs-built_in">String?</span> _userId;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _checkAuthentication();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>);
      _username = prefs.getString(<span class="hljs-string">'username'</span>);
      _userId = prefs.getString(<span class="hljs-string">'userId'</span>);
    });
  }

  <span class="hljs-keyword">void</span> _navigateToLogin() {
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    <span class="hljs-keyword">await</span> prefs.clear();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _username = <span class="hljs-keyword">null</span>;
      _userId = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-keyword">void</span> _changeLanguage(Locale locale) {
    context.setLocale(locale);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          DrawerHeader(
            decoration: BoxDecoration(
              color: Colors.blue,
            ),
            child: Text(
              _isAuthenticated ? tr(<span class="hljs-string">'hello'</span>, namedArgs: {<span class="hljs-string">'username'</span>: _username ?? <span class="hljs-string">''</span>}) : tr(<span class="hljs-string">'welcome'</span>),
              style: TextStyle(
                color: Colors.white,
                fontSize: <span class="hljs-number">24</span>,
              ),
            ),
          ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: Icon(Icons.request_page),
              title:Text(tr(<span class="hljs-string">'request_recipe'</span>)),
              onTap: () {

                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) =&gt; RecipeRequestScreen()),

                );
              },
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: <span class="hljs-keyword">const</span> Icon(Icons.person),
              title: Text(tr(<span class="hljs-string">'profile'</span>)),
              onTap: () {
                <span class="hljs-keyword">if</span> (_userId != <span class="hljs-keyword">null</span>) {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) =&gt; ProfileScreen()),
                  );
                }
              },
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            ListTile(
              leading: Icon(Icons.logout),
              title: Text(tr(<span class="hljs-string">'logout'</span>)),
              onTap: _logout,

            )
          <span class="hljs-keyword">else</span>
            ListTile(
              leading: Icon(Icons.login),
              title: Text(tr(<span class="hljs-string">'login'</span>)),
              onTap: _navigateToLogin,
            ),
          Divider(),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/en-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'english'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'en'</span>));
    },
          ),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/fr-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'french'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'fr'</span>, <span class="hljs-string">'FR'</span>));
            },
          ),
          ListTile(
            leading: SizedBox(
              width: <span class="hljs-number">24.0</span>,
              height: <span class="hljs-number">24.0</span>,
              child: Image.asset(
                <span class="hljs-string">'assets/images/ja-flag.jpg'</span>,
              ),
            ),
            title: Text(tr(<span class="hljs-string">'japanese'</span>)),
            onTap: () {
              Navigator.pop(context);
              _changeLanguage(Locale(<span class="hljs-string">'ja'</span>, <span class="hljs-string">'JP'</span>));
            },
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The <code>CustomDrawer</code> gives users access to different parts of the app and lets them switch languages. It updates its content based on the user's login status. Logged-in users see options like “Request a Recipe,” “Profile,” and “Logout,” while guests only see a “Login” option. It personalizes the user experience by greeting logged-in users with their username.</p>
<p>It also includes a language switcher with flag icons for English, French, and Japanese, powered by the <code>easy_localization</code> package. This allows users to change the app’s language instantly.</p>
<p>On startup, the drawer checks the user's authentication status using <code>SharedPreferences</code> and adjusts the UI accordingly. Navigation is handled with <code>Navigator</code>, enabling smooth transitions to different screens based on the selected menu item.</p>
<h3 id="heading-appbar">AppBar</h3>
<p>The AppBar is the top bar of your app’s screen. It typically contains the app’s title, a back button (if needed), and sometimes actions like search, settings, or a language toggle. In our multilingual recipe app, we’ll use the <code>AppBar</code> to show the current page title and allow easy navigation through the drawer.</p>
<p>In the <code>lib/components/appBar.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-comment">/// <span class="markdown">A customizable AppBar for the Recipe application.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// This AppBar allows for setting a title, actions, a leading widget, </span></span>
<span class="hljs-comment">/// <span class="markdown">centering the title, background color, and elevation.</span></span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeBar</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">PreferredSizeWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Widget&gt;? actions;
  <span class="hljs-keyword">final</span> Widget? leading;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> centerTitle;
  <span class="hljs-keyword">final</span> Color? backgroundColor;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> elevation;

  <span class="hljs-keyword">const</span> RecipeBar({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">this</span>.actions,
    <span class="hljs-keyword">this</span>.leading,
    <span class="hljs-keyword">this</span>.centerTitle = <span class="hljs-keyword">true</span>,
    <span class="hljs-keyword">this</span>.backgroundColor,
    <span class="hljs-keyword">this</span>.elevation = <span class="hljs-number">4.0</span>,
    Key? key,
  }) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> AppBar(
      title: Text(title),
      actions: actions,
      leading: leading,
      centerTitle: centerTitle,
      backgroundColor: backgroundColor,
      elevation: elevation,
    );
  }

  <span class="hljs-meta">@override</span>
  Size <span class="hljs-keyword">get</span> preferredSize =&gt; <span class="hljs-keyword">const</span> Size.fromHeight(kToolbarHeight);
}
</code></pre>
<p>The AppBar uses a <code>StatelessWidget</code> since it does not manage any state that changes over time. It implements the <code>PreferredSizeWidget</code> interface, which is necessary for AppBar customization in Flutter.</p>
<p>The constructor of the <code>RecipeBar</code> class takes several parameters to customize the AppBar. The <code>title</code> parameter is required, while the others are optional with default values. The <code>actions</code> parameter allows adding widgets like buttons for login, language switching, or simply navigating to another screen of the app.</p>
<p>In the <code>build</code> method, the AppBar is constructed using the provided parameters. The <code>preferredSize</code> getter returns the preferred height of the AppBar, which is set to the standard toolbar height using <code>kToolbarHeight</code>. This class provides a flexible and reusable AppBar component for the Recipe application, enabling easy customization and consistent UI design across different screens.</p>
<h2 id="heading-fetch-recipes">Fetch Recipes</h2>
<p>In the <code>lib/screens/home.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../components/drawer.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'detail.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _HomeScreenState createState() =&gt; _HomeScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; </span>{
  <span class="hljs-keyword">late</span> Future&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt; _recipesFuture;
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _username;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _checkAuthentication(); <span class="hljs-comment">// Check authentication state when initializing</span>
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>); <span class="hljs-comment">// Check if JWT token is stored</span>
      _username = prefs.getString(<span class="hljs-string">'username'</span>); <span class="hljs-comment">// Get the logged-in user's username from shared preferences</span>
    });
  }

  <span class="hljs-keyword">void</span> _navigateToLogin() {
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-comment">// Logout method</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> ApiService().logout();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _username = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-built_in">String</span> truncateWithEllipsis(<span class="hljs-built_in">int</span> cutoff, <span class="hljs-built_in">String</span> myString) {
    <span class="hljs-keyword">return</span> (myString.length &lt;= cutoff) ? myString : <span class="hljs-string">'<span class="hljs-subst">${myString.substring(<span class="hljs-number">0</span>, cutoff)}</span>...'</span>;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didChangeDependencies() {
    <span class="hljs-keyword">super</span>.didChangeDependencies();
    <span class="hljs-comment">// Initialize _recipesFuture  after context is available</span>
    _recipesFuture = ApiService().fetchRecipes(context);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'recipe_list'</span>)),
        actions: [
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            Padding(
              padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
              child: Center(
                child: Text(tr(<span class="hljs-string">'hello'</span>, namedArgs: {<span class="hljs-string">'username'</span>: _username ?? <span class="hljs-string">''</span>})),
              ),
            ),
          <span class="hljs-keyword">if</span> (_isAuthenticated)
            IconButton(
              icon: <span class="hljs-keyword">const</span> Icon(Icons.logout),
              onPressed: _logout,
            )
          <span class="hljs-keyword">else</span>
            TextButton(
              onPressed: _navigateToLogin,
              child: Text(
                tr(<span class="hljs-string">'login'</span>),
                style: <span class="hljs-keyword">const</span> TextStyle(color: Colors.white),
              ),
            ),
        ],
      ),
      drawer: CustomDrawer(),
      body: FutureBuilder&lt;<span class="hljs-built_in">List</span>&lt;Recipe&gt;&gt;(
        future: _recipesFuture,
        builder: (context, snapshot) {
          <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">const</span> Center(child: CircularProgressIndicator());
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
            <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error.toString()}</span>'</span>));
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.data == <span class="hljs-keyword">null</span> || snapshot.data!.isEmpty) {
            <span class="hljs-keyword">return</span> Center(child: Text(tr(<span class="hljs-string">'no_recipe'</span>)));
          }

          <span class="hljs-keyword">return</span> ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (context, index) {
              Recipe recipe = snapshot.data![index];
              <span class="hljs-built_in">String</span> fullDescription = recipe.description.isNotEmpty
                  ? recipe.description.map((d) =&gt; d.children.map((t) =&gt; t.text).join(<span class="hljs-string">' '</span>)).join(<span class="hljs-string">'\n'</span>)
                  : tr(<span class="hljs-string">'no_description'</span>);
              <span class="hljs-built_in">String</span> truncatedDescription = truncateWithEllipsis(<span class="hljs-number">100</span>, fullDescription);

              <span class="hljs-built_in">print</span>(<span class="hljs-string">"Recipe Title: <span class="hljs-subst">${recipe.title}</span>"</span>);
              <span class="hljs-built_in">print</span>(<span class="hljs-string">"Full Description: <span class="hljs-subst">$fullDescription</span>"</span>);

              <span class="hljs-keyword">return</span> GestureDetector(
                onTap: () <span class="hljs-keyword">async</span> {
                  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) =&gt; RecipeDetailPage(recipe: recipe),
                    ),
                  );

                  <span class="hljs-keyword">if</span> (result != <span class="hljs-keyword">null</span> &amp;&amp; result <span class="hljs-keyword">is</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">int</span>&gt;) {
                    setState(() {
                      Recipe updatedRecipe = Recipe(
                        id: recipe.id,
                        title: recipe.title,
                        description: recipe.description,
                        ingredients: recipe.ingredients,
                        likes: result[<span class="hljs-string">'likes'</span>]!,
                        createdAt: recipe.createdAt,
                        updatedAt: recipe.updatedAt,
                        publishedAt: recipe.publishedAt,
                        steps: recipe.steps,
                        commentCount: result[<span class="hljs-string">'commentsCount'</span>]!,
                        comments: recipe.comments,
                        coverImageUrl: recipe.coverImageUrl,
                      );
                      snapshot.data![index] = updatedRecipe;
                    });
                  }
                },
                child: Container(
                  margin: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(horizontal: <span class="hljs-number">10</span>, vertical: <span class="hljs-number">8</span>),
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">10</span>),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(<span class="hljs-number">15</span>),
                    border: Border.all(
                      color: <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xff595959</span>),
                      width: <span class="hljs-number">0.5</span>,
                    ),
                  ),
                  child: Row(
                    children: [
                      Container(
                        height: <span class="hljs-number">80</span>,
                        width: <span class="hljs-number">80</span>,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(<span class="hljs-number">15</span>),
                          image: DecorationImage(
                            image: NetworkImage(recipe.coverImageUrl),
                            fit: BoxFit.cover,
                          ),
                        ),
                      ),
                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">10</span>),
                      Expanded(
                        flex: <span class="hljs-number">3</span>,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              recipe.title.toUpperCase(),
                              style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                            ),
                            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">5</span>),
                            Text(
                              truncatedDescription,
                              style: <span class="hljs-keyword">const</span> TextStyle(color: Color(<span class="hljs-number">0xff595959</span>)),
                            ),
                            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">5</span>),
                            Row(
                              children: [
                                Expanded(
                                  child: Row(
                                    children: [
                                      Text(<span class="hljs-string">'<span class="hljs-subst">${recipe.likes}</span>'</span>),
                                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                                      <span class="hljs-keyword">const</span> Icon(Icons.thumb_up, size: <span class="hljs-number">18</span>, color: Colors.redAccent),
                                    ],
                                  ),
                                ),
                                Expanded(
                                  child: Row(
                                    children: [
                                      Text(<span class="hljs-string">'<span class="hljs-subst">${recipe.commentCount}</span>'</span>),
                                      <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                                      <span class="hljs-keyword">const</span> Icon(Icons.comment, size: <span class="hljs-number">18</span>, color: Colors.blue),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}
</code></pre>
<p>The <code>HomeScreen</code> mainly displays a list of recipes. It checks if the user is authenticated by looking for a JWT token in shared preferences and sets the authentication state accordingly. If the user is authenticated, it shows a greeting with their username and provides a logout option in the app bar.</p>
<p>The <code>FutureBuilder</code> to fetch recipes from the <code>ApiService</code>. While the data is being fetched, it shows a loading indicator. Once the data is fetched, it displays the list of recipes. Each recipe card includes the title, truncated description, cover image, and the counts of likes and comments.</p>
<p>When a user taps on a recipe, it navigates to a detailed page for that recipe. If the detailed page updates the recipe's likes or comments, the list updates accordingly without reloading the entire screen.</p>
<h2 id="heading-view-recipe">View Recipe</h2>
<p>In the <code>lib/screens/detail.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeDetailPage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> Recipe recipe;

  <span class="hljs-keyword">const</span> RecipeDetailPage({Key? key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.recipe}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  _RecipeDetailPageState createState() =&gt; _RecipeDetailPageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RecipeDetailPageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RecipeDetailPage</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _commentController = TextEditingController();
  <span class="hljs-built_in">List</span>&lt;Comment&gt; _comments = [];
  <span class="hljs-built_in">bool</span> _isLoading = <span class="hljs-keyword">true</span>;
  <span class="hljs-built_in">bool</span> _isAuthenticated = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String?</span> _userId;
  <span class="hljs-built_in">int</span> _likes = <span class="hljs-number">0</span>;
  <span class="hljs-built_in">int</span> _commentsCount = <span class="hljs-number">0</span>;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _initializePage();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _initializePage() <span class="hljs-keyword">async</span> {
    _checkAuthentication();
    _loadComments();
    _likes = widget.recipe.likes;
    _comments = widget.recipe.comments;
    _commentsCount = widget.recipe.commentCount;
    _commentController.addListener(() =&gt; setState(() {}));
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _commentController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _checkAuthentication() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
    setState(() {
      _isAuthenticated = prefs.containsKey(<span class="hljs-string">'jwt'</span>);
      _userId = prefs.getString(<span class="hljs-string">'userId'</span>);
    });
  }

  <span class="hljs-keyword">void</span> _showError(<span class="hljs-built_in">String</span> message) {
    <span class="hljs-keyword">final</span> snackBar = SnackBar(content: Text(message));
    ScaffoldMessenger.of(context).showSnackBar(snackBar);
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _loadComments() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">var</span> comments = <span class="hljs-keyword">await</span> ApiService().fetchComments(widget.recipe.id);
      setState(() {
        _comments = comments;
        _commentsCount = comments.length;
        _isLoading = <span class="hljs-keyword">false</span>;
      });
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">'Error server fetching comments: <span class="hljs-subst">$e</span>'</span>);
      _showError(<span class="hljs-string">'Failed to load comments: <span class="hljs-subst">$e</span>'</span>);
      setState(() =&gt; _isLoading = <span class="hljs-keyword">false</span>);
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _addComment() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_commentController.text.isNotEmpty &amp;&amp; _userId != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">try</span> {
        Comment newComment = <span class="hljs-keyword">await</span> ApiService().postComment(
            _commentController.text, widget.recipe.id, _userId!);

        setState(() {
          _comments.add(newComment);
          _commentsCount++;
          _commentController.clear();
        });

        <span class="hljs-keyword">await</span> ApiService().updateCommentCount(widget.recipe.id, increment: <span class="hljs-keyword">true</span>);
      } <span class="hljs-keyword">catch</span> (e) {
        log(<span class="hljs-string">"Error posting comment: <span class="hljs-subst">$e</span>"</span>);
        _showError(<span class="hljs-string">'Error posting comment: <span class="hljs-subst">$e</span>'</span>);
      }
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _likeRecipe() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> ApiService().likeRecipe(widget.recipe.id);
      setState(() =&gt; _likes++);
    } <span class="hljs-keyword">catch</span> (e) {
      log(<span class="hljs-string">"Error liking recipe: <span class="hljs-subst">$e</span>"</span>);
      _showError(<span class="hljs-string">'Error liking recipe: <span class="hljs-subst">$e</span>'</span>);
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _logout() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> ApiService().logout();
    setState(() {
      _isAuthenticated = <span class="hljs-keyword">false</span>;
      _userId = <span class="hljs-keyword">null</span>;
    });
    Navigator.pushReplacementNamed(context, <span class="hljs-string">'/login'</span>);
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> WillPopScope(
      onWillPop: () <span class="hljs-keyword">async</span> {
        Navigator.pop(context, {
          <span class="hljs-string">'likes'</span>: _likes,
          <span class="hljs-string">'commentsCount'</span>: _commentsCount,
        });
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(widget.recipe.title),
          actions: [
            <span class="hljs-keyword">if</span> (_isAuthenticated)
              IconButton(
                icon: <span class="hljs-keyword">const</span> Icon(Icons.logout),
                onPressed: _logout,
              ),
          ],
        ),
        body: SingleChildScrollView(
          child: Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8.0</span>),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                <span class="hljs-keyword">if</span> (widget.recipe.coverImageUrl.isNotEmpty)
                  Image.network(
                    widget.recipe.coverImageUrl,
                    width: <span class="hljs-built_in">double</span>.infinity,
                    height: <span class="hljs-number">200</span>,
                    fit: BoxFit.cover,
                  ),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">10</span>),
                Row(
                  children: [
                    Expanded(
                      child: Row(
                        children: [
                          Text(<span class="hljs-string">'<span class="hljs-subst">$_likes</span>'</span>),
                          <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                          IconButton(
                            icon: <span class="hljs-keyword">const</span> Icon(Icons.thumb_up, size: <span class="hljs-number">18</span>, color: Colors.redAccent),
                            onPressed: _likeRecipe,
                          ),
                        ],
                      ),
                    ),
                    Expanded(
                      child: Row(
                        children: [
                          Text(<span class="hljs-string">'<span class="hljs-subst">$_commentsCount</span>'</span>),
                          <span class="hljs-keyword">const</span> SizedBox(width: <span class="hljs-number">5</span>),
                          <span class="hljs-keyword">const</span> Icon(Icons.comment, size: <span class="hljs-number">18</span>, color: Colors.blue),
                        ],
                      ),
                    ),
                  ],
                ),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                ...widget.recipe.description.map((desc) =&gt;
                    Text(desc.children.map((child) =&gt; child.text).join())),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Ingredients'</span>, style: TextStyle(fontWeight: FontWeight.bold)),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                Text(widget.recipe.ingredients),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Procedure'</span>, style: TextStyle(fontWeight: FontWeight.bold)),
                <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">20</span>),
                ...widget.recipe.steps.map((step) =&gt;
                    Text(step.children.map((child) =&gt; child.text).join())),
                <span class="hljs-keyword">if</span> (_isLoading)
                  <span class="hljs-keyword">const</span> CircularProgressIndicator(),
                ..._comments.map((comment) =&gt; ListTile(
                  title: Text(comment.author),
                  subtitle: Text(comment.content),
                  trailing: Text(comment.createdAt.toLocal().toString()),
                )),
                <span class="hljs-keyword">if</span> (_isAuthenticated)
                  Column(
                    children: [
                      TextField(
                        controller: _commentController,
                        decoration: InputDecoration(labelText: tr(<span class="hljs-string">'add_comment'</span>)),
                      ),
                      ElevatedButton(
                        onPressed: _commentController.text.isNotEmpty ? _addComment : <span class="hljs-keyword">null</span>,
                        child: Text(tr(<span class="hljs-string">'submit'</span>)),
                      ),
                    ],
                  )
                <span class="hljs-keyword">else</span>
                  Text(tr(<span class="hljs-string">'login_comment'</span>)),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>This <code>RecipeDetailPage</code> displays detailed information about a selected recipe, including its cover image, likes, comments, ingredients, and procedure. Only authenticated users can comment or like a recipe. During initialization, the page checks if the user is authenticated by reading from local storage. If authenticated, it sets <code>_isAuthenticated</code> to <code>true</code> and retrieves the user's ID, enabling features like adding comments and liking recipes.</p>
<ul>
<li><p><strong>Adding a comment</strong>: The <code>_addComment</code> function posts the new comment to the server, adds it to the local comments list, increments the comment count, and clears the input field.</p>
</li>
<li><p><strong>Liking a recipe</strong>: The <code>_likeRecipe</code> function sends a like request to the server, increases the local like count, and updates the UI.</p>
</li>
</ul>
<p>If the user is not authenticated, they are prompted to log in to leave a comment or interact with the recipe.</p>
<h2 id="heading-create-request-recipe-screen">Create Request Recipe Screen</h2>
<p>In the <code>lib/screens/requestRecipe.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeRequestScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _RecipeRequestScreenState createState() =&gt; _RecipeRequestScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_RecipeRequestScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">RecipeRequestScreen</span>&gt; </span>{
  <span class="hljs-keyword">final</span> _formKey = GlobalKey&lt;FormState&gt;();
  <span class="hljs-keyword">final</span> _titleController = TextEditingController();
  <span class="hljs-keyword">final</span> _descriptionController = TextEditingController();
  <span class="hljs-keyword">final</span> ApiService _apiService = ApiService();

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _titleController.dispose();
    _descriptionController.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _submitRequest() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_formKey.currentState!.validate()) {
      <span class="hljs-keyword">final</span> description = _descriptionController.text;
      <span class="hljs-keyword">final</span> descriptionList = [
        Description(
          type: <span class="hljs-string">'paragraph'</span>,
          children: [
            TextContent(
              type: <span class="hljs-string">'text'</span>,
              text: description,
              bold: <span class="hljs-keyword">false</span>
            ),
          ],
        ),
      ];
      <span class="hljs-keyword">final</span> request = RecipeRequest(
        title: _titleController.text,
        description: descriptionList,
        id: <span class="hljs-number">0</span>,
      );
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> _apiService.submitRecipeRequest(request);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(tr(<span class="hljs-string">'request_successful'</span>))),
        );
        _titleController.clear();
        _descriptionController.clear();
      } <span class="hljs-keyword">catch</span> (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(<span class="hljs-string">'Failed to submit recipe request: <span class="hljs-subst">$e</span>'</span>)),
        );
      }
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'request_recipe'</span>)),
      ),
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'recipe_title'</span>)),
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> <span class="hljs-string">'Please enter a title'</span>;
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              TextFormField(
                controller: _descriptionController,
                decoration: InputDecoration(labelText: tr(<span class="hljs-string">'description'</span>)),
                maxLines: <span class="hljs-number">5</span>,
                validator: (value) {
                  <span class="hljs-keyword">if</span> (value == <span class="hljs-keyword">null</span> || value.isEmpty) {
                    <span class="hljs-keyword">return</span> tr(<span class="hljs-string">'enter_description'</span>);
                  }
                  <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
                },
              ),
              SizedBox(height: <span class="hljs-number">20</span>),
              ElevatedButton(
                onPressed: _submitRequest,
                child: Text(tr(<span class="hljs-string">'submit_request'</span>)),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>RecipeRequestPage</code> allows authenticated users to submit a request for a new recipe. <code>widget</code> is a statefull widget managed by the <code>_RecipeRequestPageState</code> class. It uses a form with two input fields: one for the recipe title and one for the description. These input fields are controlled by <code>TextEditingController</code> instances, which manage the text entered by the user.</p>
<p>The <code>_submitRequest</code> method handles the form submission. It validates the form fields, constructs a <code>RecipeRequest</code> object with the entered title and description, and sends it to the server using the <code>ApiService</code>. If the submission is successful, a success message is displayed using <code>ScaffoldMessenger</code>. If there is an error, an error message is shown.</p>
<p>The <code>build</code> method constructs the user interface of the screen and displays the form with its inputs.</p>
<h2 id="heading-create-user-profile-screen">Create User Profile Screen</h2>
<p>In the <code>lib/screens/profile.dart</code> file, add the code below:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:easy_localization/easy_localization.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_recipe_app/screens/requestRecipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../models/recipe.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../utils/server2.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  _ProfileScreenState createState() =&gt; _ProfileScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ProfileScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ProfileScreen</span>&gt; </span>{
  <span class="hljs-keyword">late</span> Future&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt; _requestedRecipesFuture;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _requestedRecipesFuture = ApiService().fetchUserRequestedRecipes();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(tr(<span class="hljs-string">'profile'</span>)),
      ),
      body: Column(
        children: [
          Padding(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">16.0</span>),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SizedBox(height: <span class="hljs-number">10</span>),
                Text(
                  tr(<span class="hljs-string">'request_list'</span>),
                  style: TextStyle(fontSize: <span class="hljs-number">16</span>, color: Colors.grey[<span class="hljs-number">600</span>]),
                ),
                SizedBox(height: <span class="hljs-number">20</span>),
                ElevatedButton(
                  onPressed: () {
                    Navigator.pop(context);
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =&gt; RecipeRequestScreen(),
                      ),
                    );
                  },
                  child: Text(tr(<span class="hljs-string">'request_new_recipe'</span>)),
                ),
              ],
            ),
          ),
          Expanded(
            child: FutureBuilder&lt;<span class="hljs-built_in">List</span>&lt;RecipeRequest&gt;&gt;(
              future: _requestedRecipesFuture,
              builder: (context, snapshot) {
                <span class="hljs-keyword">if</span> (snapshot.connectionState == ConnectionState.waiting) {
                  <span class="hljs-keyword">return</span> Center(child: CircularProgressIndicator());
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.hasError) {
                  <span class="hljs-keyword">return</span> Center(child: Text(<span class="hljs-string">'Error: <span class="hljs-subst">${snapshot.error.toString()}</span>'</span>));
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (snapshot.data == <span class="hljs-keyword">null</span> || snapshot.data!.isEmpty) {
                  <span class="hljs-keyword">return</span> Center(child: Text(tr(<span class="hljs-string">'no_request_found'</span>)));
                }

                <span class="hljs-keyword">return</span> ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) {
                    RecipeRequest request = snapshot.data![index];
                    <span class="hljs-built_in">String</span> fullDescription = request.description
                        .map((d) =&gt; d.children.map((t) =&gt; t.text).join(<span class="hljs-string">'\n'</span>))
                        .join(<span class="hljs-string">'\n\n'</span>);

                    <span class="hljs-keyword">return</span> Padding(
                      padding: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(horizontal: <span class="hljs-number">40.0</span>),
                      child: ListTile(
                        title: Text(
                          request.title.toUpperCase(),
                          style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                        ),
                        subtitle: Text(fullDescription),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The <code>ProfileScreen</code> class in this Flutter application represents a user's profile page where they can view their requested recipes. When the screen is initialized, it fetches a list of recipes requested by the user by calling the <code>fetchUserRequestedRecipes</code> method from the <code>ApiService</code>. This data is then stored in the <code>_requestedRecipesFuture</code> variable, which is a <code>Future</code> that will eventually hold the list of requested recipes.</p>
<p>In the <code>build</code> method, the screen is constructed using a <code>Scaffold</code> widget.</p>
<p>The main part of the screen is an <code>Expanded</code> widget containing a <code>FutureBuilder</code>. The <code>FutureBuilder</code> widget waits for the <code>_requestedRecipesFuture</code> to complete and then builds the list of requested recipes. If the data is still loading, it shows a <code>CircularProgressIndicator</code>. If there's an error, it displays an error message. And if there are no recipes, it shows a "no request found" message. Otherwise, it displays the list of requested recipes, each rendered as a <code>ListTile</code> with the recipe title and description.</p>
<h2 id="heading-test-the-app">Test the App</h2>
<p>To test the application, connect your device or launch an emulator then run the backend with the command below:</p>
<pre><code class="lang-bash">npm run develop
</code></pre>
<p>And the frontend:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a Flutter and Strapi recipe application where user could register and login to request a recipe from the admin, view and like recipes, or add their comments to a specific recipe.</p>
<p>To improve the application, you can add search functionality, share functionality, or allow users not only to request a recipe but also to create a personal list of recipes they can share with others.</p>
<p>Thanks for reading!</p>
<h3 id="heading-references">References</h3>
<ul>
<li><p>⁠<a target="_blank" href="https://docs.strapi.io/dev-docs/configurations/api-tokens">https://docs.strapi.io/dev-docs/configurations/api-tokens</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://docs.strapi.io/user-docs/settings/API-tokens">https://docs.strapi.io/user-docs/settings/API-tokens</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://docs.strapi.io/dev-docs/backend-customization/examples/authentication">https://docs.strapi.io/dev-docs/backend-customization/examples/authentication</a></p>
</li>
<li><p><a target="_blank" href="https://docs.strapi.io/dev-docs/plugins/i18n">https://docs.strapi.io/dev-docs/plugins/i18n</a></p>
</li>
<li><p>⁠⁠<a target="_blank" href="https://strapi.io/blog/how-to-create-a-refresh-token-feature-in-your-strapi-application">⁠⁠https://strapi.io/blog/how-to-create-a-refresh-token-feature-in-your-strapi-application</a></p>
</li>
<li><p><a target="_blank" href="https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi">https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi</a></p>
</li>
<li><p><a target="_blank" href="https://jwt.io/introduction">https://jwt.io/introduction</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Build a Full Stack App with Next.js and Strapi ]]>
                </title>
                <description>
                    <![CDATA[ Building modern, dynamic websites requires a combination of powerful front-end frameworks and flexible content management systems. Whether you're a developer looking to enhance your skills or a beginner wanting to create your first web project, learn... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-full-stack-app-with-nextjs-and-strapi/</link>
                <guid isPermaLink="false">67a4d4fb19ce74a3822b3eab</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Strapi ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 05 Feb 2025 05:00:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738855654744/dd05c620-8018-4c3c-9800-3948afe5277a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Building modern, dynamic websites requires a combination of powerful front-end frameworks and flexible content management systems. Whether you're a developer looking to enhance your skills or a beginner wanting to create your first web project, learning how to integrate Next.js with a headless CMS like Strapi can give you the tools to build scalable and maintainable applications.</p>
<p>We just published a course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you how to build a <strong>fully functional summer camp website using Next.js 15 and Strapi 5</strong>. This project-based course walks you through the entire process, from setting up the project to implementing complex features like dynamic content loading, search functionality, and form submissions. By the end of the course, you will have a professional-quality website that is both developer-friendly and easy for clients to manage. Paul Bratslavsky developed this course.</p>
<h3 id="heading-what-youll-learn-in-this-course">What You’ll Learn in This Course</h3>
<p>By completing this course, you will gain hands-on experience with:</p>
<ul>
<li><p><strong>Next.js 15</strong> – Understanding server and client components, dynamic routing, and data fetching</p>
</li>
<li><p><strong>Strapi 5</strong> – Setting up and using a headless CMS to manage content efficiently</p>
</li>
<li><p><strong>Full-stack development</strong> – Integrating front-end and back-end technologies seamlessly</p>
</li>
<li><p><strong>User-friendly navigation</strong> – Building top navigation, footers, and internal links</p>
</li>
<li><p><strong>Blog and event management</strong> – Creating structured content with search and pagination</p>
</li>
<li><p><strong>Form handling</strong> – Implementing form submissions with server actions</p>
</li>
</ul>
<h3 id="heading-course-breakdown">Course Breakdown</h3>
<p>This course is designed to take you step by step through the development of a real-world website. Here are some key sections:</p>
<ul>
<li><p>Setting up the <strong>Next.js 15</strong> project and understanding its core features</p>
</li>
<li><p>Configuring <strong>Strapi 5</strong> as a headless CMS for dynamic content management</p>
</li>
<li><p>Building pages including the <strong>homepage, experience page, blog page, and events page</strong></p>
</li>
<li><p>Implementing <strong>navigation and layout components</strong> for a smooth user experience</p>
</li>
<li><p>Adding <strong>form submission functionality</strong> to collect user input</p>
</li>
<li><p>Enhancing user experience with <strong>search and pagination</strong></p>
</li>
<li><p>Structuring and loading <strong>custom content</strong> dynamically</p>
</li>
<li><p>Using <strong>Figma for developers</strong> to understand design implementation</p>
</li>
</ul>
<p>This course is perfect for developers looking to improve their <strong>Next.js and Strapi skills</strong> while building a practical project. Whether you are a front-end developer wanting to explore full-stack development or a beginner eager to learn modern web technologies, this course will provide you with valuable knowledge and experience.</p>
<p>Watch the full course now on the <a target="_blank" href="https://youtu.be/Q-cPtlYG1cY">freeCodeCamp.org YouTube channel</a>, and start building your own content-rich, dynamic website.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Q-cPtlYG1cY" 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>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up Social Authentication with Strapi and Nuxt.js ]]>
                </title>
                <description>
                    <![CDATA[ Enhancing security is a critical aspect of every development process. But it’s crucial to make your apps accessible for users signing up for the application. So, creating a seamless experience for users while they sign up and maintaining security thr... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-up-social-authentication-with-strapi-and-nuxtjs/</link>
                <guid isPermaLink="false">6786814ce75a85acb18f1d2d</guid>
                
                    <category>
                        <![CDATA[ Strapi ]]>
                    </category>
                
                    <category>
                        <![CDATA[ social authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Nuxt ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ashimi0x ]]>
                </dc:creator>
                <pubDate>Tue, 14 Jan 2025 15:22:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736865891246/8be87fdb-c57b-4ae1-91ea-00b6dbffe09b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Enhancing security is a critical aspect of every development process. But it’s crucial to make your apps accessible for users signing up for the application.</p>
<p>So, creating a seamless experience for users while they sign up and maintaining security throughout the process is key. This is why many web developers are adopting social authentication today.</p>
<p>In this article, I’ll walk you through setting up social authentication on a Strapi project using GitHub. Then we’ll integrate it into a simple Nuxt.js setup.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-social-authentication">What is Social Authentication?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-strapi">What is Strapi?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-nuxtjs">What is Nuxt.js?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-will-you-learn-in-this-guide">What Will You Learn in This Guide?</a></p>
</li>
</ul>
</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-strapi-for-social-authentication">How to Set Up Strapi for Social Authentication</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-strapis-providers">How to Set Up Strapi’s Providers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-githubs-oauth-app">How to Create GitHub’s Oauth App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-connect-github-oauth-app-to-provider-on-strapi">How to Connect Github OAuth APP to Provider on Strapi</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-the-strapi-api">How to Test the Strapi API</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-nuxtjs-for-social-authentication">How to Set Up Nuxt.js for Social Authentication</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-social-authentication">What is Social Authentication?</h2>
<p>Social authentication leverages familiar social media accounts so that users don’t need to remember another username-password combination. Instead of creating a unique username and password for every website, users can log in via their accounts from popular platforms like Google, Facebook, Twitter, or GitHub.</p>
<p>You can set this up using OAuth. It’s a widely adopted protocol that allows websites to access limited user data without exposing sensitive credentials. It also helps eliminate barriers in the signup or login process, lowering the chances that people will abandon the site. Users can quickly log in with a few clicks, making onboarding smoother and reducing form fatigue.</p>
<p>Social auth also enhances the app's overall security by offloading certain responsibilities to trusted providers who already employ strong security protocols.</p>
<h3 id="heading-what-is-strapi"><strong>What is Strapi?</strong></h3>
<p>Strapi is an open-source headless content management system (CMS) built with Node.js. It allows developers to manage and deliver content through APIs (REST or GraphQL) while providing a customizable and extendable platform.</p>
<p>Strapi is popular for its flexibility (it works with various front-end frameworks) and plugin system, making adding features like social authentication easy.</p>
<h3 id="heading-what-is-nuxtjs"><strong>What is Nuxt.js?</strong></h3>
<p>Nuxt.js is a powerful Vue.js framework designed for building modern web applications with server-side rendering (SSR), static site generation, and powerful client-side routing. You can use it to create high-performance, SEO-friendly, and scalable applications.</p>
<p>Nuxt’s modular structure and ease of integration with APIs make it ideal for building complex front-end applications, such as those that require social authentication.</p>
<h3 id="heading-what-will-you-learn-in-this-guide"><strong>What Will You Learn in This Guide?</strong></h3>
<p>This guide will walk you through implementing social authentication using <strong>Strapi v5</strong> and <strong>Nuxt.js</strong>. You’ll learn how to:</p>
<ul>
<li><p>Set up OAuth for social logins</p>
</li>
<li><p>Configure Strapi to handle social authentication</p>
</li>
<li><p>Integrate the functionality smoothly into an existing Nuxt.js front-end</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into the tutorial, make sure you have the necessary foundational knowledge and tools:</p>
<ol>
<li><p><strong>Basic knowledge of Node.js, Vue.js, and Nuxt.js</strong>: Familiarity with these technologies is crucial, as Strapi is built on Node.js, and Nuxt.js is a Vue.js-based framework. To navigate through the integration process, you’ll need a general understanding of how Node.js handles server-side logic and how Vue.js works on the front end.</p>
</li>
<li><p><strong>Familiarity with REST APIs or GraphQL (optional but helpful)</strong>: Strapi provides both REST and GraphQL APIs to handle data and authentication. While this guide will focus primarily on REST APIs for social authentication, knowing how APIs work and how to make HTTP requests is useful. If you’re familiar with GraphQL, you could optionally use it for more complex queries and authentication handling.</p>
</li>
<li><p><strong>Social Provider Developer Accounts:</strong> To integrate social authentication, you must create developer accounts on the social platforms you want to support (like Google, Facebook, or GitHub). You’ll need API keys or client IDs from each provider. In this tutorial, you’ll learn how to use GitHub for this functionality.</p>
</li>
</ol>
<h2 id="heading-how-to-set-up-strapi-for-social-authentication">How to Set Up Strapi for Social Authentication</h2>
<p>If you haven’t already created a Strapi project, let’s start by generating a new one.</p>
<p>You can create a new Strapi project with <strong>Yarn</strong> (recommended). Run the following command in your terminal:</p>
<p><code>yarn create strapi-app@latest my-project</code></p>
<p>Or with <strong>npm</strong>:</p>
<p><code>npx create-strapi-app@latest my-project</code></p>
<p>Respond ‘yes” to all the prompts:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729719988851_Screenshot+2024-10-23+at+22.23.44.png" alt="Strapi's Installation interface on Virtual Studio Code" width="1280" height="800" loading="lazy"></p>
<p>You should now have a fresh Strapi 5 application with a default SQLite database. To spin it up in development mode, open your project and run:</p>
<p><code>yarn run develop</code></p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729722404206_Screenshot+2024-10-23+at+23.26.03.png" alt="VS Code Terminal Interface for a running Strapi project" width="1280" height="800" loading="lazy"></p>
<p>Once the project is up and running, Strapi will automatically launch in your browser at http://localhost:1337/admin. The first step is to create an <strong>admin account</strong> to access the Strapi admin panel:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729722491173_Screenshot+2024-10-23+at+23.28.01.png" alt="Strapi Admin Registration Interface" width="1280" height="800" loading="lazy"></p>
<p>Fill out the required fields and create your admin user. Once you’re logged in, you’ll have access to the full Strapi admin interface, where you can manage content types, plugins, and settings:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729722679829_Screenshot+2024-10-23+at+23.28.50.png" alt="Strapi Welcome Page" width="1280" height="800" loading="lazy"></p>
<h3 id="heading-how-to-set-up-strapis-providers">How to Set Up Strapi’s Providers</h3>
<p>For social authentication, Strapi's Users &amp; Permissions plugin is essential and already comes pre-installed with the default setup. Click on Settings in the Strapi admin panel:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729722965788_Screenshot+2024-10-23+at+23.35.39.png" alt="Strapi Settings Page Overview" width="1280" height="800" loading="lazy"></p>
<p>Scroll down and Click on Providers in the Users &amp; Permissions plugin section:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729723008188_Screenshot+2024-10-23+at+23.36.35.png" alt="The Strapi's Providers page" width="1280" height="800" loading="lazy"></p>
<p>You will see this list of Providers to select from. For this article, select GitHub by clicking the pen icon on the right:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729723164437_Screenshot+2024-10-23+at+23.39.11.png" alt="Editing the Github providers setting" width="1280" height="800" loading="lazy"></p>
<p>By default, it is set to “false”, Toggle it to True. Take note of the redirect URL and copy it.</p>
<h3 id="heading-how-to-create-githubs-oauth-app">How to Create GitHub’s Oauth App</h3>
<p>On a different browser tab, log in to your GitHub account and click on your settings:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729716332697_Screenshot+2024-10-23+at+21.45.06.png" alt="Github Explore Page" width="1280" height="800" loading="lazy"></p>
<p>Then navigate to your Developer Settings:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729716365753_Screenshot+2024-10-23+at+21.45.18.png" alt="Profile Settings Page" width="1280" height="800" loading="lazy"></p>
<p>Select OAuth Apps and New OAuth app:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729716462052_Screenshot+2024-10-23+at+21.46.56.png" alt="Developer Settings Page" width="1280" height="800" loading="lazy"></p>
<p>As you can see below, the redirect URL will be pasted as the Authorization callback URL and the homepage URL will be your App URL.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729716551370_Screenshot+2024-10-23+at+21.48.51.png" alt="OAuth's APP registration Page" width="1280" height="800" loading="lazy"></p>
<p>Go ahead and click “Register application”, and you will see your Client ID. Now you need to generate your Client Secret:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729723880674_Screenshot+2024-10-23+at+23.49.35.png" alt="Github Application successfully created." width="1280" height="800" loading="lazy"></p>
<h3 id="heading-how-to-connect-github-oauth-app-to-provider-on-strapi">How to Connect Github OAuth APP to Provider on Strapi</h3>
<p>Once your Client Secret has been generated, update the application and return to your Strapi App to input them. The redirect URL to your front-end app will be <a target="_blank" href="http://localhost:3000/connect/github">http://localhost:3000/connect/github</a>.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729723954537_Screenshot+2024-10-23+at+23.52.13.png" alt="Adding Github Provider to Strapi" width="1280" height="800" loading="lazy"></p>
<p>You can save everything now, and your GitHub Provider should be enabled.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729724039006_Screenshot+2024-10-23+at+23.53.23.png" alt="Strapi's Provider Interface" width="1280" height="800" loading="lazy"></p>
<p>You can set up as many providers as you wish.</p>
<p>To allow users to authenticate with social logins, you need to update the default public role in Strapi.</p>
<p>Click Roles right above Providers:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729724464559_Screenshot+2024-10-24+at+00.00.46.png" alt="Strapi Interface for Roles: Authenticated and Public" width="1280" height="800" loading="lazy"></p>
<p>Select the Public role and ensure that the connect and callback permissions are enabled. They are already enabled by default in Strapi 5.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729724599165_Screenshot+2024-10-24+at+00.02.09.png" alt="Strapi's Public Role Interface" width="1280" height="800" loading="lazy"></p>
<p>Click Save after updating the permissions. At this point, you’ve set up the necessary social authentication providers. Before proceeding with Nuxt.js integration, you can test it by making an API request using a tool like Postman.</p>
<h3 id="heading-how-to-test-the-strapi-api">How to Test the Strapi API</h3>
<p>Send a GET request to http://localhost:1337//api/connect/github/.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729725634296_Screenshot+2024-10-24+at+00.20.16.png" alt="Postman's Interface for testing APIs." width="1280" height="800" loading="lazy"></p>
<p>You will notice from the Console that it’s reaching for a GitHub login client. Copy the whole URL and paste it into your browser. You should see an interface like this:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729725737744_Screenshot+2024-10-24+at+00.22.04.png" alt="Interface for authorizing Strapi" width="1280" height="800" loading="lazy"></p>
<p>If you got this, congrats! You can now move on to setting up your Nuxt.Js frontend application.</p>
<h2 id="heading-how-to-set-up-nuxtjs-for-social-authentication">How to Set Up Nuxt.js for Social Authentication</h2>
<p>In this section, you will learn how to set up the <strong>authentication flow</strong> between Nuxt.js and Strapi, handle the OAuth redirects, and display a login button for users to authenticate.</p>
<p>First, you’ll need to install Nuxt.js by running one of these commands:</p>
<p><code>yarn create nuxt-app &lt;project-name&gt; //using yarn</code></p>
<p><code>npx create-nuxt-app &lt;project-name&gt; //using npx</code></p>
<p><code>npm init nuxt-app &lt;project-name&gt; //using npm</code></p>
<p>Visit the Nuxt.js docs if you need a refresher.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729732718758_Screenshot+2024-10-24+at+01.52.37.png" alt="VS code interface for installing NUXT" width="1280" height="800" loading="lazy"></p>
<p>Open your project and run <code>npm run dev</code> to get it started:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729734102247_Screenshot+2024-10-24+at+02.40.17.png" alt="Interface for a running Nuxt project." width="1280" height="800" loading="lazy"></p>
<p>Once running, visit <a target="_blank" href="https://localhost:3000">https://localhost:3000</a> on your browser and you should see a Nuxt page that looks like this:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1729734185176_Screenshot+2024-10-24+at+02.42.51.png" alt="Nuxt application interface on browser." width="1280" height="800" loading="lazy"></p>
<p>In your code editor, delete the <code>Tutorial.vue</code> in the components folder and go to <code>index.vue</code> in the pages folder. There, you’ll want to add the following:</p>
<pre><code class="lang-javascript">&lt;template&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"min-h-screen flex justify-center items-center text-center mx-auto sm:pl-24 bg-yellow-200"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-1/2 sm:text-left sm:m-5"</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>

          <span class="hljs-attr">class</span>=<span class="hljs-string">"text-3xl sm:text-6xl font-black sm:pr-10 leading-tight text-blue-900"</span>

        &gt;</span>

          Welcome

        <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"links"</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">NuxtLink</span> <span class="hljs-attr">to</span>=<span class="hljs-string">"/login"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"button--blue shadow-xl"</span>&gt;</span> Login <span class="hljs-tag">&lt;/<span class="hljs-name">NuxtLink</span>&gt;</span>

      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-1/2 hidden sm:block"</span>&gt;</span>

    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>

&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {}

</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">

<span class="hljs-comment">/* Sample apply at-rules with Tailwind CSS

.container {

@apply min-h-screen flex justify-center items-center text-center mx-auto;

}

*/</span>

<span class="hljs-selector-class">.container</span> {

  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> auto;

  <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span>;

  <span class="hljs-attribute">display</span>: flex;

  <span class="hljs-attribute">justify-content</span>: center;

  <span class="hljs-attribute">align-items</span>: center;

  <span class="hljs-attribute">text-align</span>: center;

}

<span class="hljs-selector-class">.title</span> {

  <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Quicksand'</span>, <span class="hljs-string">'Source Sans Pro'</span>, -apple-system, BlinkMacSystemFont,

    <span class="hljs-string">'Segoe UI'</span>, Roboto, <span class="hljs-string">'Helvetica Neue'</span>, Arial, sans-serif;

  <span class="hljs-attribute">display</span>: block;

  <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">300</span>;

  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">100px</span>;

  <span class="hljs-attribute">color</span>: <span class="hljs-number">#35495e</span>;

  <span class="hljs-attribute">letter-spacing</span>: <span class="hljs-number">1px</span>;

}

<span class="hljs-selector-class">.subtitle</span> {

  <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">300</span>;

  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">42px</span>;

  <span class="hljs-attribute">color</span>: <span class="hljs-number">#526488</span>;

  <span class="hljs-attribute">word-spacing</span>: <span class="hljs-number">5px</span>;

  <span class="hljs-attribute">padding-bottom</span>: <span class="hljs-number">15px</span>;

}

<span class="hljs-selector-class">.links</span> {

  <span class="hljs-attribute">padding-top</span>: <span class="hljs-number">15px</span>;

}</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>This code provides you with a simple homepage and a link to the login page which currently leads to a 404 page. In your pages folder, create <code>login.vue</code> and add the following code:</p>
<pre><code class="lang-javascript">&lt;template&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>

      <span class="hljs-attr">class</span>=<span class="hljs-string">"min-h-screen flex justify-center items-center text-center mx-auto sm:pl-24 bg-yellow-200"</span>

    &gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-1/2 hidden sm:block m-5 p-6"</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">""</span> /&gt;</span>

      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sm:w-1/2 w-4/5"</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"m-5 font-black text-3xl"</span>&gt;</span>Social Login<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"shadow-xl bg-white p-10"</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">a</span>

            <span class="hljs-attr">href</span>=<span class="hljs-string">"http://localhost:1337/api/connect/github"</span>

            <span class="hljs-attr">class</span>=<span class="hljs-string">"cursor-pointer m-3 button--blue shadow-xl"</span>

          &gt;</span>Github          <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>

        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>

  &lt;/template&gt;

  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {}

  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"scss"</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>In lines 11 - 14, the user can click the link and have the login logic executed. But you need to ensure that Nuxt can handle redirects.  </p>
<p>In your pages folder, create a folder called <code>connect</code>, and then inside that a file named <code>_provider.vue</code>, and add the following code to handle the callback function:</p>
<pre><code class="lang-javascript">&lt;template&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>user page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>

&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {

  data() {

    <span class="hljs-keyword">return</span> {

      <span class="hljs-attr">provider</span>: <span class="hljs-built_in">this</span>.$route.params.provider,

      <span class="hljs-attr">access_token</span>: <span class="hljs-built_in">this</span>.$route.query.access_token,

    }

  },

  <span class="hljs-keyword">async</span> mounted() {

    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.$axios.$get(

      <span class="hljs-regexp">/auth/</span>${<span class="hljs-built_in">this</span>.provider}/callback?access_token=${<span class="hljs-built_in">this</span>.access_token}

    )



    <span class="hljs-keyword">const</span> { jwt, user } = res

    <span class="hljs-comment">// store jwt and user object in localStorage</span>

    <span class="hljs-built_in">this</span>.$auth.$storage.setUniversal(<span class="hljs-string">'jwt'</span>, jwt)

    <span class="hljs-built_in">this</span>.$auth.$storage.setUniversal(<span class="hljs-string">'user'</span>, { <span class="hljs-attr">username</span>: user.username, <span class="hljs-attr">id</span>: user.id, <span class="hljs-attr">email</span>: user.email })


    <span class="hljs-built_in">this</span>.$router.push(<span class="hljs-string">`/users/<span class="hljs-subst">${user.id}</span>`</span>)

  },

}

</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"scss"</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>So far, your folder/file structure should look like this:</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1730069360059_Screen+Shot+2024-10-27+at+11.47.45+PM.png" alt="Nuxt File structure on VScode" width="1280" height="800" loading="lazy"></p>
<p>The code in <code>_provider.vue</code> handles redirects from the Strapi backend. Nuxt.js has a routing pattern that takes advantage of /connect/*provider where, in this case, the provider is GitHub.</p>
<p>It gets an access token which is stored as <code>access_token</code>, then makes an API call to the backend in the mounted lifecycle method. This returns a response that contains the user and a JWT. It stores the user and JWT in both cookies and localStorage using a package called <a target="_blank" href="https://auth.nuxtjs.org/">@nuxtjs/auth-next</a>, then redirects the user to the user account page.</p>
<p>You’ll need to install the @nuxtjs/auth-next module using the following command:</p>
<p><code>npm install @nuxtjs/auth-next //using npm</code></p>
<p>Or with yarn:</p>
<p><code>yarn add @nuxtjs/auth-next //using yarn</code></p>
<p>After installing, open your nuxt.config.js file and configure the Auth modules:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">modules</span>: [
   <span class="hljs-string">'@nuxtjs/auth-next'</span>,
 ],
</code></pre>
<p>Next, you’ll need the <a target="_blank" href="https://axios.nuxtjs.org/">@nuxtjs/axios</a> and <a target="_blank" href="https://strapi.nuxtjs.org/">@nuxtjs/strapi</a> packages to fetch data from the backend. <a target="_blank" href="https://axios.nuxtjs.org/">@nuxtjs/axios</a> is already integrated when installing Nuxt, but you have to set your baseurl.</p>
<p>Open up your nuxt.config.js file and add the following lines of code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Axios module configuration: https://go.nuxtjs.dev/config-axios</span>
<span class="hljs-attr">axios</span>: {
    <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:1337'</span>
},
</code></pre>
<p>Then install <a target="_blank" href="https://strapi.nuxtjs.org/">@nuxtjs/strapi</a> using the following command:</p>
<p><code>yarn add @nuxtjs/strapi //using yarn</code></p>
<p>Or:</p>
<p><code>npm install @nuxtjs/strapi //using npm</code></p>
<p>Replace the content of your nuxt.config.js file with the following lines of code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-comment">// Global page headers: https://go.nuxtjs.dev/config-head</span>

  <span class="hljs-attr">head</span>: {

    <span class="hljs-attr">title</span>: <span class="hljs-string">'nuxtstrapi'</span>,

    <span class="hljs-attr">htmlAttrs</span>: {

      <span class="hljs-attr">lang</span>: <span class="hljs-string">'en'</span>

    },

    <span class="hljs-attr">meta</span>: [

      { <span class="hljs-attr">charset</span>: <span class="hljs-string">'utf-8'</span> },

      { <span class="hljs-attr">name</span>: <span class="hljs-string">'viewport'</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">'width=device-width, initial-scale=1'</span> },

      { <span class="hljs-attr">hid</span>: <span class="hljs-string">'description'</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'description'</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">''</span> },

      { <span class="hljs-attr">name</span>: <span class="hljs-string">'format-detection'</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">'telephone=no'</span> }

    ],

    <span class="hljs-attr">link</span>: [

      { <span class="hljs-attr">rel</span>: <span class="hljs-string">'icon'</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">'image/x-icon'</span>, <span class="hljs-attr">href</span>: <span class="hljs-string">'/favicon.ico'</span> }

    ]

  },

  <span class="hljs-comment">// Global CSS: https://go.nuxtjs.dev/config-css</span>

  <span class="hljs-attr">css</span>: [

  ],

  <span class="hljs-comment">// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins</span>

  <span class="hljs-attr">plugins</span>: [

  ],

  <span class="hljs-comment">// Auto import components: https://go.nuxtjs.dev/config-components</span>

  <span class="hljs-attr">components</span>: <span class="hljs-literal">true</span>,

  <span class="hljs-comment">// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules</span>

  <span class="hljs-attr">buildModules</span>: [

    <span class="hljs-comment">// https://go.nuxtjs.dev/typescript</span>

    <span class="hljs-string">'@nuxt/typescript-build'</span>,

  ],

  <span class="hljs-comment">// Modules: https://go.nuxtjs.dev/config-modules</span>

  <span class="hljs-attr">modules</span>: [

    <span class="hljs-string">'@nuxtjs/auth-next'</span>,

    <span class="hljs-string">'@nuxtjs/axios'</span>,

    <span class="hljs-string">'@nuxtjs/strapi'</span>

  ],

  <span class="hljs-attr">axios</span>: {

    <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:1337'</span>

  },

  <span class="hljs-attr">strapi</span>: {

    <span class="hljs-attr">entities</span>: [ <span class="hljs-string">"articles', 'users' ],

    url: 'http://localhost:1337'

},

  // Build Configuration: https://go.nuxtjs.dev/config-build

  build: {

  }

}</span>
</code></pre>
<p>Run both your front end and Strapi app to test the login authentication. When you go to your Strapi admin and check User under Content Manager, you should see the newly authenticated user.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_604FB5A9DF1492F56A770C5B36ABD7454099B52858C205A9399E3B1409CA50D8_1730072300480_Screen+Shot+2024-10-28+at+12.36.40+AM.png" alt="Content Manager Interface on Strapi showcasing Users" width="1280" height="800" loading="lazy"></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>At this point, you have successfully set up your Strapi application with social authentication. Now you can add as many providers as you want and customize your applications to serve your needs.</p>
<p>Thanks for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
