<?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" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Bool False on Medium]]></title>
        <description><![CDATA[Stories by Bool False on Medium]]></description>
        <link>https://medium.com/@boolfalse?source=rss-2952bff55059------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*l-0UESSqXeXaCSiXO2QcaA.png</url>
            <title>Stories by Bool False on Medium</title>
            <link>https://medium.com/@boolfalse?source=rss-2952bff55059------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 17 May 2026 11:11:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@boolfalse/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Setup Custom Email with Cloudflare and Mailgun]]></title>
            <link>https://medium.com/@boolfalse/setup-custom-email-with-cloudflare-and-mailgun-41ef4858fb04?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/41ef4858fb04</guid>
            <category><![CDATA[email]]></category>
            <category><![CDATA[boolfalse]]></category>
            <category><![CDATA[mailgun]]></category>
            <category><![CDATA[cloudflare]]></category>
            <category><![CDATA[gmail]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Tue, 16 Apr 2024 08:48:14 GMT</pubDate>
            <atom:updated>2024-04-16T08:48:14.313Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*44V8UKYccm84Mmx7-kJJEg.png" /><figcaption>Manage Emails on Gmail using your custom email address</figcaption></figure><p>As a software engineer, you may want to consider having a professional email account along with your own website like “<em>info@example.com</em>”. But you may realized that it must cost a certain amount that you are not willing to pay. How would you behave if you knew you could do it for free? There is actually a way to do it, and besides the fact that having a professional email account is free, it will help you be more efficient, reliable and secure in your daily work.</p><p>In this article, you’ll learn how to create and set up your own email address using Cloudflare and Mailgun to manage emails in Gmail. It means you can send and receive emails directly on your Gmail inbox.<br>I’ve already done this for personal use and have taken screenshots of the entire process to show it in this article. So I’ll share here all the necessary steps you need to follow to set up your own email.</p><p><strong>The original article initially was published on </strong><a href="https://www.freecodecamp.org/news/how-to-set-up-custom-email/"><strong>freeCodeCamp</strong></a><strong>.</strong></p><h3>Introduction</h3><p>Let’s figure out what you need to have before you start, what you are going to do and how it will work.</p><h4>What you need to have before you start:</h4><p>At first, we assume, you already have a domain, let’s call it “<em>yourdomain.com</em>”, on which you have control. Specifically, you need to have accessibility to connect your domain with Cloudflare and setup DNS records there. A classic example of that is having a domain on some domain registrar (like GoDaddy, Namecheap), and adding your domain to Cloudflare by setting DNS records provided by Cloudflare on your domain registrar account.</p><p>Adding a domain to Cloudflare involves updating your domain’s DNS nameservers to point to Cloudflare’s nameservers. Once the domain is added, Cloudflare acts as an intermediary for web traffic, providing security features like DDoS protection, firewall, and SSL encryption, as well as performance enhancements through caching and content optimization.<br>If you haven’t done that yet, here’s the official <a href="https://www.youtube.com/watch?v=7hY3gp_-9EU">video on YouTube</a> on how to connect your domain to Cloudflare.</p><p>Additionally, Cloudflare manages DNS records for your domain, allowing you to control how traffic is routed and ensuring reliable delivery of services like email.<br>So, our work in this article will be focusing exactly on that, on how to setup your domain on Cloudflare Email.<br><a href="https://blog.cloudflare.com/email-routing-leaves-beta/">Cloudflare Email</a> is one of the services of Cloudflare since 2021, which can be used for free (as for now at least).</p><p>The second assumption is that you have account at Gmail, and you have access to its email settings. Simply, if you have just a regular “<em>youremail@gmail.com</em>” email, which isn’t under control of any administrator or kind of that, then you’re have nothing to worry about for this. We’ll explore and work on email settings later on.</p><h4>What you are going to do:</h4><p>In simple words, you’re going to create a custom email like “<em>something@yourdomain.com</em>”, which you can use to send and receive emails from that email by using Gmail’s platform. So you will be able receiving and reading emails sent to “<em>something@yourdomain.com</em>” in Gmail, as well as sending emails from that custom email using Gmail.<br>For all of that you’ll use Cloudflare Email for the email routing, and Mailgun’s SMTP server for sending emails.</p><h4>How it will work technically:</h4><p>When composing an email from Gmail with the sender set as “<em>something@yourdomain.com</em>”, Gmail utilizes Mailgun’s SMTP server through the provided credentials, transmitting the email. Mailgun then processes the message and forwards it to the recipient’s email server, likely involving DNS lookups to find the recipient’s server.<br>Emails sent to “<em>something@yourdomain.com</em>” are received by Cloudflare’s email servers, configured via MX records in the domain’s DNS settings. Cloudflare stores the received emails in the associated account, accessible through Gmail, which periodically connects to Cloudflare’s servers (using IMAP or POP3 protocols) to retrieve new messages, enabling seamless access to incoming emails.</p><h3>Email Routing on Cloudflare</h3><blockquote>Cloudflare Email Routing is designed to simplify the way you create and manage email addresses, without needing to keep an eye on additional mailboxes. With Email Routing, you can create any number of custom email addresses to use in situations where you do not want to share your primary email address, such as when you subscribe to a new service or newsletter. Emails are then routed to your preferred email inbox, without you ever having to expose your primary email address. (<a href="https://developers.cloudflare.com/email-routing/">Docs</a>)</blockquote><p>Sign in to your Cloudflare account and navigate to Dashboard.<br>Choose and click to the desired website. For me it’s “<em>boolfalse.com</em>”, as I want to create a custom email like “<em>email@boolfalse.com</em>”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6sqXhz3zwWllR5eYErxe8g.png" /><figcaption>Cloudflare: Websites</figcaption></figure><p>Navigate to the “<em>Email Routing</em>” for the selected website.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*8kwWwmbbHuuCafec4jGLPA.png" /><figcaption>Cloudflare: Email Routing</figcaption></figure><p>If you don’t have email routing configured, you may see something similar to the screenshot above. Click “Get started”. You may be able to create your own address to receive emails and take action.<br>We’ll skip this without creating our own address for doing it manually.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*MfRvIXt7Xug8P_iRjhPENA.png" /><figcaption>Cloudflare: Custom Email</figcaption></figure><p>By default, email routing is disabled, so you need to enable it. Click the link to navigate to the “<em>Email Routing</em>” page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*EVK-QvmUBUjoH_kPGALrWg.png" /><figcaption>Cloudflare: Email Routing</figcaption></figure><p>Submit it by clicking “Enable Email Routing”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*8718fDCsXS_SSKIaLMkNNA.png" /><figcaption>Cloudflare: Enable Email Routing</figcaption></figure><p>For get it done, you need to have three MX and one TXT records:</p><ul><li>Type: <strong><em>MX</em></strong>; Name: <strong><em>@;</em></strong> Mail Server: <strong><em>route1.mx.cloudflare.net</em></strong>; TTL: <strong><em>Auto</em></strong>; Priority: <strong><em>69</em></strong></li><li>Type: <strong><em>MX</em></strong>; Name: <strong><em>@;</em></strong> Mail Server: <strong><em>route2.mx.cloudflare.net</em></strong>; TTL: <strong><em>Auto</em></strong>; Priority: <strong><em>99</em></strong></li><li>Type: <strong><em>MX</em></strong>; Name: <strong><em>@;</em></strong> Mail Server: <strong><em>route3.mx.cloudflare.net</em></strong>; TTL: <strong><em>Auto</em></strong>; Priority: <strong><em>40</em></strong></li><li>Type: <strong><em>TXT</em></strong>; Name: <strong><em>@;</em></strong> TTL: <strong><em>Auto</em></strong>; Content: <strong><em>v=spf1 include:_spf.mx.cloudflare.net ~all</em></strong></li></ul><p>You can see them at the bottom of the “<em>Email Routing</em>” page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*co4ZgRdt4KFHTH0dciFQnQ.png" /><figcaption>Cloudflare: DNS records for Email Routing</figcaption></figure><p>So, as already said, in the left menu, go to “DNS” -&gt; “Records” and add the following records there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*5ZDCFZ5WG0rxY0XY7rqgxw.png" /><figcaption>Cloudflare: DNS records added</figcaption></figure><p>After creating these records, go to the “<em>Email Routing</em>” page again.</p><p>There you only need to have the records you just created.<br>So, if you have any other records, just delete them.<br>For example, I already had an unnecessary entry there that I should delete.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*t151ls_SLxHMJydQz1pocg.png" /><figcaption>Cloudflare: existing records for Email Routing</figcaption></figure><p>Submit to delete existing unnecessary records.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*H6x8-VfI1orTwqiNulKCjQ.png" /><figcaption>Cloudflare: deleting unnecessary records</figcaption></figure><p>After removing unnecessary DNS records, you will see only the ones you need there.<br>You will now be able to enable email routing by clicking the “Add records and enable” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*LkjYg_XfyLtQGn1VvBRu6w.png" /><figcaption>Cloudflare: Enable Email Routing</figcaption></figure><p>After enabling it you’ll see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*hbkEnW8WEZ2bBf9gzs1VQA.png" /><figcaption>Cloudflare: Email DNS records configured</figcaption></figure><h3>Creating Custom Email on Cloudflare</h3><p>Now go to the “<em>Routes</em>” tab and create an email by clicking the “Create address” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*tvJrYbM3Dkhz7l9iRAKgLw.png" /><figcaption>Cloudflare: Email Routing (enabled)</figcaption></figure><p>In this example, we’ll create “<em>email@boolfalse.com</em>” email address, by adding “<em>email</em>” as a custom address, and a destination email address, where I’ll be able to receive emails.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*qID_RQjEVwKYRNJ6BJLjWw.png" /><figcaption>Cloudflare: Email Routing</figcaption></figure><p>You’ll see a notification about that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*lVShSAwjXDk0OiAFR6IBbw.png" /><figcaption>Cloudflare: creating a custom email</figcaption></figure><p>You will also get an email for confirming this action.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*taW2tRqc-B0RFbyTajdHUg.png" /><figcaption>Verifying the destination email</figcaption></figure><p>Just verify the email address.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*rLYG2a4l4d1zPSlDv4thtg.png" /><figcaption>Verify email address</figcaption></figure><p>Once you’ve verified the email address, you may get this page:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*ipeE_-0Vtl7HyspvuqUoOQ.png" /><figcaption>Cloudflare: custom email address is verified</figcaption></figure><p>You probably will also get an email that you’ve verified your domain with Mailgun:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*icoIIDaTgOHbAZYsRZxUgw.png" /><figcaption>Notification about custom email address verification</figcaption></figure><h3>Receiving Emails into the Custom Email</h3><p>Now, your email address is activated, and you can see that here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*3WiVcV3QifAp0Wwf5kisgg.png" /><figcaption>Cloudflare: custom email address is active</figcaption></figure><p>At this point you already can send emails to the custom email you just set up. In this case, it’s “<em>email@boolfalse.com</em>”.<br>Below is a test email sent from a different email.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*fn24qboRSfDkikxXxefXQw.png" /><figcaption>Testing email receiving</figcaption></figure><p>You’ll receive a test email to the custom email.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0esM6_F2TW_WcsaGvns5TQ.png" /><figcaption>Test email has been received</figcaption></figure><h3>Mailgun: Adding New Domain</h3><p>You can now successfully receive emails, but you can’t send emails from that custom email yet.</p><p>So, it’s time to switch to the Mail Service provider. In our case it will be <a href="https://www.mailgun.com/">Mailgun</a>.<br>To do this, you just need to register and attach the card to your Mailgun account. After activating your account with the card attached, you can set up a domain for your email.</p><p>You don’t have to worry about the card, because Mailgun does not charge for limited quantities. I think the amount it gives is quite suitable for a free package.<br>You can find the price packages in detail <a href="https://www.mailgun.com/pricing/">here</a>.</p><p>Go to “<em>Sending</em>” -&gt; “<em>Domains</em>” page, and click the “Add New Domain” button.</p><p>In our case it will be “<em>mg.boolfalse.com</em>”, as Mailgun recommends to use like that to be able to send emails from your root domain, e.g. “<em>email@boolfalse.com</em>”.<br>You can see that recommendation on the right in below image:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*SqHyZm4RyC3SuTOPBFYQXw.png" /><figcaption>Mailgun: create a new domain</figcaption></figure><p>You can also select the domain region and DCIM key length, but you can leave everything as default.<br>I will leave DCIM key lenght as 1024 and “US” as a domain region.</p><p>After creating the domain, you may be shown some tips on how to verify your domain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*Q23V5m8cQnF765Hp7Yyfxw.png" /><figcaption>Mailgun: adding a new domain</figcaption></figure><p>Mailgun will give you two TXT records, two MX records and one CNAME record to add to your provider.</p><ul><li>Type: <strong><em>TXT</em></strong>; Name: <strong><em>mailto._domainkey.mg.boolfalse.com</em></strong>; TTL: <strong><em>Auto</em></strong>; Content: <strong><em>&lt;SECRET&gt;</em></strong></li><li>Type: <strong><em>TXT</em></strong>; Name: <strong><em>mg.boolfalse.com</em></strong>; TTL: <strong><em>Auto</em></strong>; Content: <strong><em>v=spf1 include:mailgun.org ~all</em></strong></li><li>Type: <strong><em>MX</em></strong>; Name: <strong><em>mg.boolfalse.com</em></strong>; Mail Server: <strong><em>mxa.mailgun.org</em></strong>; TTL: <strong><em>Auto</em></strong>; Priority: <strong><em>10</em></strong></li><li>Type: <strong><em>MX</em></strong>; Name <strong><em>mg.boolfalse.com</em></strong>; Mail Server: <strong><em>mxb.mailgun.org</em></strong>; TTL: Auto; Priority: <strong><em>10</em></strong></li><li>Type: <strong><em>CNAME</em></strong>; Name: <strong><em>email</em></strong>; Target: <strong><em>mailgun.org</em></strong>; TTL: <strong><em>Auto</em></strong>; Proxy Status: <strong><em>On</em></strong></li></ul><p>In our case, we will add them to Cloudflare.</p><p>Below is the first TXT record:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*m5hJr8sEkFCaxtC_QSFYPg.png" /><figcaption>Mailgun: first TXT record for a new domain</figcaption></figure><p>Below is the second TXT record:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*ou0ewxnBmnQUaqEvj7BmBg.png" /><figcaption>Mailgun: second TXT record for a new domain</figcaption></figure><p>Below is the first MX record:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*AUm8-KxV_f4UdLy-HIMAXQ.png" /><figcaption>Mailgun: first MX record for a new domain</figcaption></figure><p>Below is the second MX record:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*Bq4UTU0FJ3bpR9MdIysbWw.png" /><figcaption>Mailgun: second MX record for a new domain</figcaption></figure><p>After you’ve added two TXT and two MX records, you can check and verify them by clicking the “Verify DNS Records” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*OuloYtvB1tK5Fou6FQXXNw.png" /><figcaption>Mailgun: checking TXT and MX records for a new domain</figcaption></figure><p>Lastly, add CNAME record.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*os8vxsdu8DS_Eb098rxlmg.png" /><figcaption>Mailgun: adding CNAME record for a new domain</figcaption></figure><p>You may see a warning icon on the left of the CNAME record. You don’t need to worry about that, cause what <a href="https://developers.cloudflare.com/ssl/edge-certificates/additional-options/total-tls/error-messages">official documentation says</a> about it:</p><blockquote>If you recently <a href="https://developers.cloudflare.com/fundamentals/setup/manage-domains/add-site/">added your domain</a> to Cloudflare — meaning that your zone is in a <a href="https://developers.cloudflare.com/dns/zone-setups/reference/domain-status/">pending state</a> — you can often ignore this warning.<br>Once most domains becomes <strong>Active</strong>, Cloudflare will automatically issue a Universal SSL certificate, which will provide SSL/TLS coverage and remove the warning message.</blockquote><p>After adding a CNAME record, you can check and verify it again by clicking the second “Verify DNS Records” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*yTS40r8Y0is6VDrwb4UO5g.png" /><figcaption>Mailgun: checking CNAME record for a new domain</figcaption></figure><p>If you have added all 5 records on the Cloudflare successfully, after clicking the verifying button, Mailgun will automatically redirect you to the “<em>Overview</em>” page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*srJbUItf04nWYXLsO1n0Xg.png" /><figcaption>Mailgun: 2 TXT, 2 MX and 1 CNAME records added for a new domain</figcaption></figure><p>It means you’re ready to add a Sending API key on Mailgun.</p><h3>Mailgun: Sending API key &amp; SMPT User</h3><p>Go to “<em>Sending</em>” -&gt; “<em>Domain Settings</em>” page. Choose the “<em>Sending API keys</em>” tab at the top. Probably you won’t see any API keys there. You just need to create a new Sending API key. Click “Add sending key” from the top right corner, and in the popup fill the name of the key you about to create.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*996HLpQNO8Yz9cbnAXkeCA.png" /><figcaption>Mailgun: creating a Sending API key</figcaption></figure><p>After pressing “Create sending key”, you’ll get the secret API key that you need to copy and save somewhere safe. After saving the key, you can just close the popup.<br>You’ll see the created key listed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*aGRYxke_luibHF2BjU7Ojw.png" /><figcaption>Mailgun: Sending API key created</figcaption></figure><p>You also need to create a new SMTP user in Mailgun dashbaord.<br>Go to “<em>Sending</em>” -&gt; “<em>Domain Settings</em>” page. Choose the “<em>SMTP credentials</em>” tab at the top and press “Add new SMTP user” button on the top left corner. It will open up a popup. Type user credentials there. In our case I’ll create a user with the name “<em>email</em>”. It will be like a login for your email on Gmail.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*M_MYrCrve4SfvTvfDGSbQA.png" /><figcaption>Mailgun: creating SMTP user</figcaption></figure><p>Once you create an SMTP user in Mailgun, you’ll see it listed and a password for that user will be generated automatically. To get this password, copy it by clicking the “Copy” button in the pop-up notification in the lower right corner.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*r3Vmu6VpJepTo8RV4mMftA.png" /><figcaption>Mailgun: SMTP user created</figcaption></figure><p>Keep this in a safe place for future use. You will need this login and password to authenticate on Gmail.</p><p>You are now ready to set up email configurations with your email provider. In our case, we will do this in Gmail.<br>Open your Gmail account in your desktop browser and go to Settings by clicking the settings icon in the top right corner and click the “See all settings” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*rrCy_3hp8iRt00STepgxHw.png" /><figcaption>Mailgun: new domain is verified</figcaption></figure><h3>Gmail Authentication with Mailgun SMTP Server</h3><p>In the Gmail settings page choose the “<em>Accounts and Import</em>” tab and click on the “Add another email address” from the “<em>Send mail as</em>” section:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*o_qaLtmebrQf9VpV--GORg.png" /><figcaption>Gmail: Settings</figcaption></figure><p>It will open a popup for the authentication. Use the login and the password you just got by creating an SMTP user on Mailgun. Make sure to fill out the credentials correctly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/470/1*8sjrsMkBOkj3NEFj2VYWqw.png" /><figcaption>Gmail: authenticate a new user using a created SMTP server on Mailgun</figcaption></figure><p>Submit the form by clicking the “Add Account” button.<br>It probably will ask you to save the username/password in your browser. It’s up to you.<br>And the last important thing here, that it will ask you to verify adding an account.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/467/1*ZxDC6QvXDumGghRJaWc_jw.png" /><figcaption>Gmail: authentication confirmation for a new user</figcaption></figure><p>For the verification, the confirmation email will be sent to your primary email.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*hwCoc4DKi8h0KH5Td97IWw.png" /><figcaption>Gmail: authentication verification email</figcaption></figure><p>You can either use the confirmation code to verify it using the pop-up window or simply follow the link provided in the confirmation email.<br>In this case, we’ll click on a link that will open the page, where will be asked to confirm. Click on “Confirm” and simply close the previously opened pop-up window without worrying.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*LuCFABk0JDbr2hWYlcGXnA.png" /><figcaption>Gmail: verifying the authentication</figcaption></figure><p>Now you’re ready to send and receive emails from the custom email you just created.</p><p>For sending an email from the custom email, you just need to choose that email as a sender email:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/824/1*khwaFgqm8nfd92SIAmiZqw.png" /><figcaption>Gmail: sending emails</figcaption></figure><p><strong>That’s it!</strong></p><p>An additional thing that may be useful to you is that you can set the custom email address you just created as the default address for sending emails from Gmail.<br>You can set this on the settings page in the “<em>Send mail as</em>” section:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*cgTo8Xxw-WCd5lLhl_WKNQ.png" /><figcaption>Gmail: Settings (default sender)</figcaption></figure><p>I hope this guide will be a good resource for setting up your custom email.</p><h3>Conclusion</h3><p>In this article, you learned how to set up your own email to manage emails in Gmail using Cloudflare Email and Mailgun.<br>In conclusion, it is worth noting that this choice of tools is not mandatory, other tools could be used instead, but the basic idea and logic would be similar.</p><p>You can check out my website at: <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Feel free to share this article. 😇</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=41ef4858fb04" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Use React.js with Laravel. Build a Tasklist app]]></title>
            <link>https://medium.com/@boolfalse/use-react-js-with-laravel-build-a-tasklist-app-625989d49868?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/625989d49868</guid>
            <category><![CDATA[boolfalse]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Tue, 02 Apr 2024 19:49:17 GMT</pubDate>
            <atom:updated>2024-04-04T01:06:01.739Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qPGSA8sZ1MLmOLmCnI5e8A.png" /></figure><p><strong>The original article initially was published on </strong><a href="https://www.freecodecamp.org/news/use-react-with-laravel/"><strong>freeCodeCamp</strong></a><strong>.<br></strong>After doing some changes, I decided to have this on Medium as well.<br>For those who are just curious: the full code for this article is on <a href="https://github.com/boolfalse/laravel-react-tasklist"><strong>GitHub⭐</strong></a>․</p><h3>Introduction</h3><p>You may have seen tutorials that help you build a simple React.js app that use some third-party API or a Node.js server as a backend. You could also use Laravel for this purpose and integrate it with React.</p><p>As a backend framework, Laravel actually offers a tool to help you do this, called <a href="https://laravel.com/docs/10.x/frontend#inertia">Inertia</a>. Here’s what the docs say about it:</p><blockquote>It bridges the gap between your Laravel application and your modern Vue or React frontend, allowing you to build full-fledged, modern frontends using Vue or React while leveraging Laravel routes and controllers for routing, data hydration, and authentication — all within a single code repository.</blockquote><p>But what if you don’t want to use such a tool? And instead, you just want to use React.js as a frontend library and have a simple Laravel-powered backend?</p><p>Well, in this article, you will learn how to use React.js with Laravel as a backend by building a draggable tasklist app.</p><p>For this full-stack single-page app, you’ll use <a href="https://vitejs.dev/">Vite.js</a> as your frontend build tool and the <a href="https://www.npmjs.com/package/react-beautiful-dnd">react-beautiful-dnd</a> package for draggable items.</p><p>By the end of this article, you will have a single-page app for managing tasks, which will look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gipaoojzeBeCL4nPbBF5Vw.png" /><figcaption>Captured from a local working project</figcaption></figure><p>In this article, we’ll create a dynamic page that will have a list of tasks, each of which will belong to a specific project. This way, the user will be able to select a project, and only the tasks of the selected project will be shown on the page. The user can also create a new task for the current project, as well as edit, delete and reorder tasks by dragging and dropping them.</p><h3>Prerequisites</h3><p>Before following along, it would be helpful to have a basic understanding of React.js, Laravel, and familiarity with fundamental web development concepts.</p><p>You’ll need the following tools for the app that we’ll build in this article:</p><ul><li><strong>PHP</strong> 8.1 or above<br>Run php -vto check the version.</li><li><strong>Composer<br></strong>Run composerto check that it exists.</li><li><strong>Node.js</strong> 18 or above<br>Run node -vto check the version.</li><li><strong>MySQL</strong> 5.7 or above<br>Run mysql --versionto check if it exists, or follow the <a href="https://dev.mysql.com/doc/mysql-windows-excerpt/5.7/en/windows-testing.html">docs</a>.</li></ul><p>Additional (optional) tools that you can use:</p><ul><li><strong>Postman</strong> — a program with a UI for testing the API routes</li></ul><p>We’ll start by building out the backend, and then move to the frontend.</p><h3>The Backend: Install Laravel</h3><p>First, if you don’t have it already, you’ll need to install the Laravel framework on your local machine.</p><p>One way to install Laravel is by using Composer:</p><pre>composer create-project laravel/laravel tasklist</pre><p>This will install the latest stable version of Laravel in your local machine (currently it’s version 10).</p><p>At this point, you can cdinto the project’s folder and run the backend app without needing to have a virtual server set up:</p><pre>cd tasklist/ &amp;&amp; php artisan serve</pre><p>Visit http://127.0.0.1:8000in your browser to see the default page. It should look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YqKgv4CE2N0lr5DoA4hspw.png" /><figcaption>Laravel welcome page</figcaption></figure><h3>Create Models and Migrations</h3><p>Now, let’s create Projectand Taskmodels, as well as migrations for them.</p><p>You can create model and migration files manually as well as generate them using the artisancommand:</p><pre>php artisan make:model Project -m<br>php artisan make:model Task -m</pre><p>The -margument will automatically generate a migration file using the provided model name.</p><p>Keep the command execution sequence as it is, so the Project’s migration later can run before the Task’s migration.</p><p>This is important, because the projectsand tasks tables should have a one-to-many relationship (1-N): each task will refer to a single project, or, in other words, each project can have multiple tasks.</p><p>Set the Projectmodel’s $fillablefields and the task()relationship method as below:</p><pre>&lt;?php<br><br>namespace App\Models;<br><br>use Illuminate\Database\Eloquent\Factories\HasFactory;<br>use Illuminate\Database\Eloquent\Model;<br>use Illuminate\Database\Eloquent\Relations\HasMany;<br><br>class Project extends Model<br>{<br>    use HasFactory;<br><br>    protected $table = &#39;projects&#39;;<br>    public $timestamps = false;<br>    protected $fillable = [<br>        &#39;id&#39;, // primary key, auto-increment, integer<br>        &#39;name&#39;, // string<br>    ];<br><br>    // a project can have multiple tasks<br>    public function tasks(): HasMany<br>    {<br>        return $this-&gt;hasMany(Task::class);<br>    }<br>}</pre><p>By default, the $timestampspublic property has a truevalue, which is coming from the parent Modelclass. This means that the created_atand updated_atcolumns in your database table will be maintained automatically by Eloquent. We don’t need to have created_atand updated_atfields in the projectstable, so we’ll set the $timestampsto false.</p><p>Set the Taskmodel’s $fillablefields, project()relationship method, and createdaccessor.</p><pre>&lt;?php<br><br>namespace App\Models;<br><br>use Illuminate\Database\Eloquent\Factories\HasFactory;<br>use Illuminate\Database\Eloquent\Model;<br>use Illuminate\Database\Eloquent\Relations\BelongsTo;<br>use Illuminate\Database\Eloquent\SoftDeletes;<br><br>class Task extends Model<br>{<br>    use HasFactory, SoftDeletes;<br><br>    protected $table = &#39;tasks&#39;;<br>    protected $fillable = [<br>        &#39;id&#39;, // primary key, auto-increment, integer<br>        &#39;project_id&#39;, // foreign key, integer<br><br>        &#39;priority&#39;, // integer<br>        &#39;title&#39;, // string<br>        &#39;description&#39;, // text<br>    ];<br>    protected $appends = [<br>        &#39;created&#39;,<br>    ];<br><br>    // each task belongs to a single project<br>    public function project(): BelongsTo<br>    {<br>        return $this-&gt;belongsTo(Project::class);<br>    }<br><br>    public function getCreatedAttribute()<br>    {<br>        return $this-&gt;created_at-&gt;diffForHumans();<br>    }<br>}</pre><p>Above, in the Taskmodel, there is an accessor called created. For having an accessor, we have the createdfield in the $appendsarray, and also a public function getCreatedAttribute().</p><p>Now that the models are ready, let’s set up the migrations.</p><p>First, set up a migration for the projectstable:</p><pre>&lt;?php<br><br>use Illuminate\Database\Migrations\Migration;<br>use Illuminate\Database\Schema\Blueprint;<br>use Illuminate\Support\Facades\Schema;<br><br>return new class extends Migration<br>{<br>    public function up(): void<br>    {<br>        Schema::create(&#39;projects&#39;, function (Blueprint $table) {<br>            $table-&gt;id();<br>            $table-&gt;string(&#39;name&#39;);<br>        });<br>    }<br><br>    public function down(): void<br>    {<br>        Schema::dropIfExists(&#39;projects&#39;);<br>    }<br>};</pre><p>Then set up a migration for the taskstable:</p><pre>&lt;?php<br><br>use Illuminate\Database\Migrations\Migration;<br>use Illuminate\Database\Schema\Blueprint;<br>use Illuminate\Support\Facades\Schema;<br><br>return new class extends Migration<br>{<br>    public function up(): void<br>    {<br>        Schema::create(&#39;tasks&#39;, function (Blueprint $table) {<br>            $table-&gt;id();<br><br>            $table-&gt;foreignId(&#39;project_id&#39;)-&gt;nullable()-&gt;constrained();<br><br>            $table-&gt;integer(&#39;priority&#39;);<br>            $table-&gt;string(&#39;title&#39;);<br>            $table-&gt;text(&#39;description&#39;)-&gt;nullable();<br><br>            $table-&gt;timestamps();<br>            $table-&gt;softDeletes();<br>        });<br>    }<br><br>    public function down(): void<br>    {<br>        // drop existing foreign keys<br>        Schema::table(&#39;tasks&#39;, function (Blueprint $table) {<br>            if (Schema::hasColumn(&#39;tasks&#39;, &#39;project_id&#39;)) {<br>                $table-&gt;dropForeign([&#39;project_id&#39;]);<br>            }<br>        });<br><br>        // drop the table<br>        Schema::dropIfExists(&#39;tasks&#39;);<br>    }<br>};</pre><p>The taskstable has a foreign key project_id, which is a reference to the projectstable. So it’s a good practice to update the down()method too, to be sure that the project_idforeign will be dropped before dropping the actual projectstable.</p><p>There is also a priorityfield, which will be a non-nullable natural number for ordering the tasks. And optionally, you can add a soft deletion feature to the Taskmodel.</p><h3>Create Seeders</h3><p>Now we need to add dummy data to the projectsand taskstables. To seed some data in the database, you can use Laravel seeders:</p><pre>php artisan make:seeder ProjectsSeeder<br>php artisan make:seeder TasksSeeder</pre><p>At first, you’ll need to set up the ProjectsSeederto add a few projects to the projectstable. Then you can set up the TasksSeederto add tasks to the taskstable.</p><p>You can imagine the database structure by looking at the following visuals:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BoxoOFMjvSPASWUZ3tfNIw.png" /><figcaption>Generated by PHPStorm IDE</figcaption></figure><p>Using the example below, you can generate 3 projects:</p><pre>&lt;?php<br><br>namespace Database\Seeders;<br><br>use App\Models\Project;<br>use Illuminate\Database\Seeder;<br><br>class ProjectsSeeder extends Seeder<br>{<br>    public function run(): void<br>    {<br>        for ($i = 1; $i &lt;= 3; $i++) {<br>            Project::create([<br>                &#39;name&#39; =&gt; &quot;Project $i&quot;,<br>            ]);<br>        }<br>    }<br>}</pre><p>Next, set up TasksSeeder. You’ll run all the seeder files after setting them up, and they will run one by one. That being said, at this point your ProjectsSeederis ready to create a few projects.</p><p>The next step will be generating the tasks, each of them will have a reference to one of the already existing projects by its project_idfield.</p><p>Using the example below, you can generate 10 projects:</p><pre>&lt;?php<br><br>namespace Database\Seeders;<br><br>use App\Models\Project;<br>use App\Models\Task;<br>use Illuminate\Database\Seeder;<br><br>class TasksSeeder extends Seeder<br>{<br>    public function run(): void<br>    {<br>        $project_ids = Project::all()-&gt;pluck(&#39;id&#39;)-&gt;toArray();<br><br>        $now = now();<br>        $tasks = [];<br>        $project_priorities = [];<br>        foreach ($project_ids as $project_id) {<br>            $project_priorities[$project_id] = 0;<br>        }<br><br>        for ($i = 1; $i &lt;= 10; $i++) {<br>            $project_id = $project_ids[array_rand($project_ids)];<br>            $project_priorities[$project_id]++;<br><br>            $tasks[] = [<br>                &#39;project_id&#39; =&gt; $project_id,<br>                &#39;priority&#39; =&gt; $project_priorities[$project_id],<br>                &#39;title&#39; =&gt; &quot;Task &quot; . $project_priorities[$project_id],<br>                &#39;description&#39; =&gt; &quot;Description for Task &quot; . $project_priorities[$project_id],<br>                <br>                &#39;created_at&#39; =&gt; $now,<br>                &#39;updated_at&#39; =&gt; $now,<br>            ];<br>        }<br><br>        Task::insert($tasks);<br>    }<br>}</pre><p>The above code just grabs all the project IDs, then randomly chooses a project for each task. In the end, it inserts all the tasks into the tasks table.</p><p>As you may have noticed, we’re inserting $tasksinto the taskstable using the insert()static function, which allows us to insert all the items into the database table with a single query.</p><p>But it has a downside as well: it doesn’t manage created_atand updated_atfields. That’s why there’s a need to set up those fields manually by assigning them the same $nowtimestamp.</p><p>Now, when you have all the seeders ready, you need to register them into the DatabaseSeeder.</p><pre>&lt;?php<br><br>namespace Database\Seeders;<br><br>use Illuminate\Database\Seeder;<br><br>class DatabaseSeeder extends Seeder<br>{<br>    public function run(): void<br>    {<br>        $this-&gt;call([<br>            ProjectsSeeder::class,<br>            TasksSeeder::class,<br>        ]);<br>    }<br>}</pre><h3>Connect to the MySQL Database</h3><p>Before running migrations and seeds, create a MySQL database and set up the appropriate credentials in the .envfile. If there is not a .env, then create it and paste the .env.examplefile’s content into it.</p><p>After setting up the database credentials, you’ll have these kinds of environment variables:</p><pre>DB_CONNECTION=mysql<br>DB_HOST=127.0.0.1<br>DB_PORT=3306<br>DB_DATABASE=&quot;&lt;DATABASE_NAME&gt;&quot;<br>DB_USERNAME=&quot;&lt;USERNAME&gt;&quot;<br>DB_PASSWORD=&quot;&lt;PASSWORD&gt;&quot;</pre><p>After setting up environment variables, optimize the cache:</p><pre>php artisan optimize</pre><p>Now you’ll be able to create projectsand taskstables in the MySQL database, setup their structure, and add initial records with a single command:</p><pre>php artisan migrate:fresh --seed</pre><p>In the above command, the migrate:freshargument will drop all tables from the database. Then it will execute the migratecommand, which will run your migrations to create projectsand taskstables appropriately.</p><p>With the--seedargument, it will run ProjectsSeederand TasksSeederafter the migrations. That being said, it will empty your database for you, and will create all the tables and fill all the necessary dummy data.</p><p>After running the command, you’ll have these kinds of database records:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H_7TuHKE2GhWTzjksFvmrQ.png" /><figcaption>A screenshot from the PHPStorm IDE</figcaption></figure><h3>Service Injection</h3><p>Now let’s create a controller and a service classes to manage all the task features, such as listing, creating, updating, deleting, and reordering the tasks.</p><p>At first, use the below command to generate a controller.</p><pre>php artisan make:controller TaskController</pre><p>In order not to place all the code in the controller, you can keep only the main logic in it, and move the other logic implementations to another class file.</p><p>Those classes are generally called <em>“services”</em>, and using service implementations in a controller method is called <strong>service injection</strong> (it comes from the term <em>dependency injection</em>).</p><p>One of the main advantages of using services is that it helps you create a maintainable codebase.</p><p>You can inject your service class into the controller’s construction method as an argument, so after each controller execution (when a controller’s __construct()method runs) you can initialize an object of service. This means that you can access your service’s functions right in your controller.</p><p>Now, let’s create two separate service classes, which will be used in the TaskController.</p><p>Manually create a app/Services/ProjectService.phpservice class, which will be responsible for the project-related logic.</p><pre>&lt;?php<br><br>namespace App\Services;<br><br>use App\Models\Project;<br>use Illuminate\Database\Eloquent\Collection;<br><br>class ProjectService<br>{<br>    public function getAll(): Collection<br>    {<br>        return Project::all();<br>    }<br>}</pre><p>The second service class will be the app/Services/TaskService.php, which will be responsible for doing task manipulations:</p><pre>&lt;?php<br><br>namespace App\Services;<br><br>use App\Models\Task;<br>use Illuminate\Support\Facades\DB;<br><br>class TaskService<br>{<br>    public function list(int $projectId)<br>    {<br>        return Task::with(&#39;project&#39;)-&gt;where(&#39;project_id&#39;, $projectId)<br>            -&gt;orderBy(&#39;priority&#39;)-&gt;get();<br>    }<br><br>    public function getById(int $id)<br>    {<br>        return Task::where(&#39;id&#39;, $id)-&gt;with(&#39;project&#39;)-&gt;first();<br>    }<br><br>    public function store($data): void<br>    {<br>        $count = Task::where(&#39;project_id&#39;, $data[&#39;project_id&#39;])-&gt;count();<br>        $data[&#39;priority&#39;] = $count + 1;<br><br>        Task::create($data);<br>    }<br><br>    public function update(int $id, array $data): void<br>    {<br>        $task = $this-&gt;getById($id);<br>        if (!$task) { return; }<br><br>        $task-&gt;update($data);<br>    }<br><br>    public function delete(int $id): void<br>    {<br>        $task = $this-&gt;getById($id);<br>        if (!$task) { return; }<br>        <br>        $task-&gt;delete();<br><br>        $tasks = Task::where(&#39;project_id&#39;, $task-&gt;project_id)<br>            -&gt;where(&#39;priority&#39;, &#39;&gt;&#39;, $task-&gt;priority)-&gt;get();<br>        if ($tasks-&gt;isEmpty()) {<br>            return;<br>        }<br><br>        $when_then = &quot;&quot;;<br>        $where_in = &quot;&quot;;<br>        foreach ($tasks as $task) {<br>            $when_then .= &quot;WHEN &quot;.$task-&gt;id<br>                .&quot; THEN &quot;.($task-&gt;priority - 1).&quot; &quot;;<br>            $where_in .= $task-&gt;id.&quot;,&quot;;<br>        }<br><br>        $table_name = (new Task())-&gt;getTable();<br>        $bulk_update_query = &quot;UPDATE `&quot;.$table_name<br>            .&quot;` SET `priority` = (CASE `id` &quot;.$when_then.&quot;END)&quot;<br>            .&quot; WHERE `id` IN(&quot;.substr($where_in, 0, -1).&quot;);&quot;;<br><br>        // there is no way to be SQL injected here<br>        // because all the values are not provided by the user<br>        DB::update($bulk_update_query);<br>    }<br><br>    public function reorder(int $project_id, int $start, int $end): void<br>    {<br>        $items = Task::where(&#39;project_id&#39;, $project_id)<br>            -&gt;orderBy(&#39;priority&#39;)-&gt;pluck(&#39;priority&#39;, &#39;id&#39;)-&gt;toArray();<br><br>        if ($start &gt; count($items) || $end &gt; count($items)) {<br>            return;<br>        }<br><br>        $ids = [];<br>        $priorities = [];<br>        foreach ($items as $id =&gt; $priority) {<br>            $ids[] = $id;<br>            $priorities[] = $priority;<br>        }<br><br>        $out_priority = array_splice($priorities, $start - 1, 1);<br>        array_splice($priorities, $end - 1, 0, $out_priority);<br><br>        $when_then = &quot;&quot;;<br>        $where_in = &quot;&quot;;<br>        foreach ($priorities as $out_k =&gt; $out_v) {<br>            $id = $ids[$out_v - 1];<br>            $when_then .= &quot;WHEN &quot;.$id.&quot; THEN &quot;.($out_k + 1).&quot; &quot;;<br>            $where_in .= $id.&quot;,&quot;;<br>        }<br><br>        $table_name = (new Task())-&gt;getTable();<br>        $bulk_update_query = &quot;UPDATE `&quot;.$table_name<br>            .&quot;` SET `priority` = (CASE `id` &quot;.$when_then.&quot;END)&quot;<br>            .&quot; WHERE `id` IN(&quot;.substr($where_in, 0, -1).&quot;)&quot;<br>            .&quot; AND `deleted_at` IS NULL;&quot;; // soft delete<br><br>        DB::update($bulk_update_query);<br>    }<br>}</pre><p>In the above TaskServiceclass, you’ll use the following functions in the TaskController.</p><ul><li><strong>list:</strong> fetches tasks for a given project ID, including the related project, and orders them by <em>priority</em>.</li><li><strong>getById:</strong> retrieves a specific task by its ID, including the related project.</li><li><strong>store:</strong> stores a new task, calculating the <em>priority</em> based on existing tasks for the same project.</li><li><strong>update:</strong> updates an existing task by its ID.</li><li><strong>delete:</strong> deletes a task by its ID and adjusts the priorities of remaining tasks in the same project.</li><li><strong>reorder:</strong> changes the priorities of tasks within a project, (handles soft delete as well with deleted_at is NULL).</li></ul><h3>Web and API Routes</h3><p>Now you can add routes to test the methods you’ve already written. In this project, we have a stateless app on the frontend which requests API routes for getting JSON data, so it will follow RESTful principles (GET, POST, PUT, DELETE methods). Only the initial HTML page will be retrieved as a whole web page.</p><p>So now, set up a route in routes/web.php for the initial single-page:</p><pre>&lt;?php<br><br>use Illuminate\Support\Facades\Route;<br>use App\Http\Controllers\TaskController;<br><br>Route::group([&#39;prefix&#39; =&gt; &#39;/&#39;, &#39;as&#39; =&gt; &#39;tasks.&#39;], function () {<br>    Route::get(&#39;/&#39;, [TaskController::class, &#39;index&#39;])-&gt;name(&#39;index&#39;);<br>});</pre><p>Set up API routes in routes/api.php like this:</p><pre>&lt;?php<br><br>use Illuminate\Support\Facades\Route;<br>use App\Http\Controllers\TaskController;<br><br>Route::group([&#39;prefix&#39; =&gt; &#39;/tasks&#39;, &#39;as&#39; =&gt; &#39;tasks.&#39;], function () {<br>    Route::get(&#39;/&#39;, [TaskController::class, &#39;list&#39;]);<br>    Route::get(&#39;/{id}&#39;, [TaskController::class, &#39;get&#39;])<br>  -&gt;where(&#39;id&#39;, &#39;[1-9][0-9]*&#39;);<br>    Route::post(&#39;/&#39;, [TaskController::class, &#39;store&#39;]);<br>    Route::put(&#39;/{id}&#39;, [TaskController::class, &#39;update&#39;])<br>     -&gt;where(&#39;id&#39;, &#39;[1-9][0-9]*&#39;);<br>    Route::delete(&#39;/{id}&#39;, [TaskController::class, &#39;delete&#39;])<br>     -&gt;where(&#39;id&#39;, &#39;[1-9][0-9]*&#39;);<br>    Route::put(&#39;/&#39;, [TaskController::class, &#39;reorder&#39;]);<br>});</pre><p>We have all the API routes in the routes/api.php instead of the routes/web.php, because in the web.phpfile all the routes by default are <a href="https://laravel.com/docs/10.x/routing#csrf-protection">CSRF protected</a>.</p><p>As you can see, there is a <em>task</em> prefix for all API routes. It’s optional to have a prefix, but it’s just a good practice. And for the specific API routes, there are regex validations for accepting only natural numbers as project IDs.</p><p>Don’t forget to refresh route caches after the above changes. It’s important to remember that Laravel (version 10 in this case) reads routes from the cached bootstrap/cache/routes-v7.php file, and they won’t be updated automatically right after your changes. It just generates one if it hasn’t cached yet.</p><p>Use the below command to refresh Laravel caches as well as the route caches:</p><pre>php artisan optimize</pre><h3>Requests Validation</h3><p>Before writing controller methods, you’ll need to add some validation request files. You can do that manually or by just using the artisan command:</p><pre>php artisan make:request Task/CreateTaskRequest<br>php artisan make:request Task/ListTasksRequest<br>php artisan make:request Task/ReorderTasksRequest<br>php artisan make:request Task/UpdateTaskRequest</pre><p>After creating them, you’ll need to set up validation rules for each request.</p><p>Below are the validation rules for creating a new task:</p><pre>return [<br>    &#39;project_id&#39; =&gt; &#39;required|integer|exists:projects,id&#39;,<br>    &#39;title&#39; =&gt; &#39;required|string|max:255&#39;,<br>    &#39;description&#39; =&gt; &#39;nullable|string&#39;,<br>];</pre><p>Below is the validation rule for listing project tasks:</p><pre>return [<br>    &#39;project_id&#39; =&gt; &#39;required|integer|exists:projects,id&#39;,<br>];</pre><p>Below are the validation rules for tasks reordering:</p><pre>return [<br>    &#39;project_id&#39; =&gt; &#39;required|integer|exists:projects,id&#39;,<br>    &#39;start&#39; =&gt; &#39;required|integer&#39;,<br>    &#39;end&#39; =&gt; &#39;required|integer|different:start&#39;,<br>];</pre><p>Below are the validation rules for updating a task:</p><pre>return [<br>    &#39;title&#39; =&gt; &#39;required|string|max:255&#39;,<br>    &#39;description&#39; =&gt; &#39;nullable|string&#39;,<br>];</pre><p>Don’t forget to return true in the authorize() method in all validation classes:</p><pre>public function authorize(): bool<br>{<br>    return true;<br>}</pre><p>This function is usually designed to determine if the user is authorized to make the request. As we don’t use authentication as well as authorization stuff in the app, it should return true for all the cases.</p><h3>Using Services in a Controller</h3><p>As the last step in the backend part, it’s time to write controller methods for each API route, which will use service functions.</p><pre>&lt;?php<br><br>namespace App\Http\Controllers;<br><br>use App\Http\Requests\Task\CreateTaskRequest;<br>use App\Http\Requests\Task\ListTasksRequest;<br>use App\Http\Requests\Task\ReorderTasksRequest;<br>use App\Http\Requests\Task\UpdateTaskRequest;<br>use App\Services\ProjectService;<br>use App\Services\TaskService;<br>use Illuminate\Http\JsonResponse;<br><br>class TaskController extends Controller<br>{<br>    protected ?TaskService $taskService = null;<br><br>    public function __construct(TaskService $taskService)<br>    {<br>        $this-&gt;taskService = $taskService;<br>    }<br><br>    public function index()<br>    {<br>        $projects = (new ProjectService())-&gt;getAll();<br><br>        return view(&#39;tasks.index&#39;, [<br>            &#39;projects&#39; =&gt; $projects,<br>        ]);<br>    }<br><br>    public function list(ListTasksRequest $request): JsonResponse<br>    {<br>        $tasks = $this-&gt;taskService-&gt;list($request-&gt;get(&#39;project_id&#39;));<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;tasks&#39; =&gt; $tasks,<br>            &#39;message&#39; =&gt; &quot;Tasks retrieved successfully.&quot;,<br>        ]); // 200<br>    }<br><br>    public function store(CreateTaskRequest $request): JsonResponse<br>    {<br>        $this-&gt;taskService-&gt;store($request-&gt;all());<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;message&#39; =&gt; &quot;Task created successfully.&quot;,<br>        ], 201);<br>    }<br><br>    public function get(int $id): JsonResponse<br>    {<br>        $task = $this-&gt;taskService-&gt;getById($id);<br><br>        if ($task) {<br>            return response()-&gt;json([<br>                &#39;success&#39; =&gt; true,<br>                &#39;task&#39; =&gt; $task,<br>                &#39;message&#39; =&gt; &quot;Task retrieved successfully.&quot;,<br>            ]); // 200<br>        } else {<br>            return response()-&gt;json([<br>                &#39;success&#39; =&gt; false,<br>                &#39;message&#39; =&gt; &quot;Task not found!&quot;,<br>            ], 404);<br>        }<br>    }<br><br>    public function update(UpdateTaskRequest $request, int $id): JsonResponse<br>    {<br>        $this-&gt;taskService-&gt;update($id, $request-&gt;all());<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;message&#39; =&gt; &quot;Task updated successfully.&quot;,<br>        ], 201);<br>    }<br><br>    public function delete(int $id): JsonResponse<br>    {<br>        $this-&gt;taskService-&gt;delete($id);<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;message&#39; =&gt; &quot;Task deleted successfully.&quot;,<br>        ], 201);<br>    }<br><br>    public function reorder(ReorderTasksRequest $request): JsonResponse<br>    {<br>        $this-&gt;taskService-&gt;reorder(<br>            $request-&gt;get(&#39;project_id&#39;),<br>            $request-&gt;get(&#39;start&#39;),<br>            $request-&gt;get(&#39;end&#39;)<br>        );<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;message&#39; =&gt; &quot;Tasks reordered successfully.&quot;,<br>        ], 201);<br>    }<br>}</pre><p>As you can see in the TaskController:</p><ul><li>TaskService is injected into the constructor method as an argument. In the constructor body, an instance of the TaskService class is created, and the $taskService property is initialized. So in the custom methods, you’ll be able to access that $taskService and its functions.</li><li>The index method is for returning the HTML.</li><li>All the other custom methods ( list, store, get, update, delete, reorder ) are using the TaskService functions through the already initialized $taskService property. So, all the logic implementation goes to the service, and this way, you just call a service function and return the response.</li></ul><h3>Test the API Routes</h3><p>At this point, you can test the API routes by requesting them via Postman or any similar tool. Just run (or rerun) the backend:</p><pre>php artisan serve</pre><p>Here’s the published <a href="https://documenter.getpostman.com/view/1747137/2s9YsJArg7">Postman collection</a> with all the requests.</p><h3>Frontend — Install the Packages</h3><p>Now it’s time to switch to the frontend. We’ll use TypeScript for React.js. After completing this part, you’ll be able to integrate React.js (with Vite) in your Laravel app.</p><p>First, make sure you have Node.js version 18 or above by using this command:</p><pre>node -v</pre><p>Install these necessary npm packages:</p><pre>npm i react-dom dotenv react-beautiful-dnd react-responsive-modal react-toastify @vitejs/plugin-react</pre><ul><li>react-dom is a library from the React team for rendering React components in the DOM (Document Object Model)</li><li>dotenv is for loading environment variables from the .env file into the process environment</li><li>react-beautiful-dnd is a library from Atlassian for creating drag-and-drop interfaces with animations</li><li>react-responsive-modal is for creating simple and responsive modal dialogs</li><li>react-toastify is for displaying notifications or toasts</li><li>@vitejs/plugin-react is a plugin for the Vite build tool that enables seamless integration of React with fast development and optimized production builds</li></ul><p>Install the development dependencies with this command:</p><pre>npm i -D @types/react-dom @types/react-beautiful-dnd</pre><ul><li>@types/react-dom is TypeScript type definitions for the react-dom package</li><li>@types/react-beautiful-dnd is TypeScript type definitions for the react-beautiful-dnd package</li></ul><h3>Vite Configuration</h3><p>As Laravel v10 already has vite.config.js, you’ll want to set up any React-related stuff there. Or if you still don’t have this file, create one like this:</p><pre>import { defineConfig } from &#39;vite&#39;;<br>import laravel from &#39;laravel-vite-plugin&#39;;<br>import react from &#39;@vitejs/plugin-react&#39;;<br>import &#39;dotenv/config&#39;;<br><br>export default defineConfig({<br>    build: {<br>        minify: process.env.APP_ENV === &#39;production&#39; ? &#39;esbuild&#39; : false,<br>        cssMinify: process.env.APP_ENV === &#39;production&#39;,<br>    },<br>    plugins: [<br>        laravel({<br>            input: [&#39;resources/react/app.tsx&#39;],<br>            refresh: true,<br>        }),<br>        react(),<br>    ],<br>});</pre><p>As you can see in the Vite configuration file, there is a reference to the resources/react/app.tsx, which will be the entry point for Laravel to use React resources.</p><p>For the initial HTML page, create a resources/views/react/index.blade.php blade file, so all the frontend assets will be injected there in the div with ID app:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;{{ str_replace(&#39;_&#39;, &#39;-&#39;, app()-&gt;getLocale()) }}&quot;&gt;<br>&lt;head&gt;<br>    &lt;meta charset=&quot;utf-8&quot;&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;<br>    &lt;title&gt;{{ config(&quot;app.name&quot;) }}&lt;/title&gt;<br>    &lt;link rel=&quot;shortcut icon&quot; href=&quot;{{ asset(&#39;favicon.ico&#39;) }}&quot; /&gt;<br>    &lt;link rel=&quot;stylesheet&quot; href=&quot;https://fonts.googleapis.com/css?family=Montserrat&quot;&gt;<br>    &lt;link rel=&quot;stylesheet&quot; href=&quot;https://fonts.googleapis.com/icon?family=Material+Icons&quot;&gt;<br>    @viteReactRefresh<br>    @vite(&#39;resources/react/app.tsx&#39;)<br>&lt;/head&gt;<br>&lt;body&gt;<br>&lt;div id=&quot;app&quot; data-projects=&quot;{{ json_encode($projects) }}&quot;&gt;&lt;/div&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>As you can see in the blade file, there is a $projects variable passed from the backend. It’s the whole project data that will be used to filter tasks in the frontend.</p><p>Also, there are two directives: @viteReactRefresh and @vite(&#39;resources/react/app.tsx&#39;). The first one is for enabling React hot module replacement, and the second one is for injecting the React app’s entry point.</p><h3>React.js — Initial Integration</h3><p>In this article, we’ll just have a basic React.js app working with Laravel.</p><p>At first, it’s a good idea to delete unnecessary resources, like default resources/css and resources/js directories.</p><p>Create a resources/react/app.tsx file like this:</p><pre>import ReactDOM from &#39;react-dom/client&#39;;<br>import Main from &quot;./Main&quot;;<br>import &#39;./index.css&#39;<br><br>ReactDOM.createRoot(document.getElementById(&#39;app&#39;)).render(<br>    &lt;Main /&gt;<br>);</pre><p>So, resources/react folder will be the root directory for all the upcoming React stuff.</p><p>Create an index.css with some temporary content:</p><pre>.test-class {<br>  color: red;<br>}</pre><p>Also create a Main.tsx with some temporary content:</p><pre>function Main() {<br>    return (<br>        &lt;div&gt;<br>            &lt;h2 className=&quot;test-class&quot;&gt;React App&lt;/h2&gt;<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default Main;</pre><p>To check the result in the browser, make sure you have backend running and build the assets via the vite tool:</p><pre>npm run build</pre><p>Or, if you want to watch React file changes and automatically build assets, you can keep this command running:</p><pre>npm run dev</pre><p>The two npm runcommands above refer to vite, which builds the assets.<br>You can see this by checking the package.json file, scripsfield:</p><pre>&quot;scripts&quot;: {<br>    &quot;dev&quot;: &quot;vite&quot;,<br>    &quot;build&quot;: &quot;vite build&quot;<br>}</pre><p>Now you can open http://localhost:8000 to see the initial rendered view:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/465/1*rSdlRXFlmBJQVjeu07okdQ.png" /><figcaption>Screenshot from browser</figcaption></figure><h3>Adding CSS</h3><p>Now, once you’ve set up Vite and have React integrated into your Laravel app, you can work on the React part.</p><p>We won’t spend too much time on styles, so you can paste this CSS into your index.css:</p><pre>body { background-color: whitesmoke; color: rgba(255, 255, 255, 0.7); font-family: &quot;Montserrat&quot;, sans-serif; cursor: default; margin: auto 0; }<br><br>/* MODAL start */<br>.modal-content { display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #fff; padding: 20px; border-radius: 5px; width: 500px; color: #2d3748; }<br>.modal-header { font-size: 1.5rem; font-weight: 600; margin-bottom: 20px; }<br>.modal-input-header { font-weight: 600; margin-bottom: 10px; }<br>.modal-input { width: 100%; height: 30px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; }<br>.modal-textarea { width: 100%; height: 100px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; margin-bottom: 20px; resize: vertical; }<br>.modal-actions { display: flex; justify-content: space-between; align-items: center; width: 100%; }<br>.modal-btn { padding: 10px 20px; border-radius: 5px; cursor: pointer; }<br>.modal-btn-cancel { background-color: #e53e3e; color: #fff; border: none; }<br>.modal-btn-cancel:hover { background-color: #c53030; }<br>.modal-btn-submit { background-color: #2d3748; color: #fff; border: none; }<br>.modal-btn-submit:hover { background-color: #4a5568; }<br>.modal-question { font-size: 1.2rem; font-weight: 600; margin-bottom: 20px; }<br>/* MODAL end */<br><br>/* LEFT &amp; RIGHT SIDE start */<br>.left-side { width: 50%; float: left; }<br>.right-side { width: 50%; float: left; }<br>/* LEFT &amp; RIGHT SIDE end */<br><br>/* LEFT SIDE start */<br>.left-side .no-tasks { font-size: 1.2rem; font-weight: 600; margin-bottom: 20px; text-align: center; color: #2d3748; }<br>.left-side .task-item { padding: 10px; margin: 10px; min-height: 20px; min-width: 200px; color: #2d3748; list-style-type: none; }<br>.left-side .task-item-content { display: flex; justify-content: space-between; align-items: center; }<br>.left-side .task-project { display: flex; flex-direction: column; justify-content: center; align-items: center; margin-right: 10px; }<br>.left-side .task-project-name { font-size: 1.5rem; font-weight: 600; margin-bottom: 5px; }<br>.left-side .task-time { font-size: 0.8rem; }<br>.left-side .task-title { font-size: 1.2rem; }<br>.left-side .task-actions { display: flex; justify-content: space-between; align-items: center; }<br>.left-side .task-edit-btn { background-color: #2d3748; color: #fff; border: none; border-radius: 5px; padding: 5px 10px; cursor: pointer; margin: 0 5px; }<br>.left-side .task-edit-btn:hover { background-color: #4a5568; }<br>.left-side .task-delete-btn { background-color: #e53e3e; color: #fff; border: none; border-radius: 5px; padding: 5px 10px; cursor: pointer; margin: 0 5px; }<br>.left-side .task-delete-btn:hover { background-color: #c53030; }<br>/* LEFT SIDE end */<br><br>/* RIGHT SIDE start */<br>.right-side .projects { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }<br>.right-side .projects-select { width: 100%; height: 30px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; }<br>.right-side .no-project-selected { font-size: 1.2rem; font-weight: 600; margin-bottom: 20px; }<br>.right-side .right-side-wrapper { display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #fff; padding: 20px; border-radius: 5px; color: #2d3748; }<br>.right-side .add-task-header { font-size: 1.5rem; font-weight: 600; margin-bottom: 20px; }<br>.right-side .add-task-input-header { font-weight: 600; margin-bottom: 10px; }<br>.right-side .add-task-input { width: 100%; height: 30px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; }<br>.right-side .add-task-textarea { width: 100%; height: 100px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; margin-bottom: 20px; resize: vertical; }<br>.right-side .add-task-actions { display: flex; justify-content: space-between; align-items: center; width: 100%; }<br>.right-side .add-task-btn { padding: 10px 20px; border-radius: 5px; cursor: pointer; }<br>.right-side .add-task-btn-cancel { background-color: #e53e3e; color: #fff; border: none; }<br>.right-side .add-task-btn-cancel:hover { background-color: #c53030; }<br>.right-side .add-task-btn-submit { background-color: #2d3748; color: #fff; border: none; }<br>.right-side .add-task-btn-submit:hover { background-color: #4a5568; }<br>/* RIGHT SIDE end */</pre><p>Later you’ll attach the index.css file in your main component.</p><h3>A Service for the API Requests</h3><p>As you did in backend, here in the frontend you also can move all the logic implementations into a different file, so your code will be more readable and maintainable. We can name that file utils.ts, as there will be utilities in it we need.</p><p>Before that, just create axiosConfig.ts for the global Axios configuration, which you’ll use in utils.ts.</p><pre>import axios from &#39;axios&#39;;<br><br>export default axios.create({ baseURL: &#39;/api&#39; });</pre><p>Using the above setup, you can be sure that all the HTTP requests will have the /api prefix.</p><p>For example, if you use axiosConfig.get(&#39;/example&#39;), it will send a GET request to the /api/example. This is an optional configuration, but it’s a recommended way to have non-repetitive code.</p><p>As you’ll have a few use cases for sending HTTP requests to the server, you can have separate utilities file for those operations:</p><ul><li>Create a new task for a project</li><li>Update a task</li><li>List project’s tasks</li><li>Delete a task</li><li>Reorder project’s tasks</li></ul><p>So below is the utils.ts file:</p><pre>import axiosConfig from &#39;./axiosConfig&#39;;<br>import { toast } from &#39;react-toastify&#39;;<br><br>export const getErrorMessage = (error: unknown) =&gt; {<br>    if (error instanceof Error) return error.message;<br>    return String(error)<br>}<br><br>export const getTasks = async (projectId) =&gt; {<br>    if (!projectId) {<br>        toast.error(&quot;Project is required!&quot;);<br>        return;<br>    }<br><br>    try {<br>        const response = await axiosConfig.get(`/tasks?project_id=${projectId}`);<br>        const { success, tasks, message } = response.data;<br><br>        if (success) {<br>            return tasks;<br>        } else {<br>            toast.error(message);<br>            return [];<br>        }<br>    } catch (err) {<br>        toast.error(getErrorMessage(err));<br>        return [];<br>    }<br>}<br><br>export const reorderTasks = async (projectId, start, end) =&gt; {<br>    try {<br>        const response = await axiosConfig.put(&#39;/tasks&#39;, {<br>            project_id: projectId,<br>            start,<br>            end,<br>        });<br>        const { success, message } = response.data;<br>        <br>        toast[success ? &#39;success&#39; : &#39;error&#39;](message);<br>    } catch (err) {<br>        toast.error(getErrorMessage(err));<br>    }<br>}<br><br>export const editTask = async (task) =&gt; {<br>    if (!task.id) return;<br>    if (!task.title) {<br>        toast.error(&quot;Title is required!&quot;);<br>        return;<br>    }<br><br>    try {<br>        const response = await axiosConfig.put(`/tasks/${task.id}`, {<br>            title: task.title,<br>            description: task.description,<br>        });<br>        const { success, message } = response.data;<br><br>        toast[success ? &#39;success&#39; : &#39;error&#39;](message);<br>    } catch (err) {<br>        toast.error(getErrorMessage(err));<br>    }<br>}<br><br>export const deleteTask = async (id) =&gt; {<br>    if (!id) {<br>        toast.error(&quot;Invalid task!&quot;);<br>        return;<br>    }<br><br>    try {<br>        const response = await axiosConfig.delete(`/tasks/${id}`);<br>        const { success, message } = response.data;<br><br>        toast[success ? &#39;success&#39; : &#39;error&#39;](message);<br>    } catch (err) {<br>        toast.error(getErrorMessage(err));<br>    }<br>}<br><br>export const createTask = async (task, projectId) =&gt; {<br>    if (!projectId) {<br>        toast.error(&quot;Project is required!&quot;);<br>        return;<br>    }<br>    if (!task.title) {<br>        toast.error(&quot;Title is required!&quot;);<br>        return;<br>    }<br><br>    try {<br>        const response = await axiosConfig.post(`/tasks?project_id=${projectId}`, {<br>            title: task.title,<br>            description: task.description,<br>        });<br>        const { success, message } = response.data;<br><br>        toast[success ? &#39;success&#39; : &#39;error&#39;](message);<br>    } catch (err) {<br>        toast.error(getErrorMessage(err));<br>    }<br>}</pre><p>In the above file, you’ll find the following functions:</p><ul><li><strong><em>getErrorMessage</em></strong>: Returns the error message if the input is an instance of Error — otherwise, converts it to a string.</li><li><strong><em>getTasks</em></strong>: Retrieves tasks for a given project ID using Axios. Displays an error toast if the project ID is missing or if the API request is unsuccessful.</li><li><strong><em>reorderTasks</em></strong>: Sends a PUT request to reorder tasks within a project based on start and end positions. Displays a success or error toast based on the API response.</li><li><strong><em>editTask</em></strong>: Sends a PUT request to update task information. Validates that the task has an ID and a title before making the request. Displays a success or error toast based on the API response.</li><li><strong><em>deleteTask</em></strong>: Sends a DELETE request to delete a task by its ID. Displays a success or error toast based on the API response.</li><li><strong><em>createTask</em></strong>: Sends a POST request to create a new task for a given project ID. Validates that the project ID is present, and the task has a title before making the request. Displays a success or error toast based on the API response.</li></ul><h3>React.js Components</h3><p>Now, since you have utilities ready, in the resources/react/components folder, you can create the components you need to use in your Main.tsx.</p><p>First, create SelectProject.tsx, which will be responsible for choosing the current project:</p><pre>import {getTasks} from &quot;../utils&quot;;<br><br>function SelectProject({projectId, projects, setProjectId, setTasks}) {<br>    const selectProject = (e) =&gt; {<br>        const value = e.target.value;<br>        setProjectId(value);<br>        if (value === &#39;&#39;) {<br>            setTasks([]);<br>        } else {<br>            getTasks(value).then((tasksData) =&gt; setTasks(tasksData));<br>        }<br>    };<br><br>    return (<br>        &lt;div className=&quot;projects&quot;&gt;<br>            &lt;select className=&quot;projects-select&quot;<br>                    value={projectId}<br>                    onChange={selectProject}&gt;<br>                &lt;option value=&quot;&quot; defaultValue&gt;Choose a project&lt;/option&gt;<br>                {projects.map((project) =&gt; (<br>                    &lt;option key={project.id}<br>                            value={project.id}&gt;{project.name}&lt;/option&gt;<br>                ))}<br>            &lt;/select&gt;<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default SelectProject;</pre><p>The SelectProject component renders a dropdown menu allowing the user to select a project. When a project is selected, it updates the state with the selected project ID, fetches tasks for that project using the getTasks utility function, and updates the state with the retrieved tasks, providing dynamic interaction with project selection and task loading.</p><p>Then create TaskList.tsx, which will be responsible for rendering the project’s tasks and for their drag and drop manipulations:</p><pre>import {DragDropContext, Draggable, Droppable} from &quot;react-beautiful-dnd&quot;;<br>import Task from &quot;./Task&quot;;<br>import {reorderTasks} from &quot;../utils&quot;;<br><br>const getItemStyle = (isDragging, draggableStyle) =&gt; ({<br>    background: isDragging ? &#39;lightgreen&#39; : &#39;grey&#39;,<br>    ...draggableStyle,<br>});<br><br>function TaskList({ tasks, setIsModalEditOpen, setModalEditTask, setIsModalDeleteOpen, setModalDeleteTaskId, projectId, setTasks })<br>{<br>    const handleDragEnd = (result) =&gt; {<br>        if (!result.destination || result.destination.index === result.source.index) {<br>            return;<br>        }<br><br>        const items = Array.from(tasks);<br>        const [reorderedItem] = items.splice(result.source.index, 1);<br>        items.splice(result.destination.index, 0, reorderedItem);<br>        reorderTasks(projectId, result.source.index + 1, result.destination.index + 1);<br><br>        setTasks(items);<br>    };<br><br>    return (<br>        &lt;DragDropContext onDragEnd={handleDragEnd}&gt;<br>            &lt;Droppable droppableId=&quot;droppable&quot;&gt;<br>                {(provided) =&gt; (<br>                    &lt;ul {...provided.droppableProps} ref={provided.innerRef}&gt;<br>                        {tasks.map((task, index) =&gt; (<br>                            &lt;Draggable key={task.id.toString()} draggableId={task.id.toString()} index={index}&gt;<br>                                {(provided, snapshot) =&gt; (<br>                                    &lt;li ref={provided.innerRef}<br>                                        {...provided.draggableProps}<br>                                        {...provided.dragHandleProps}<br>                                        className=&quot;task-item&quot;<br>                                        style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}<br>                                    &gt;<br>                                        &lt;Task task={task}<br>                                              setIsModalEditOpen={setIsModalEditOpen}<br>                                              setModalEditTask={setModalEditTask}<br>                                              setIsModalDeleteOpen={setIsModalDeleteOpen}<br>                                              setModalDeleteTaskId={setModalDeleteTaskId}<br>                                        /&gt;<br>                                    &lt;/li&gt;<br>                                )}<br>                            &lt;/Draggable&gt;<br>                        ))}<br>                        {provided.placeholder}<br>                    &lt;/ul&gt;<br>                )}<br>            &lt;/Droppable&gt;<br>        &lt;/DragDropContext&gt;<br>    );<br>}<br><br>export default TaskList;</pre><p>The TaskList component utilizes the react-beautiful-dnd library to implement a draggable task list. It renders a list of tasks, allowing users to drag and drop tasks to reorder them, with drag-and-drop functionality triggering a function (handleDragEnd()) that updates the task order both visually and in the backend using the reorderTasks utility function.</p><p>Now, create Task.tsx, which will be responsible for a single Task from a list:</p><pre>function Task({ task, setIsModalEditOpen, setModalEditTask, setIsModalDeleteOpen, setModalDeleteTaskId })<br>{<br>    const handleEdit = () =&gt; {<br>        setModalEditTask(task);<br>        setIsModalEditOpen(true);<br>    };<br><br>    const handleDelete = () =&gt; {<br>        setModalDeleteTaskId(task.id);<br>        setIsModalDeleteOpen(true);<br>    };<br><br>    return (<br>        &lt;div className=&quot;task-item-content&quot;&gt;<br>            &lt;span className=&quot;task-project&quot;&gt;<br>                &lt;span className=&quot;task-project-name&quot;&gt;<br>                 {task.project.name}<br>                &lt;/span&gt;<br>                &lt;span className=&quot;task-time&quot;&gt;<br>                 Created {task.created}<br>                &lt;/span&gt;<br>            &lt;/span&gt;<br>            &lt;span className=&quot;task-title&quot;&gt;{task.title}&lt;/span&gt;<br>            &lt;div className=&quot;task-actions&quot;&gt;<br>                &lt;button className=&quot;task-edit-btn&quot;<br>                 onClick={handleEdit}&gt;Edit&lt;/button&gt;<br>                &lt;button className=&quot;task-delete-btn&quot;<br>                 onClick={handleDelete}&gt;Delete&lt;/button&gt;<br>            &lt;/div&gt;<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default Task;</pre><p>The Task component represents a single task item. It displays task details including the project name, creation time, and title, and provides buttons to trigger actions such as editing and deleting the task, with corresponding handlers ( handleEdit and handleDelete).</p><p>Next, create AddTaskForm.tsx, which will be responsible for the task form for adding tasks to the current selected project:</p><pre>import {createTask} from &quot;../utils&quot;;<br><br>function AddTaskForm({newTask, setNewTask, projectId, reloadTasks })<br>{<br>    const clearTaskCreate = () =&gt; {<br>        setNewTask({title: &#39;&#39;, description: &#39;&#39;});<br>    };<br>    const submitTaskCreate = () =&gt; {<br>        createTask(newTask, projectId).then(() =&gt; {<br>            setNewTask({title: &#39;&#39;, description: &#39;&#39;});<br>            reloadTasks();<br>        });<br>    };<br><br>    return (<br>        &lt;&gt;<br>            &lt;h2 className=&quot;add-task-header&quot;&gt;Add Task&lt;/h2&gt;<br><br>            &lt;h3 className=&quot;add-task-header&quot;&gt;Title&lt;/h3&gt;<br>            &lt;input type=&quot;text&quot;<br>                   className=&quot;add-task-input&quot;<br>                   onChange={(e) =&gt; setNewTask({<br>                    ...newTask,<br>                    title: e.target.value<br>                   })}<br>                   value={newTask.title}<br>            /&gt;<br>            &lt;h3 className=&quot;add-task-input-header&quot;&gt;Description&lt;/h3&gt;<br>            &lt;textarea className=&quot;add-task-textarea&quot;<br>                      onChange={(e) =&gt; setNewTask({<br>                       ...newTask,<br>                        description: e.target.value<br>                      })}<br>                      value={newTask.description || &#39;&#39;}<br>            /&gt;<br>            &lt;div className=&quot;add-task-actions&quot;&gt;<br>                &lt;button className=&quot;add-task-btn add-task-btn-cancel&quot;<br>                        onClick={clearTaskCreate}&gt;Clear<br>                &lt;/button&gt;<br>                &lt;button className=&quot;add-task-btn add-task-btn-submit&quot;<br>                        onClick={submitTaskCreate}&gt;Add&lt;/button&gt;<br>            &lt;/div&gt;<br>        &lt;/&gt;<br>    );<br>}<br><br>export default AddTaskForm;</pre><p>The AddTaskForm component provides a form for adding new tasks. It includes input fields for the task title and description, with buttons to clear the form or submit the task creation, and it utilizes the createTask utility function to handle the task creation process, triggering a reload of tasks upon success.</p><p>Then, create ModalEdit.tsx, which will be responsible for the modal popup for editing and submitting changes to a task:</p><pre>import {Modal} from &quot;react-responsive-modal&quot;;<br>import React from &quot;react&quot;;<br>import {editTask} from &quot;../utils&quot;;<br><br>function ModalEdit({<br> isModalEditOpen, setIsModalEditOpen, setModalEditTask,<br>    modalEditTask, reloadTasks<br>}) {<br>    const submitTaskEdit = () =&gt; {<br>        setIsModalEditOpen(false);<br>        editTask(modalEditTask).then(() =&gt; {<br>            reloadTasks();<br>        });<br>    };<br><br>    return (<br>        &lt;Modal open={isModalEditOpen} center<br>         onClose={() =&gt; setIsModalEditOpen(false)}&gt;<br>            &lt;div className=&quot;modal-content&quot;&gt;<br>                &lt;h2 className=&quot;modal-header&quot;&gt;Edit Task&lt;/h2&gt;<br>                &lt;h3 className=&quot;modal-input-header&quot;&gt;Title&lt;/h3&gt;<br>                &lt;input type=&quot;text&quot; value={modalEditTask.title}<br>                       className=&quot;modal-input&quot;<br>                       onChange={(e) =&gt; setModalEditTask({<br>                        ...modalEditTask,<br>                        title: e.target.value<br>                       })}<br>                /&gt;<br>                &lt;h3 className=&quot;modal-input-header&quot;&gt;Description&lt;/h3&gt;<br>                &lt;textarea className=&quot;modal-textarea&quot;<br>                          onChange={(e) =&gt; setModalEditTask({<br>                           ...modalEditTask,<br>                            description: e.target.value<br>                          })}<br>                          value={modalEditTask.description || &#39;&#39;}<br>                /&gt;<br>                &lt;div className=&quot;modal-actions&quot;&gt;<br>                    &lt;button className=&quot;modal-btn modal-btn-cancel&quot;<br>                            onClick={() =&gt; setIsModalEditOpen(false)}<br>                    &gt;Close<br>                    &lt;/button&gt;<br>                    &lt;button className=&quot;modal-btn modal-btn-submit&quot;<br>                            onClick={submitTaskEdit}<br>                    &gt;Save&lt;/button&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/Modal&gt;<br>    )<br>}<br><br>export default ModalEdit;</pre><p>The ModalEdit component displays a modal for editing task details. It includes input fields for modifying the task title and description, and buttons to close the modal or save the changes, using the editTask utility function to handle the task editing process and triggering a reload of tasks upon successful editing.</p><p>Next, create ModalDelete.tsx, which will be responsible for submitting a task deletion:</p><pre>import {Modal} from &quot;react-responsive-modal&quot;;<br>import {deleteTask} from &quot;../utils&quot;;<br><br>function ModalDelete({<br> isModalDeleteOpen, setIsModalDeleteOpen,<br>    modalDeleteTaskId, reloadTasks<br>}) {<br>    const submitTaskDelete = () =&gt; {<br>        setIsModalDeleteOpen(false);<br>        deleteTask(modalDeleteTaskId).then(() =&gt; {<br>            reloadTasks();<br>        });<br>    };<br><br>    return (<br>        &lt;Modal open={isModalDeleteOpen} onClose={() =&gt; setIsModalDeleteOpen(false)} center&gt;<br>            &lt;div className=&quot;modal-content&quot;&gt;<br>                &lt;h2 className=&quot;modal-header&quot;&gt;Delete Task&lt;/h2&gt;<br>                &lt;p className=&quot;modal-question&quot;&gt;<br>                 Are you sure you want to delete this task?<br>                &lt;/p&gt;<br>                &lt;div className=&quot;modal-actions&quot;&gt;<br>                    &lt;button className=&quot;modal-btn modal-btn-cancel&quot;<br>                            onClick={() =&gt; setIsModalDeleteOpen(false)}<br>                    &gt;Cancel&lt;/button&gt;<br>                    &lt;button className=&quot;modal-btn modal-btn-submit&quot;<br>                            onClick={submitTaskDelete}<br>                    &gt;Yes&lt;/button&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/Modal&gt;<br>    );<br>}<br><br>export default ModalDelete;</pre><p>The ModalDelete component displays a modal for confirming the deletion of a task. It provides options to either cancel the deletion or proceed with deleting the task, utilizing the deleteTask utility function and triggering a reload of tasks upon successful deletion.</p><p>And lastly, set up the Main.tsx by using the above-defined components.</p><pre>import { useState } from &#39;react&#39;;<br>import {getTasks} from &quot;./utils&quot;;<br>import &quot;react-responsive-modal/styles.css&quot;;<br>import { ToastContainer } from &#39;react-toastify&#39;;<br>import &#39;react-toastify/dist/ReactToastify.css&#39;;<br>import ModalEdit from &quot;./components/ModalEdit&quot;;<br>import ModalDelete from &quot;./components/ModalDelete&quot;;<br>import TaskList from &quot;./components/TaskList&quot;;<br>import SelectProject from &quot;./components/SelectProject&quot;;<br>import AddTaskForm from &quot;./components/AddTaskForm&quot;;<br><br>function Main () {<br>    const [projectId, setProjectId] = useState(&#39;&#39;);<br>    const projectsData = document.getElementById(&#39;app&#39;).getAttribute(&#39;data-projects&#39;);<br>    const projects = JSON.parse(projectsData);<br>    const [tasks, setTasks] = useState([]);<br>    const [isModalEditOpen, setIsModalEditOpen] = useState(false);<br>    const [modalEditTask, setModalEditTask] = useState({id: &#39;&#39;, title: &#39;&#39;, description: &#39;&#39;});<br>    const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false);<br>    const [modalDeleteTaskId, setModalDeleteTaskId] = useState(&#39;&#39;);<br>    const [newTask, setNewTask] = useState({title: &#39;&#39;, description: &#39;&#39;});<br><br>    const reloadTasks = () =&gt; {<br>        getTasks(projectId).then((tasksData) =&gt; setTasks(tasksData));<br>    };<br><br>    return (<br>        &lt;div&gt;<br>            &lt;ToastContainer autoClose={2000} /&gt;<br>            &lt;ModalEdit isModalEditOpen={isModalEditOpen}<br>                       setIsModalEditOpen={setIsModalEditOpen}<br>                       modalEditTask={modalEditTask}<br>                       setModalEditTask={setModalEditTask}<br>                       reloadTasks={reloadTasks}<br>            /&gt;<br>            &lt;ModalDelete isModalDeleteOpen={isModalDeleteOpen}<br>                         setIsModalDeleteOpen={setIsModalDeleteOpen}<br>                         modalDeleteTaskId={modalDeleteTaskId}<br>                         reloadTasks={reloadTasks}<br>            /&gt;<br>            &lt;div className=&quot;left-side&quot;&gt;<br>                {tasks.length &gt; 0 ? (<br>                    &lt;TaskList tasks={tasks}<br>                              setIsModalEditOpen={setIsModalEditOpen}<br>                              setModalEditTask={setModalEditTask}<br>                              setIsModalDeleteOpen={setIsModalDeleteOpen}<br>                              setModalDeleteTaskId={setModalDeleteTaskId}<br>                              projectId={projectId}<br>                              setTasks={setTasks}<br>                    /&gt;<br>                ) : (<br>                    &lt;div className=&quot;no-tasks&quot;&gt;<br>                        {projectId === &#39;&#39; ? (<br>                            &lt;p&gt;Choose a project to see its tasks.&lt;/p&gt;<br>                        ) : (<br>                            &lt;p&gt;This project has no tasks.&lt;/p&gt;<br>                        )}<br>                    &lt;/div&gt;<br>                )}<br>            &lt;/div&gt;<br>            &lt;div className=&quot;right-side&quot;&gt;<br>                &lt;div className=&quot;right-side-wrapper&quot;&gt;<br>                    &lt;SelectProject projectId={projectId}<br>                                   projects={projects}<br>                                   setProjectId={setProjectId}<br>                                   setTasks={setTasks}<br>                    /&gt;<br>                    {projectId === &#39;&#39; ? (<br>                        &lt;div className=&quot;no-project-selected&quot;&gt;<br>                            &lt;p&gt;Please select a project.&lt;/p&gt;<br>                        &lt;/div&gt;<br>                    ) : (<br>                        &lt;AddTaskForm newTask={newTask}<br>                                     setNewTask={setNewTask}<br>                                     projectId={projectId}<br>                                     reloadTasks={reloadTasks}<br>                        /&gt;<br>                    )}<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default Main;</pre><p>The Main component is a central component that managing project and task-related functionalities. It includes modals for editing and deleting tasks, a task list with dynamic updates, a project selection dropdown, and a form for adding new tasks, leveraging state management and utility functions for smooth user interaction.</p><h3>Final Results</h3><p>At this point, all the components are ready to interact with each other. So you can build the frontend assets and run the server:</p><pre>npm run build &amp;&amp; php artisan serve</pre><p>By visiting http://127.0.0.1:8000, you’ll get this kind of result:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*iO2DvXAeBuFjh4lmnmxskg.gif" /><figcaption>GIF generated from a local working project</figcaption></figure><p><strong>That’s it!</strong></p><p>Now you can easily integrate React.js into your Laravel app without using any additional Laravel tools (like Inertia). And as a result, you can continue to maintain your Laravel app to build more scalable APIs with its authentication and other stuff.</p><p>So, this can be just an example app for your next full-stack Laravel and React.js project.</p><h3>Conclusion</h3><p>With this article, you built a single-page, full-stack Tasklist app using React.js (with TypeScript) with Vite.js as frontend technologies, Laravel as a backend framework, and react-beautiful-dnd package for having draggable items.<br>Now you know how to manually integrate React.js in your Laravel app and maintain it.</p><p>You can find the complete code of the project here on my <a href="https://github.com/boolfalse/laravel-react-tasklist"><strong>GitHub ⭐</strong></a>, where I actively publicize much of my work about various modern technologies.<br>For more information, you can visit my website: <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Feel free to share this article. 😇</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=625989d49868" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build Real-Time Chat App with Laravel Reverb]]></title>
            <link>https://medium.com/@boolfalse/build-real-time-chat-app-with-laravel-reverb-2c0eedc8db07?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/2c0eedc8db07</guid>
            <category><![CDATA[boolfalse]]></category>
            <category><![CDATA[reactjs]]></category>
            <category><![CDATA[realtime-chat-app]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[laravel-reverb]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Sun, 31 Mar 2024 23:36:16 GMT</pubDate>
            <atom:updated>2024-03-31T23:36:16.341Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LkEFP4gTqs0IqIm467693g.png" /></figure><p><strong><em>This article was initially published on </em></strong><a href="https://www.freecodecamp.org/news/laravel-reverb-realtime-chat-app/"><strong><em>freeCodeCamp</em></strong></a><strong><em>.</em></strong><br>In recent days, <a href="https://www.youtube.com/watch?v=yrL5eCMpqtc">a talk by Reverb creator Joe Dixon</a> was published on the Laracon EU’s YouTube channel. And since my article was published a few days before that, I decided to have that article here on Medium as well. I just did some minor additions and corrections on it.<br>For those who are just curious: the full code for this article is on <a href="https://github.com/boolfalse/laravel-reverb-react-chat"><strong>GitHub⭐</strong></a>․</p><h3>Introduction</h3><p>In March of 2024, <a href="https://blog.laravel.com/laravel-11-now-available">Laravel 11 was released</a>. And with it arrived a new family member in the Laravel ecosystem: <a href="https://reverb.laravel.com/"><strong>Laravel Reverb</strong></a>.</p><p>Reverb is a separate open-source package that’s a first-party WebSocket server for Laravel applications. It helps facilitate real-time communication between client and server.</p><p>Before this new package, Laravel had event broadcasting, but basically it didn’t have a built-in way to set up a self-hosted WebSocket server. Fortunately, Reverb now gives us that option.</p><p>Laravel Reverb has a few key features: it’s written in PHP, it’s fast, and it’s scalable. It was developed in particular to be horizontally scalable.</p><p>Reverb basically allows you to run an application on a single server — but if the application starts to outgrow that server, you can add multiple additional servers. Then those servers can all communicate with each other via <a href="https://redis.io/docs/interact/pubsub/">Redis Pub/Sub</a> to distribute the messages across each other.</p><p>In this article, you will learn how to build a real-time chat application using Laravel Reverb. This will let you easily implement WebSocket communications between your backend and frontend.</p><p>For a frontend technology, you can use anything you want — but in this case, we’ll use React.js with the Vite.js build tool.</p><p>By the end of this article, you’ll have a full-stack, real-time app on your local machine, which will work like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*fMiC9NbULD_3UXJUKqzWmA.gif" /><figcaption>Demo of the app showing messaging between two logged-in users</figcaption></figure><h3>Prerequisites</h3><p>You’ll need the following tools for the app that we’ll build in this article:</p><ul><li><strong>PHP</strong> 8.2 or above<br>Run php -v to check the version.</li><li><strong>Composer<br></strong>Run composerto check that it exists.</li><li><strong>Node.js</strong> 20 or above<br>Run node -v to check the version.</li><li><strong>MySQL</strong> 5.7 or above<br>Run mysql --versionto check if it exists, or follow the <a href="https://dev.mysql.com/doc/refman/5.7/en/linux-installation.html">docs</a>.</li></ul><h3>General Steps</h3><p>The main steps in this article will be:</p><ul><li>Installing Laravel 11.</li><li>Adding authentication flow to it.<br>Laravel provides a basic starting point for the authentication scaffolding using Bootstrap with React / Vue.</li><li>Installing Reverb.</li><li>React.js components and event listening in the frontend.</li></ul><h3>Install Laravel</h3><p>To start, install Laravel 11 by using the composer command:</p><pre>composer create-project laravel/laravel:^11.0 laravel-reverb-react-chat &amp;&amp; cd laravel-reverb-react-chat/</pre><p>At this point, you can check out the app by running the servecommand:</p><pre>php artisan serve</pre><h3>Create a Model and Migration</h3><p>You can generate a model and a migration for the messages by using this single command:</p><pre>php artisan make:model -m Message</pre><p>Then you’ll need to set up the Message’s model with the following code:</p><p><strong><em>app/Models/Message.php</em></strong></p><pre>&lt;?php<br><br>namespace App\Models;<br><br>use Illuminate\Database\Eloquent\Factories\HasFactory;<br>use Illuminate\Database\Eloquent\Model;<br>use Illuminate\Database\Eloquent\Relations\BelongsTo;<br><br>class Message extends Model<br>{<br>    use HasFactory;<br><br>    public $table = &#39;messages&#39;;<br>    protected $fillable = [&#39;id&#39;, &#39;user_id&#39;, &#39;text&#39;];<br><br>    public function user(): BelongsTo {<br>        return $this-&gt;belongsTo(User::class, &#39;user_id&#39;);<br>    }<br><br>    public function getTimeAttribute(): string {<br>        return date(<br>            &quot;d M Y, H:i:s&quot;,<br>            strtotime($this-&gt;attributes[&#39;created_at&#39;])<br>        );<br>    }<br>}</pre><p>As you can see, there’s a getTimeAttribute()accessor that will format the message creation timestamp into a human-readable date and time format. It will show it on the top of each message in the chat box.</p><p>Next, set up the migration for the messagesdatabase table with this code:</p><p><strong><em>database/migrations/2024_03_25_000831_create_messages_table.php</em></strong></p><pre>&lt;?php<br><br>use Illuminate\Database\Migrations\Migration;<br>use Illuminate\Database\Schema\Blueprint;<br>use Illuminate\Support\Facades\Schema;<br><br>return new class extends Migration<br>{<br>    public function up(): void {<br>        Schema::create(&#39;messages&#39;, function (Blueprint $table) {<br>            $table-&gt;id();<br>            $table-&gt;foreignId(&#39;user_id&#39;)-&gt;constrained();<br>            $table-&gt;text(&#39;text&#39;)-&gt;nullable();<br>            $table-&gt;timestamps();<br>        });<br>    }<br><br>    public function down(): void {<br>        Schema::dropIfExists(&#39;messages&#39;);<br>    }<br>};</pre><p>This migration creates a messagestable in the database. The table contains columns for an auto-incrementing primary key (id), a foreign key (user_id) referencing the idcolumn of the userstable, a textcolumn for storing the message content, and timestamps to automatically track the creation and modification times of each record.</p><p>The migration also includes a rollback method (down()) to drop the messagestable if needed.</p><p>In this article, we’ll use the MySQL database, but you can go with SQLite as the default one if you prefer. Just make sure to set up your database credentials in .envfile correctly:</p><p><strong><em>.env</em></strong></p><pre>DB_CONNECTION=mysql<br>DB_HOST=127.0.0.1<br>DB_PORT=3306<br>DB_DATABASE=database_name<br>DB_USERNAME=username<br>DB_PASSWORD=password</pre><p>After setting up the environment variables, optimize the cache:</p><pre>php artisan optimize</pre><p>Run migrations to recreate the database tables as well as to add the messagestable:</p><pre>php artisan migrate:fresh</pre><h3>Add Authentication</h3><p>Now, you can add authentication scaffolding to your app. You can use Laravel’s UI package to import some asset files. First, you’ll need to install the appropriate package:</p><pre>composer require laravel/ui</pre><p>Then import the React-related assets into the application:</p><pre>php artisan ui react --auth</pre><p>It may ask to overwrite the app/Http/Controllers/Controller.php, and you can go ahead and allow it:</p><pre>The [Controller.php] file already exists. Do you want to replace it? (yes/no) [no]</pre><p>This will do all of the authentication scaffolding compiled and installed, including routes, controllers, views, vite configurations, and a simple React-specific sample.<br>At this point, you’re just one step away from the app being ready to go.</p><p><strong><em>NOTE:</em></strong><em> Make sure you have </em><strong><em>Node.js</em></strong><em> (with </em><strong><em>npm</em></strong><em>) version 20 or above installed. You can check that by running the </em><em>node -vcommand. Otherwise, just go ahead and install it using the </em><a href="https://nodejs.org/en/download"><em>official page</em></a><em>.</em></p><pre>npm install &amp;&amp; npm run build</pre><p>The command above will install NPM packages and build frontend assets. Now you can start the Laravel application and check out your fully ready app sample:</p><pre>php artisan optimize &amp;&amp; php artisan serve</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/806/1*hzky93DlHJ6YiN2anf7djQ.png" /><figcaption>A screenshot of the Register page</figcaption></figure><p>It’s also important to note that you can separately run the devcommand instead of using buildevery time when you’re making changes to frontend files:</p><pre>npm run dev</pre><p>See the details in the package.jsonfile, in the scriptsfield.</p><h3>Set Up Routes</h3><p>In this real-time chat app, you’ll need to have a few routes:</p><ul><li>home for the home page (already should be added)</li><li>message for adding a new message</li><li>messages to get all the existing messages</li></ul><p>You’ll have these kinds of routes in the web.phpfile:</p><p><strong><em>routes/web.php</em></strong></p><pre>&lt;?php<br><br>use Illuminate\Support\Facades\Auth;<br>use Illuminate\Support\Facades\Route;<br>use App\Http\Controllers\HomeController;<br><br>Route::get(&#39;/&#39;, function () { return view(&#39;welcome&#39;); });<br><br>Auth::routes();<br><br>Route::get(&#39;/home&#39;, [HomeController::class, &#39;index&#39;])<br>    -&gt;name(&#39;home&#39;);<br>Route::get(&#39;/messages&#39;, [HomeController::class, &#39;messages&#39;])<br>    -&gt;name(&#39;messages&#39;);<br>Route::post(&#39;/message&#39;, [HomeController::class, &#39;message&#39;])<br>    -&gt;name(&#39;message&#39;);</pre><p>After setting up those routes, let’s use Laravel Events and Queue Jobs advantages.</p><h3>Set Up a Laravel Event</h3><p>You need to create a GotMessageevent for listening for a specific event:</p><pre>php artisan make:event GotMessage</pre><blockquote>Laravel’s events provide a simple observer pattern implementation, allowing you to subscribe and listen for various events that occur within your application. Event classes are typically stored in the app/Events directory. (<a href="https://laravel.com/docs/11.x/events">Docs</a>)</blockquote><p>Set up a private WebSocket channel in the broadcastOnmethod for all the authenticated users to receive messages in real time. In this case, we will call it &quot;channel_for_everyone&quot;, but you can also make it dynamic, depending on user, like &quot;App.Models.User.{$this-&gt;message[&#39;user_id&#39;]}&quot;.</p><p><strong><em>app/Events/GotMessage.php</em></strong></p><pre>&lt;?php<br><br>namespace App\Events;<br><br>use Illuminate\Broadcasting\InteractsWithSockets;<br>use Illuminate\Broadcasting\PrivateChannel;<br>use Illuminate\Contracts\Broadcasting\ShouldBroadcast;<br>use Illuminate\Foundation\Events\Dispatchable;<br>use Illuminate\Queue\SerializesModels;<br><br>class GotMessage implements ShouldBroadcast<br>{<br>    use Dispatchable, InteractsWithSockets, SerializesModels;<br><br>    public function __construct(public array $message) {<br>        //<br>    }<br><br>    public function broadcastOn(): array {<br>        // $this-&gt;message is available here<br>        return [<br>            new PrivateChannel(&quot;channel_for_everyone&quot;),<br>        ];<br>    }<br>}</pre><p>As you can see, there’s a public $messageproperty as a constructor argument, so you can get message information in the front end.</p><p>We’ve already used the channel name in the channels file, and we’ll use it in the front end as well for real-time message updates.</p><p>Don’t forget to implement the ShouldBroadcastinterface in the event’s class.</p><h3>Set Up a Laravel Queue Job</h3><p>Now it’s time to create the SendMessagejob for sending messages:</p><pre>php artisan make:job SendMessage</pre><blockquote>Laravel allows you to easily create queued jobs that may be processed in the background. By moving time intensive tasks to a queue, your application can respond to web requests with blazing speed and provide a better user experience to your customers. (<a href="https://laravel.com/docs/11.x/queues">Docs</a>)</blockquote><p><strong><em>app/Jobs/SendMessage.php</em></strong></p><pre>&lt;?php<br><br>namespace App\Jobs;<br><br>use App\Events\GotMessage;<br>use App\Models\Message;<br>use Illuminate\Bus\Queueable;<br>use Illuminate\Contracts\Queue\ShouldQueue;<br>use Illuminate\Foundation\Bus\Dispatchable;<br>use Illuminate\Queue\InteractsWithQueue;<br>use Illuminate\Queue\SerializesModels;<br><br>class SendMessage implements ShouldQueue<br>{<br>    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;<br><br>    public function __construct(public Message $message) {<br>        //<br>    }<br><br>    public function handle(): void {<br>        GotMessage::dispatch([<br>            &#39;id&#39; =&gt; $this-&gt;message-&gt;id,<br>            &#39;user_id&#39; =&gt; $this-&gt;message-&gt;user_id,<br>            &#39;text&#39; =&gt; $this-&gt;message-&gt;text,<br>            &#39;time&#39; =&gt; $this-&gt;message-&gt;time,<br>        ]);<br>    }<br>}</pre><p>The SendMessage.phpqueue job is responsible for dispatching the GotMessageevent with information about a newly sent message. It receives a Messageobject upon construction, representing the message to be sent.</p><p>In its handle()method, it dispatches the GotMessageevent with details such as the message ID, user ID, text, and timestamp. This job is designed to be queued for asynchronous processing, enabling efficient handling of message sending tasks in the background.</p><p>As you can see, there’s a public $messageproperty as a constructor argument, which we’ll use to attach message information to the queue job.</p><h3>Controller Methods</h3><p>For the defined routes, here are the appropriate controller methods:</p><p><strong><em>app/Http/Controllers/HomeController.php</em></strong></p><pre>&lt;?php<br><br>namespace App\Http\Controllers;<br><br>use App\Jobs\SendMessage;<br>use App\Models\Message;<br>use App\Models\User;<br>use Illuminate\Http\JsonResponse;<br>use Illuminate\Http\Request;<br><br>class HomeController extends Controller<br>{<br>    public function __construct() {<br>        $this-&gt;middleware(&#39;auth&#39;);<br>    }<br><br>    public function index() {<br>        $user = User::where(&#39;id&#39;, auth()-&gt;id())-&gt;select([<br>            &#39;id&#39;, &#39;name&#39;, &#39;email&#39;,<br>        ])-&gt;first();<br><br>        return view(&#39;home&#39;, [<br>            &#39;user&#39; =&gt; $user,<br>        ]);<br>    }<br><br>    public function messages(): JsonResponse {<br>        $messages = Message::with(&#39;user&#39;)-&gt;get()-&gt;append(&#39;time&#39;);<br><br>        return response()-&gt;json($messages);<br>    }<br><br>    public function message(Request $request): JsonResponse {<br>        $message = Message::create([<br>            &#39;user_id&#39; =&gt; auth()-&gt;id(),<br>            &#39;text&#39; =&gt; $request-&gt;get(&#39;text&#39;),<br>        ]);<br>        SendMessage::dispatch($message);<br><br>        return response()-&gt;json([<br>            &#39;success&#39; =&gt; true,<br>            &#39;message&#39; =&gt; &quot;Message created and job dispatched.&quot;,<br>        ]);<br>    }<br>}</pre><ul><li>In the homemethod, we’ll get the logged-in user’s data from the database using the Usermodel and send it to the blade view.</li><li>In the messagesmethod, we’ll retrieve all the messages from the database using the Messagemodel, attach the userrelationship data to it, append the timefield (accessor) to each item, and send all that to the view.</li><li>In the message method, a new message record will be created in the database table by using the Messagemodel, and the SendMessagequeue job will be dispatched.</li></ul><h3>Install Laravel Reverb</h3><p>Now we’ve come to the most important moment: it’s time to install <a href="https://laravel.com/docs/11.x/reverb#installation">Reverb</a> in your Laravel app.</p><p>It’s so easy. All the necessary packaging and configuration setup can be done using this single command:</p><pre>php artisan install:broadcasting</pre><p>It will ask you to install Laravel Reverb as well as install and build the Node dependencies required for broadcasting. Just press enter to continue.</p><p>After the command execution, make sure you’ve automatically added reverb-specific environment variables to the .envfile, like:</p><p><strong><em>.env</em></strong></p><pre>BROADCAST_CONNECTION=reverb<br><br>###<br><br>REVERB_APP_ID=795051<br>REVERB_APP_KEY=s3w3thzezulgp5g0e5bs<br>REVERB_APP_SECRET=gncsnk3rzpvczdakl6pz<br>REVERB_HOST=&quot;localhost&quot;<br>REVERB_PORT=8080<br>REVERB_SCHEME=http<br><br>VITE_REVERB_APP_KEY=&quot;${REVERB_APP_KEY}&quot;<br>VITE_REVERB_HOST=&quot;${REVERB_HOST}&quot;<br>VITE_REVERB_PORT=&quot;${REVERB_PORT}&quot;<br>VITE_REVERB_SCHEME=&quot;${REVERB_SCHEME}&quot;</pre><p>You’ll also have a new broadcasting.php configuration file in the config directory.</p><h3>Set Up WebSocket Channels</h3><p>Lastly, you’ll need to add a channel in the channels.phpfile. It should already be created after installing Reverb.</p><p><strong><em>routes/channels.php</em></strong></p><pre>&lt;?php<br><br>use Illuminate\Support\Facades\Broadcast;<br><br>Broadcast::channel(&#39;channel_for_everyone&#39;, function ($user) {<br>    return true;<br>});</pre><p>You’ll have only one channel. You can change the channel’s name and make it dynamic — it’s up to you. In the closure of the channel, we’ll always return true, but you can modify it later to make some restrictions regarding the channel’s subscription.</p><p>Optimize caches one more time:</p><pre>php artisan optimize</pre><h3>Customize Laravel Views</h3><p>Now your back end should be ready at this point, so you can switch to the front end.</p><p>Before working on the React stuff, you’ll want to set up Laravel *.blade.phpviews. In the homeblade view, make sure to have the root div with an ID of mainto render all the React components there.</p><p><strong><em>resources/views/home.blade.php</em></strong></p><pre>@extends(&#39;layouts.app&#39;)<br><br>@section(&#39;content&#39;)<br>    &lt;div class=&quot;container&quot;&gt;<br>        &lt;div id=&quot;main&quot; data-user=&quot;{{ json_encode($user) }}&quot;&gt;&lt;/div&gt;<br>    &lt;/div&gt;<br>@endsection</pre><p>The div with ID of maingets a data property for holding the $user info sent from the controller’s homemethod.</p><p>I won’t put the whole resources/views/welcome.blade.phpcontent here, but you can just make the following small changes to it:</p><ul><li>Replace url(&#39;/dashboard&#39;)with url(&#39;/home&#39;);</li><li>Replace Dashboardwith Home;</li><li>Remove mainand footersections.</li></ul><h3>The Work on the Front End</h3><p>In Reverb, event broadcasting is done by a server-side broadcasting driver that broadcasts your Laravel events so that the front end can receive them within the browser client.</p><p>In the front end, <a href="https://github.com/laravel/echo">Laravel Echo</a> does that job under the hood. Echo is a JavaScript library that makes it painless to subscribe to channels and listen for events broadcast by your server-side broadcasting driver.</p><p>You can find the WebSocket configurations setup with Echo in the resources/js/echo.jsfile, but you don’t need to do anything there for this project.</p><p>Let’s create a few React components so that we have a refactored and more readable project.</p><p>Create a Main.jsxcomponent in the new componentsfolder:</p><p><strong><em>resources/js/components/Main.jsx</em></strong></p><pre>import React from &#39;react&#39;;<br>import ReactDOM from &#39;react-dom/client&#39;;<br>import &#39;../../css/app.css&#39;;<br>import ChatBox from &quot;./ChatBox.jsx&quot;;<br><br>if (document.getElementById(&#39;main&#39;)) {<br>    const rootUrl = &quot;http://127.0.0.1:8000&quot;;<br>    <br>    ReactDOM.createRoot(document.getElementById(&#39;main&#39;)).render(<br>        &lt;React.StrictMode&gt;<br>            &lt;ChatBox rootUrl={rootUrl} /&gt;<br>        &lt;/React.StrictMode&gt;<br>    );<br>}</pre><p>Here we’ll check if there’s an element with the id &#39;main&#39;. If it exists, it proceeds with rendering the React application.</p><p>As you can see, there’s a ChatBox component. We’ll learn more about it soon.</p><p>Remove the resources/js/components/Example.jsx file, and import the Main.jsxcomponent in the app.js:</p><p><strong><em>resources/js/app.js</em></strong></p><pre>import &#39;./bootstrap&#39;;<br>import &#39;./components/Main.jsx&#39;;</pre><p>Create Message.jsxand MessageInput.jsxfiles, so you can use them in the ChatBoxcomponent.</p><p>The Messagecomponent will get userIdand messagearguments (fields) to show each message in the chat box.</p><p><strong><em>resources/js/components/Message.jsx</em></strong></p><pre>import React from &quot;react&quot;;<br><br>const Message = ({ userId, message }) =&gt; {<br>    return (<br>        &lt;div className={`row ${<br>        userId === message.user_id ? &quot;justify-content-end&quot; : &quot;&quot;<br>        }`}&gt;<br>            &lt;div className=&quot;col-md-6&quot;&gt;<br>  &lt;small className=&quot;text-muted&quot;&gt;<br>                    &lt;strong&gt;{message.user.name} | &lt;/strong&gt;<br>                &lt;/small&gt;<br>                &lt;small className=&quot;text-muted float-right&quot;&gt;<br>                    {message.time}<br>                &lt;/small&gt;<br>                &lt;div className={`alert alert-${<br>                userId === message.user_id ? &quot;primary&quot; : &quot;secondary&quot;<br>                }`} role=&quot;alert&quot;&gt;<br>                    {message.text}<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/div&gt;<br>    );<br>};<br><br>export default Message;</pre><p>The Message.jsxcomponent renders individual messages within the chat interface. It receives the userIdand messageprops. Based on whether the message sender matches the current user, it aligns the message to the appropriate side of the screen.</p><p>Each message includes the sender’s name, timestamp, and the message content itself, styled differently based on whether the message is sent by the current user or another user.</p><p>The MessageInputcomponent will care about creating a new message:</p><p><strong><em>resources/js/components/MessageInput.jsx</em></strong></p><pre>import React, { useState } from &quot;react&quot;;<br><br>const MessageInput = ({ rootUrl }) =&gt; {<br>    const [message, setMessage] = useState(&quot;&quot;);<br><br>    const messageRequest = async (text) =&gt; {<br>        try {<br>            await axios.post(`${rootUrl}/message`, {<br>                text,<br>            });<br>        } catch (err) {<br>            console.log(err.message);<br>        }<br>    };<br><br>    const sendMessage = (e) =&gt; {<br>        e.preventDefault();<br>        if (message.trim() === &quot;&quot;) {<br>            alert(&quot;Please enter a message!&quot;);<br>            return;<br>        }<br><br>        messageRequest(message);<br>        setMessage(&quot;&quot;);<br>    };<br><br>    return (<br>        &lt;div className=&quot;input-group&quot;&gt;<br>            &lt;input onChange={(e) =&gt; setMessage(e.target.value)}<br>                   autoComplete=&quot;off&quot;<br>                   type=&quot;text&quot;<br>                   className=&quot;form-control&quot;<br>                   placeholder=&quot;Message...&quot;<br>                   value={message}<br>            /&gt;<br>            &lt;div className=&quot;input-group-append&quot;&gt;<br>                &lt;button onClick={(e) =&gt; sendMessage(e)}<br>                        className=&quot;btn btn-primary&quot;<br>                        type=&quot;button&quot;&gt;Send&lt;/button&gt;<br>            &lt;/div&gt;<br>        &lt;/div&gt;<br>    );<br>};<br><br>export default MessageInput;</pre><p>The MessageInputcomponent provides a form input field for users to type messages and send them in the chat interface. By clicking the button, it triggers a function to send the message to the server via an Axios POST request to the specified rootUrlthat it got from the parent ChatBoxcomponent. It also handles validation to ensure that users cannot send empty messages. You can customize it later if you want.</p><p>Now create a ChatBox.jsxcomponent to have the front end ready:</p><p><strong><em>resources/js/components/ChatBox.jsx</em></strong></p><pre>import React, { useEffect, useRef, useState } from &quot;react&quot;;<br>import Message from &quot;./Message.jsx&quot;;<br>import MessageInput from &quot;./MessageInput.jsx&quot;;<br><br>const ChatBox = ({ rootUrl }) =&gt; {<br>    const userData = document.getElementById(&#39;main&#39;)<br>        .getAttribute(&#39;data-user&#39;);<br><br>    const user = JSON.parse(userData);<br>    // `App.Models.User.${user.id}`;<br>    const webSocketChannel = `channel_for_everyone`;<br><br>    const [messages, setMessages] = useState([]);<br>    const scroll = useRef();<br><br>    const scrollToBottom = () =&gt; {<br>        scroll.current.scrollIntoView({ behavior: &quot;smooth&quot; });<br>    };<br><br>    const connectWebSocket = () =&gt; {<br>        window.Echo.private(webSocketChannel)<br>            .listen(&#39;GotMessage&#39;, async (e) =&gt; {<br>                // e.message<br>                await getMessages();<br>            });<br>    }<br><br>    const getMessages = async () =&gt; {<br>        try {<br>            const m = await axios.get(`${rootUrl}/messages`);<br>            setMessages(m.data);<br>            setTimeout(scrollToBottom, 0);<br>        } catch (err) {<br>            console.log(err.message);<br>        }<br>    };<br><br>    useEffect(() =&gt; {<br>        getMessages();<br>        connectWebSocket();<br><br>        return () =&gt; {<br>            window.Echo.leave(webSocketChannel);<br>        }<br>    }, []);<br><br>    return (<br>        &lt;div className=&quot;row justify-content-center&quot;&gt;<br>            &lt;div className=&quot;col-md-8&quot;&gt;<br>                &lt;div className=&quot;card&quot;&gt;<br>                    &lt;div className=&quot;card-header&quot;&gt;Chat Box&lt;/div&gt;<br>                    &lt;div className=&quot;card-body&quot;<br>                         style={{height: &quot;500px&quot;, overflowY: &quot;auto&quot;}}&gt;<br>                        {<br>                            messages?.map((message) =&gt; (<br>                                &lt;Message key={message.id}<br>                                         userId={user.id}<br>                                         message={message}<br>                                /&gt;<br>                            ))<br>                        }<br>                        &lt;span ref={scroll}&gt;&lt;/span&gt;<br>                    &lt;/div&gt;<br>                    &lt;div className=&quot;card-footer&quot;&gt;<br>                        &lt;MessageInput rootUrl={rootUrl} /&gt;<br>                    &lt;/div&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/div&gt;<br>    );<br>};<br><br>export default ChatBox;</pre><p>The ChatBoxcomponent manages a chat interface within the application. It fetches and displays messages from a server using WebSocket and HTTP requests.</p><p>The component renders a list of messages, a message input field, and automatically scrolls to the bottom when new messages arrive.</p><p>It defines a WebSocket channel for real-time message updates. You need to set up that channel by using the same name as it was written in the routes/channels.phpand in the app/Events/GotMessage.phpqueue job.</p><p>Also, the leave()function is called within the useEffectcleanup function to unsubscribe from the WebSocket channel when the component unmounts. This prevents memory leaks and unnecessary network connections by stopping the component from listening to updates on the WebSocket channel after it’s no longer needed.</p><h3>Running the Application</h3><p>Now, everything’s ready, and it’s time to check out the app. Follow these instructions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*63Ucaus_RRuE0ie66Kab0A.png" /><figcaption>A screenshot from the terminal with all the necessary commands</figcaption></figure><ul><li>Build frontend assets (this is not a “forever” running command):<br> npm run build</li><li>Start listening to the Laravel events:<br> php artisan queue:listen</li><li>Start the WebSocket server:<br> php artisan reverb:start</li><li>Start the server (you may use an alternative for your app like a local running server):<br> php artisan serve</li></ul><p>After all the necessary commands are running, you can check out the app by visiting the default URL: http://127.0.0.1:8000.</p><p>For testing, you can register two different users, have those users log in, send messages from each of them, and see the chat box.</p><h3>Useful Resources</h3><p>Now that we’ve reached the end of this article, it’s worth listing some useful resources about Reverb:</p><ul><li><a href="https://laravel.com/docs/11.x/broadcasting">Laravel Broadcasting</a> (official documentation)</li><li><a href="https://www.youtube.com/watch?v=yrL5eCMpqtc">Joe Dixon — Real-Time Laravel</a> (talk on Laracon EU 2024)</li><li><a href="https://www.youtube.com/watch?v=0g7HqfsCX4Y">Taylor Otwell — Laravel Update</a> (talk on Laracon EU 2024)</li></ul><h3>Conclusion</h3><p>Now you know how to build real-time applications with Laravel Reverb in the new version of Laravel. With this, you can implement WebSocket communications in your full-stack app and avoid using any additional 3rd-party services (like Pusher and Socket.io).</p><p>If you want to have a clear idea of how to integrate React.js into your Laravel app without using any additional Laravel tools (like Inertia), you can read through my <a href="https://www.freecodecamp.org/news/use-react-with-laravel/">freeCodeCamp article</a>, where you can build a single-page, full-stack Tasklist app.</p><p>The complete code for this article is here on my <a href="https://github.com/boolfalse/laravel-reverb-react-chat"><strong>GitHub⭐</strong></a>, where I actively publicize much of my work about various modern technologies.</p><p>For more information, you can visit my website: <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Feel free to share this article. 😇</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2c0eedc8db07" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Create your own AI voice assistant bot with Node.js using Google Bard]]></title>
            <link>https://medium.com/@boolfalse/create-your-own-ai-voice-assistant-bot-with-node-js-using-google-bard-8d3572ed5272?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/8d3572ed5272</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[telegram-bot]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Fri, 10 Nov 2023 11:26:36 GMT</pubDate>
            <atom:updated>2023-11-12T09:36:24.530Z</atom:updated>
            <content:encoded><![CDATA[<p>Before starting, just watch this very short <a href="https://www.youtube.com/shorts/YFpR8x7sPQY"><strong>YouTube</strong></a><strong> </strong>video to see what exactly you can make with no cost by following this article until the end.</p><p>As you may already know, voice conversations have come to the ChatGPT app. That was officially announced by OpenAI:</p><ul><li>Event Livestream: <a href="https://www.youtube.com/watch?v=U9mJuUkhUzk&amp;t=542s">OpenAI DevDay, Opening Keynote</a></li><li>Blog: <a href="https://openai.com/blog/new-models-and-developer-products-announced-at-devday">New models and developer products announced at DevDay</a></li><li>Short video by CNET: <a href="https://www.youtube.com/watch?v=a0W2SgX_u-Y">Voice Conversations Come to the ChatGPT App</a></li></ul><p>The bot you see is simple and cost-limited, but practically, you could do that without using OpenAI’s TTS (text-to-speech) model, by the integration of voice communication feature in your mobile app, or even in a Telegram bot project. So I did that for myself just for fun.</p><p>In this article you can learn a way to build an AI voice assistant using Google Bard. You will create a Telegram bot API using the following stack:</p><ul><li><a href="https://nodejs.org/en/download"><strong>Node.js</strong></a> — the running backend server which will interact with Telegram to receive and send voice/text messages</li><li><a href="https://www.python.org/downloads/"><strong>Python</strong></a> — scripts for TTS (text-to-speech), audio-transcribing (speech-to-text) and getting Google Bard answers</li><li><a href="https://web.telegram.org/"><strong>Telegram</strong></a> — the client mobile app which will be used</li><li><a href="https://bard.google.com/"><strong>Google Bard</strong></a> — as an AI ChatBot service</li><li><a href="https://www.mongodb.com/"><strong>MongoDB</strong></a> — a database service</li><li><a href="https://aws.amazon.com/polly/"><strong>Amazon Polly</strong></a> —AWS service for TTS conversion (free tier)</li></ul><p>The prerequisites that you will need are:</p><ul><li><a href="https://nodejs.org/en/download">Node.js</a> (v18 or higher) installed.</li><li><a href="https://www.python.org/downloads/">Python</a> (v3) installed.</li><li><a href="https://www.ffmpeg.org/download.html">FFmpeg</a> installed.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bswvaC3m5v2yhZEcdboUZw.png" /></figure><p>In the above image is the whole project’s lifecycle.</p><p>The required steps you need to follow are:</p><ul><li>Create a new AWS IAM user and give it access to <a href="https://aws.amazon.com/polly/">Amazon Polly</a>. Get the <strong><em>AWS Access Key</em></strong> and <strong><em>AWS Secret Key</em></strong>.</li></ul><blockquote><a href="https://aws.amazon.com/polly/pricing/"><strong>AWS provides free tier for Polly.</strong></a><br>For Amazon Polly’s Standard voices, the free tier includes 5 million characters per month for speech or Speech Marks requests, for the first 12 months, starting from your first request for speech.</blockquote><blockquote><em>1. Go to the AWS Management Console and sign in.<br>2. Click Identity &amp; Access Management (IAM).<br>3. In the left pane, click Users.<br>4. Click the name of the user you want to create access keys for.<br>5. Click the Security tab.<br>6. Under Access keys (access key ID and secret access key), click Create Access Key.<br>7. AWS generates an access key ID and secret access key. Download the CSV file that contains your access key credentials, or copy and paste the access key ID and secret access key into a secure location.<br>8. Click Close.</em></blockquote><ul><li>Create a Telegram bot with <a href="https://t.me/botfather/">BotFather</a>. Get the <strong><em>Telegram Bot Token</em></strong>.</li></ul><blockquote><em>1. Open Telegram and search for “BotFather”.<br>2. Tap Start to start the conversation.<br>3. Type </em><em>/newbot and send it to BotFather.<br>4. Enter a name for your bot.<br>5. Enter a username for your bot. The username must end with the word “bot”.<br>6. BotFather will reply with your bot’s token.<br>7. Copy and paste the token in a safe place.</em></blockquote><ul><li>Create a <a href="https://cloud.mongodb.com/">MongoDB Atlas</a> cluster and set up IP whitelist. Get the <strong><em>MongoDB connection string</em></strong>.</li></ul><blockquote><em>Create a MongoDB Atlas cluster:<br>1. Go to the </em><a href="https://www.mongodb.com/cloud/atlas"><em>MongoDB Atlas</em></a><em>, and sign up for a free account.<br>2. Once you are logged in, click </em>New Cluster<em>.<br>3. Select the cluster tier and region that you want.<br>4. Click </em>Create Cluster<em>.</em></blockquote><blockquote><em>Set up IP whitelist:<br>1. Go to the </em>Security<em> tab of your cluster.<br>2. Under </em>IP Access List<em>, click </em>Add IP Address<em>.<br>3. Enter your IP address and click </em>Add IP Address<em>.<br>4. You can also add a </em>CIDR<em> range to allow access from multiple IP addresses.</em></blockquote><blockquote><em>Get the MongoDB connection string:<br>1. Go to the </em>Connect<em> tab of your cluster.<br>2. Under </em>Connection Strings<em>, select the connection string format that you want.<br>3. Copy and paste the connection string into a safe place. You will need it to connect to your MongoDB Atlas cluster from your Node.js app.<br>4. Set the appropriate credentials in the connection string.</em></blockquote><ul><li>Login to your <a href="https://bard.google.com/">Google Bard</a> account via desktop web app to get the values of <strong><em>__Secure-1PSID</em></strong> and <strong><em>__Secure-1PSIDTS</em></strong> cookies. Like so:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*POoJbnFY32bWpRDrOpTk1Q.png" /></figure><p>Now, as you have all the necessary credentials, you can start building a Node.js app:</p><pre>mkdir sqca &amp;&amp; cd sqca/</pre><p>Initialize a basic Node.js app:</p><pre>npm init -y</pre><p>For a better VCS experience, modify the <strong><em>.gitignore</em></strong> file content using <a href="https://www.toptal.com/developers/gitignore/api/node,intellij+all,jetbrains+all,phpstorm+all,webstorm+all,visualstudiocode,linux,windows,macos,python">this</a>.</p><p>Install some NPM packages that you will use in your app:</p><pre>npm i aws-sdk axios dotenv fluent-ffmpeg mongoose telegraf<br><br># optional<br>npm i -D nodemon</pre><p>In the <strong><em>package.json</em></strong> setup <em>“start”</em> command like this:</p><pre>&quot;scripts&quot;: {<br>  &quot;start&quot;: &quot;node ./src/index.js&quot;,<br>  &quot;dev&quot;: &quot;nodemon ./src/index.js&quot;<br>}</pre><p>As you can see, we have to have some <strong><em>index.js</em></strong> file inside the <strong><em>src</em></strong> folder. Let’s create them:</p><pre>mkdir src &amp;&amp; touch src/index.js</pre><p>As you need to use some functionalities, you can have separate <strong><em>src/utils.js</em></strong> file to implement them:</p><pre>import AWS from &#39;aws-sdk&#39;;<br>import fs from &#39;fs&#39;;<br>import fluent from &#39;fluent-ffmpeg&#39;;<br>import axios from &#39;axios&#39;;<br>import { exec } from &#39;child_process&#39;;<br><br>export default {<br>    // download .oga file<br>    download: async (url, outputPath) =&gt; {<br>        const writer = fs.createWriteStream(outputPath);<br>        const response = await axios({url, method: &#39;GET&#39;, responseType: &#39;stream&#39;});<br><br>        response.data.pipe(writer);<br>    },<br>    // check if user folder exists, if not create it<br>    folderStructureSync: (folder) =&gt; {<br>        if (!fs.existsSync(folder)) {<br>            fs.mkdirSync(folder);<br>        }<br>    },<br>    // convert .oga to .wav<br>    convert: async (inputPath, outputPath) =&gt; {<br>        return await fluent(inputPath).toFormat(&#39;wav&#39;).save(outputPath);<br>    },<br>    // extract text from .wav<br>    speechToText: async (filePath) =&gt; {<br>        const result = await new Promise((resolve, reject) =&gt; {<br>            const pythonExec = process.env.PYTHON_EXEC_PATH || &#39;python&#39;;<br>            exec(`${pythonExec} python/transcribe.py &quot;${filePath}&quot;`, (err, stdout, stderr) =&gt; {<br>                if (err) {<br>                    console.error(err.message);<br>                    reject(err);<br>                } else {<br>                    resolve(stdout);<br>                }<br>            });<br>        });<br><br>        return JSON.parse(result);<br>    },<br>    // get answer from Google Bard<br>    getAnswer: async (text) =&gt; {<br>        const gb1psid = process.env.GOOGLE_BARD_SECURE_1PSID;<br>        const gb1psidts = process.env.GOOGLE_BARD_SECURE_1PSIDTS;<br>        const result = await new Promise((resolve, reject) =&gt; {<br>            const pythonExec = process.env.PYTHON_EXEC_PATH || &#39;python&#39;;<br>            exec(`${pythonExec} python/answer.py &quot;${gb1psid}&quot; &quot;${gb1psidts}&quot; &quot;${text}&quot;`, (err, stdout, stderr) =&gt; {<br>                if (err) {<br>                    console.error(err.message);<br>                    reject(err);<br>                } else {<br>                    resolve(stdout);<br>                }<br>            });<br>        });<br><br>        return JSON.parse(result);<br>    },<br>    // empty folder<br>    emptyFolder: async (folder) =&gt; {<br>        fs.readdir(folder, (err, files) =&gt; {<br>            if (err) {<br>                console.error(err.message);<br>            }<br>            for (const file of files) {<br>                fs.unlink(`${folder}/${file}`, err =&gt; {<br>                    if (err) {<br>                        console.error(err.message);<br>                    }<br>                });<br>            }<br>        });<br>    },<br>    // text to voice<br>    textToVoice: async (text, folderPath) =&gt; {<br>        AWS.config.credentials = new AWS.Credentials(process.env.AWS_ACCESS_KEY, process.env.AWS_SECRET_KEY);<br>        AWS.config.region = &quot;us-west-2&quot;;<br><br>        // text-to-speech service<br>        const Polly = new AWS.Polly({signatureVersion: &#39;v4&#39;, region: &#39;us-west-2&#39;});<br><br>        const params = {&#39;Text&#39;: text, &#39;OutputFormat&#39;: &#39;ogg_vorbis&#39;, &#39;VoiceId&#39;: &#39;Joanna&#39;};<br>        const data = await Polly.synthesizeSpeech(params).promise();<br>        if (data.AudioStream instanceof Buffer) {<br>            const fileName = Date.now();<br>            const filePath = `${folderPath}/${fileName}.wav`;<br>            await fs.writeFile(filePath, data.AudioStream, (err) =&gt; err &amp;&amp; console.error(err));<br><br>            return {success: true, file_path: filePath, message: &#39;Text to voice success!&#39;};<br>        } else {<br>            return {success: false, message: &#39;Text to voice failed!&#39;};<br>        }<br>    },<br>    // filter answer from Google Bard<br>    filterAnswer: async (originalText) =&gt; {<br>        let text = originalText;<br><br>        if (text.includes(&#39;Google Bard&#39;)) {text = text.replace(&#39;Google Bard&#39;, &#39;SQCA bot&#39;);}<br>        if (text.includes(&#39;Bard of Google&#39;)) {text = text.replace(&#39;Bard of Google&#39;, &#39;SQCA bot&#39;);}<br>        if (text.includes(&#39;Bard&#39;)) {text = text.replace(&#39;Bard&#39;, &#39;SQCA bot&#39;);}<br>        if (text.includes(&#39;https://bard.google.com&#39;)) {text = text.replace(&#39;https://bard.google.com&#39;, &#39;boolfalse.com&#39;);}<br><br>        return text;<br>    },<br>};</pre><p>For interacting with DB you can have some <strong><em>src/handlers.js</em></strong>:</p><pre>import User from &quot;../models/userModel.js&quot;;<br>import Message from &quot;../models/messageModel.js&quot;;<br>import settings from &quot;../config/settings.js&quot;;<br><br>export default {<br>    getUser: async (userTelegramId) =&gt; {<br>        return await User.findOne({ telegram_id: userTelegramId });<br>    },<br>    createUser: async (createObj) =&gt; {<br>        return await User.create(createObj);<br>    },<br>    updateUser: async (userId, updateObj) =&gt; {<br>        await User.updateOne({ _id: userId }, updateObj);<br>    },<br>    createMessage: async (createObj) =&gt; {<br>        return await Message.create(createObj);<br>    },<br>    updateMessage: async (messageId, updateObj) =&gt; {<br>        await Message.updateOne({ _id: messageId }, updateObj);<br>    },<br>    isLimitExceeded: async (userId) =&gt; {<br>        const today = new Date();<br>        const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate());<br>        const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);<br>        const voiceMessagesLength = await Message.aggregate([{<br>            $match: {<br>                user: userId,<br>                // answer_status: { $ne: 4 },<br>                answer_status: 1,<br>                createdAt: { $gte: todayStart, $lt: todayEnd },<br>            },<br>        }, {<br>            $group: {<br>                _id: null,<br>                total: { $sum: &#39;$question_voice_duration&#39; },<br>            },<br>        }]);<br>        const voiceMessagesLengthSum = voiceMessagesLength.length &gt; 0 ?<br>            voiceMessagesLength[0].total : 0;<br><br>        const messagesCount = await Message.countDocuments({<br>            user: userId,<br>            answer_status: 1,<br>            createdAt: { $gte: todayStart, $lt: todayEnd },<br>        });<br><br>        return messagesCount &gt;= settings.max_questions_per_day ||<br>            voiceMessagesLengthSum &gt;= settings.max_voice_messages_length_per_day;<br>    },<br>};</pre><p>You can keep some configuration files in a separate <strong><em>config</em></strong> folder.</p><ul><li><strong><em>config/commands.js</em></strong></li></ul><pre>export default {<br>    start: &quot;Start bot (automatically called when you start the bot)&quot;,<br>    help: &quot;Help (show the commands list)&quot;,<br>    test: &quot;Test (on success, the bot will reply with voice)&quot;,<br>    // new: &quot;New session (forget the previous conversation context)&quot;,<br>};</pre><ul><li><strong><em>config/db.js</em></strong></li></ul><pre>import mongoose from &quot;mongoose&quot;;<br><br>const connectDB = async () =&gt; {<br>    try {<br>        const conn = await mongoose.connect(process.env.MONGO_URL);<br>        console.info(`MongoDB Connected: ${conn.connection.host}`);<br>    } catch (err) {<br>        console.error(`Error: ${err.message}`);<br>        process.exit(1);<br>    }<br>};<br><br>export default connectDB;</pre><ul><li><strong><em>config/settings.js</em></strong></li></ul><pre>export default {<br>    max_questions_per_day: 10, // 10 questions per day<br>    max_voice_messages_length_per_day: 60, // 60 seconds per day<br>};</pre><p>Create models in a <strong><em>models</em></strong> folder:</p><ul><li><strong><em>models/messageModel.js</em></strong></li></ul><pre>import mongoose from &#39;mongoose&#39;;<br><br>const messageSchema = mongoose.Schema({<br>    _id: {<br>        type: mongoose.Schema.Types.ObjectId,<br>        required: true,<br>        auto: true,<br>    },<br>    user: {<br>        type: mongoose.Schema.Types.ObjectId,<br>        required: true,<br>        ref: &#39;User&#39;,<br>    },<br>    question_type: {<br>        type: String,<br>        required: true,<br>        default: &#39;text&#39;, // &#39;text&#39;, &#39;voice&#39;, &#39;command&#39;<br>    },<br>    command: { // question_type: command<br>        type: String,<br>        required: false,<br>    },<br>    question_text: { // question_type: text<br>        type: String,<br>        required: false,<br>    },<br>    question_voice_url: { // question_type: voice<br>        type: String,<br>        required: false,<br>    },<br>    question_voice_duration: { // question_type: voice<br>        type: Number,<br>        required: false,<br>    },<br>    answer_type: {<br>        type: String,<br>        required: true,<br>        default: &#39;text&#39;, // &#39;text&#39; or &#39;voice&#39;<br>    },<br>    answer_text: { // answer_type: text<br>        type: String,<br>        required: false,<br>    },<br>    answer_voice_url: { // answer_type: voice<br>        type: String,<br>        required: false,<br>    },<br>    answer_status: {<br>        type: Number,<br>        required: true,<br>        default: 0, // 0: pending, 1: answered, 2: rejected, 3: error, 4: limit_exceeded<br>    },<br>}, {<br>    timestamps: true,<br>});<br><br>const Message = mongoose.model(&#39;Message&#39;, messageSchema);<br>export default Message;</pre><ul><li><strong><em>models/userModel.js</em></strong></li></ul><pre>import mongoose from &#39;mongoose&#39;;<br><br>const userSchema = mongoose.Schema({<br>    _id: {<br>        type: mongoose.Schema.Types.ObjectId,<br>        required: true,<br>        auto: true,<br>    },<br>    telegram_id: {<br>        type: Number,<br>        required: true,<br>        unique: true,<br>    },<br>    name: {<br>        type: String,<br>        required: true,<br>    },<br>    username: {<br>        type: String,<br>        required: true,<br>        unique: true,<br>    },<br>    limit_exceeded: {<br>        type: Boolean,<br>        required: true,<br>        default: false,<br>    },<br>    limited_until: {<br>        type: Date,<br>        required: false,<br>    },<br>    isAdmin: {<br>        type: Boolean,<br>        required: true,<br>        default: false,<br>    },<br>}, {<br>    timestamps: true,<br>});<br><br>const User = mongoose.model(&#39;User&#39;, userSchema);<br>export default User;</pre><p>You may already have noticed, that in <strong><em>src/utils.js</em></strong> there are two methods, where functions calls some external python scripts: <em>transcribe.py</em> for speech-to-text conversion, and <em>answer.py</em> to retrieve the answer from the Google Bard response. Create those script files in a separate <strong><em>python</em></strong> folder:</p><ul><li><strong><em>python/transcribe.py</em></strong></li></ul><pre>import speech_recognition as sr<br>from os import path<br>import json<br>import sys<br><br>if len(sys.argv) &lt; 2:<br>    result = {<br>        &quot;success&quot;: False,<br>        &quot;message&quot;: &quot;Audio file not provided!&quot;<br>    }<br>    print(json.dumps(result))<br><br>AUDIO_FILE = path.join(path.dirname(path.realpath(__file__)), sys.argv[1])<br><br># use the audio file as the audio source<br>r = sr.Recognizer()<br>with sr.AudioFile(AUDIO_FILE) as source:<br>    audio = r.record(source)  # read the entire audio file<br><br># recognize speech using Google Speech Recognition<br>try:<br>    # to use another API key, use `r.recognize_google(audio, key=&quot;GOOGLE_SPEECH_RECOGNITION_API_KEY&quot;)`<br>    result = {<br>        &quot;success&quot;: True,<br>        &quot;message&quot;: r.recognize_google(audio)<br>    }<br>    print(json.dumps(result))<br>except sr.UnknownValueError:<br>    result = {<br>        &quot;success&quot;: False,<br>        &quot;message&quot;: &quot;Google Speech Recognition could not understand audio!&quot;<br>    }<br>    print(json.dumps(result))<br>except sr.RequestError as e:<br>    result = {<br>        &quot;success&quot;: False,<br>        &quot;message&quot;: &quot;Could not request results from Google Speech Recognition service; {0}&quot;.format(e)<br>    }<br>    print(json.dumps(result))</pre><ul><li><strong><em>python/answer.py</em></strong></li></ul><pre>import sys<br>import json<br>from Bard import Chatbot<br><br>def process_parameters(gb1psid, gb1psidts, text):<br>    if gb1psid is None or gb1psidts is None or text is None:<br>        result = {<br>            &quot;success&quot;: False,<br>            &quot;message&quot;: &quot;Command sample: python answer.py \&quot;&lt;gb1psid&gt;\&quot; \&quot;&lt;gb1psidts&gt;\&quot; \&quot;&lt;text&gt;\&quot;&quot;<br>        }<br>        return json.dumps(result)<br><br>    # Process the parameters as needed<br>    chatbot = Chatbot(gb1psid, gb1psidts)<br>    answer = chatbot.ask(text)<br>    result = {<br>        &quot;success&quot;: True,<br>        &quot;message&quot;: answer<br>    }<br>    return json.dumps(result)<br><br><br># Retrieve parameters from command line arguments<br>gb1psid = sys.argv[1] if len(sys.argv) &gt; 1 else None<br>gb1psidts = sys.argv[2] if len(sys.argv) &gt; 2 else None<br>text = sys.argv[3] if len(sys.argv) &gt; 3 else None<br><br># Process the parameters and return the result<br>try:<br>    result = process_parameters(gb1psid, gb1psidts, text)<br>    print(result)<br>except Exception as e:<br>    result = {<br>        &quot;success&quot;: False,<br>        &quot;message&quot;: str(e)<br>    }<br>    print(json.dumps(result))</pre><p>Finally, let’s work on the main <strong><em>src/index.js</em></strong> file. Here’s a high level view of it:</p><pre>// IMPORTS<br>import dotenv from &#39;dotenv&#39;;<br>dotenv.config();<br>import { Telegraf } from &quot;telegraf&quot;;<br>import { message } from &quot;telegraf/filters&quot;;<br>import utils from &quot;./utils.js&quot;;<br>import handlers from &quot;./handlers.js&quot;;<br>import path from &quot;path&quot;;<br>import commands from &quot;../config/commands.js&quot;;<br>import connectDB from &quot;./../config/db.js&quot;;<br>import settings from &quot;../config/settings.js&quot;;<br><br>// INITIALIZATIONS<br>connectDB();<br>const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN);<br><br>// EVENT LISTENERS<br>bot.command(&#39;start&#39;, async (ctx) =&gt; {<br>    // ...<br>});<br>bot.command(&#39;new&#39;, async (ctx) =&gt; {<br>    // ...<br>});<br>bot.command(&#39;help&#39;, async (ctx) =&gt; {<br>    // ...<br>});<br>bot.command(&#39;text&#39;, async (ctx) =&gt; {<br>    // ...<br>});<br>bot.command(&#39;voice&#39;, async (ctx) =&gt; {<br>    // ...<br>});<br><br>// START THE BOT<br>bot.launch();<br><br>// TERMINATION SIGNALS<br>process.once(&#39;SIGINT&#39;, () =&gt; bot.stop(&#39;SIGINT&#39;));<br>process.once(&#39;SIGTERM&#39;, () =&gt; bot.stop(&#39;SIGTERM&#39;));</pre><p>In the above code let’s inject event listener callbacks step by step:</p><ul><li>Start the bot</li></ul><pre>bot.command(&#39;start&#39;, async (ctx) =&gt; {<br>    const userTelegramId = ctx.message.from.id;<br>    let answerMessage = &#39;&#39;;<br><br>    // check if user exists in DB, if not create it<br>    let dbUser = await handlers.getUser(userTelegramId);<br>    if (dbUser) {<br>        // build multiline text message<br>        answerMessage += &#39;I\&#39;m an assistant. Just ask me anything using text or voice messages.&#39;<br>    } else {<br>        dbUser = await handlers.createUser({<br>            telegram_id: userTelegramId,<br>            username: ctx.message.from.username,<br>            name: ctx.message.from.first_name,<br>        });<br>        // build multiline text message<br>        answerMessage = `\u{1F64B} Hello ${dbUser.name}! \n\n`;<br>        answerMessage += &#39;Below are the commands you can use:\n\n&#39;;<br>        Object.keys(commands).forEach((command) =&gt; {<br>            answerMessage += `/${command} - ${commands[command]}\n`;<br>        });<br>        answerMessage += &#39;\nYou can send up to 10 questions per day, and in case of voice messages, up to 60 seconds per day.&#39;;<br>        answerMessage += &#39;\n\nHappy hacking! \u{1F680}&#39;;<br>    }<br><br>    // send text to user<br>    await ctx.reply(answerMessage);<br><br>    // add a message to DB<br>    await handlers.createMessage({<br>        user: dbUser._id,<br>        question_type: &#39;command&#39;,<br>        command: &#39;start&#39;,<br>        answer_text: answerMessage,<br>        answer_status: 1,<br>    });<br>});</pre><ul><li>New chat</li></ul><pre>bot.command(&#39;new&#39;, async (ctx) =&gt; {<br>    const userTelegramId = ctx.message.from.id;<br><br>    // check if user exists in DB, if not create it<br>    let dbUser = await handlers.getUser(userTelegramId);<br>    if (!dbUser) {<br>        dbUser = await handlers.createUser({<br>            telegram_id: userTelegramId,<br>            username: ctx.message.from.username,<br>            name: ctx.message.from.first_name,<br>        });<br>    }<br><br>    let answerMessage = &quot;Previous conversation ended. You can start a new one \u{1F609}&quot;;<br><br>    // send text to user<br>    await ctx.reply(answerMessage);<br><br>    // add a message to DB<br>    await handlers.createMessage({<br>        user: dbUser._id,<br>        question_type: &#39;command&#39;,<br>        command: &#39;new&#39;,<br>        answer_text: answerMessage,<br>        answer_status: 1,<br>    });<br>});</pre><ul><li>Special message for help</li></ul><pre>bot.command(&#39;help&#39;, async (ctx) =&gt; {<br>    const userTelegramId = ctx.message.from.id;<br><br>    // check if user exists in DB, if not create it<br>    let dbUser = await handlers.getUser(userTelegramId);<br>    if (!dbUser) {<br>        dbUser = await handlers.createUser({<br>            telegram_id: userTelegramId,<br>            username: ctx.message.from.username,<br>            name: ctx.message.from.first_name,<br>        });<br>    }<br><br>    // build multiline text message<br>    let answerMessage = `These are bot commands:\n\n`;<br>    Object.keys(commands).forEach((command) =&gt; {<br>        answerMessage += `/${command} - ${commands[command]}\n`;<br>    });<br>    answerMessage += &#39;\nJust use them if need!&#39;;<br><br>    // send text to user<br>    await ctx.reply(answerMessage);<br><br>    // add a message to DB<br>    await handlers.createMessage({<br>        user: dbUser._id,<br>        question_type: &#39;command&#39;,<br>        command: &#39;help&#39;,<br>        answer_text: answerMessage,<br>        answer_status: 1,<br>    });<br>});</pre><ul><li>Text messages</li></ul><pre>bot.on(message(&#39;text&#39;), async (ctx) =&gt; {<br>    const userTelegramId = ctx.message.from.id;<br>    const questionMessage = ctx.message.text;<br><br>    // check if user exists in DB, if not create it<br>    let dbUser = await handlers.getUser(userTelegramId);<br>    if (!dbUser) {<br>        dbUser = await handlers.createUser({<br>            telegram_id: userTelegramId,<br>            username: ctx.message.from.username,<br>            name: ctx.message.from.first_name,<br>        });<br>    }<br><br>    // check if &#39;limited_until&#39; exists and not expired<br>    if (dbUser.limited_until &amp;&amp; new Date() &lt; dbUser.limited_until) {<br>        const answerMessage = `Sorry, you have exceeded the daily limit.` +<br>            `\nIt is max ${settings.max_questions_per_day} messages per day, ` +<br>            `and max ${settings.max_voice_messages_length_per_day} seconds for the voice messages.` +<br>            `\n\nPlease try again tomorrow! \u{1F609}`;<br><br>        // send text to user<br>        await ctx.reply(answerMessage);<br><br>        // add a message to DB<br>        await handlers.createMessage({<br>            user: dbUser._id,<br>            question_type: &#39;text&#39;,<br>            question_text: questionMessage,<br>            answer_status: 4,<br>        });<br><br>        return;<br>    }<br><br>    // get answer from Google Bard<br>    const resAnswerText = await utils.getAnswer(questionMessage);<br>    if (!resAnswerText || !resAnswerText.success) {<br>        // add a message to DB<br>        await handlers.createMessage({<br>            user: dbUser._id,<br>            question_type: &#39;text&#39;,<br>            question_text: questionMessage,<br>            answer_status: 3,<br>        });<br><br>        // send text to user<br>        await ctx.reply(resAnswerText.message || &quot;Sorry. We couldn&#39;t get answer!&quot;);<br><br>        return;<br>    }<br><br>    const folderPath = path.resolve(`./voices/${userTelegramId}`);<br><br>    // check if user folder exists named userTelegramId, if not create it<br>    utils.folderStructureSync(folderPath);<br><br>    // filter/modify the response answer got from Google Bard<br>    const filteredAnswer = await utils.filterAnswer(resAnswerText.message.content);<br><br>    // text to voice<br>    const resTextToVoice = await utils.textToVoice(filteredAnswer, folderPath);<br>    if (!resTextToVoice || !resTextToVoice.success) {<br>        // add a message to DB<br>        await handlers.createMessage({<br>            user: dbUser._id,<br>            question_type: &#39;text&#39;,<br>            question_text: questionMessage,<br>            answer_text: resAnswerText.message?.content || &#39;&#39;,<br>            answer_status: 3,<br>        });<br><br>        // empty folder<br>        await utils.emptyFolder(folderPath);<br><br>        // send text to user<br>        await ctx.reply(filteredAnswer); // || &quot;Sorry. We couldn&#39;t convert text to voice!&quot;<br><br>        return;<br>    }<br><br>    // send voice to user<br>    const answerVoice = await ctx.replyWithVoice({<br>        source: resTextToVoice.file_path,<br>    });<br><br>    // get answer voice<br>    const answerVoiceFile = await ctx.telegram.getFileLink(answerVoice.voice.file_id);<br><br>    // add a message to DB<br>    await handlers.createMessage({<br>        user: dbUser._id,<br>        question_type: &#39;text&#39;,<br>        question_text: questionMessage,<br>        answer_type: &#39;voice&#39;,<br>        answer_text: resAnswerText.message?.content || &#39;&#39;,<br>        answer_voice_url: answerVoiceFile.href || &#39;&#39;,<br>        answer_status: 1,<br>    });<br><br>    // empty folder<br>    await utils.emptyFolder(folderPath);<br><br>    if (!dbUser.isAdmin) {<br>        // review user limit<br>        const limitExceeded = await handlers.isLimitExceeded(dbUser._id);<br>        if (limitExceeded) {<br>            const limit = new Date().getTime() + 24 * 60 * 60 * 1000;<br>            await handlers.updateUser(dbUser._id, {<br>                limit_exceeded: true,<br>                limited_until: new Date(limit),<br>            });<br>        } else {<br>            await handlers.updateUser(dbUser._id, {<br>                limit_exceeded: false,<br>                limited_until: null,<br>            });<br>        }<br>    }<br>});</pre><ul><li>Voice messages</li></ul><pre>bot.on(message(&#39;voice&#39;), async (ctx) =&gt; {<br>    try {<br>        const questionVoiceFile = await ctx.telegram.getFileLink(ctx.message.voice.file_id);<br>        const userTelegramId = ctx.message.from.id;<br><br>        // check if user exists in DB, if not create it<br>        let dbUser = await handlers.getUser(userTelegramId);<br>        if (!dbUser) {<br>            dbUser = await handlers.createUser({<br>                telegram_id: userTelegramId,<br>                username: ctx.message.from.username,<br>                name: ctx.message.from.first_name,<br>                // isAdmin: false,<br>            });<br>        }<br><br>        const fileName = ctx.message.voice.file_unique_id;<br>        const folderPath = path.resolve(`./voices/${userTelegramId}`);<br><br>        // check if &#39;limited_until&#39; exists and not expired<br>        if (!dbUser.isAdmin &amp;&amp; dbUser.limited_until &amp;&amp; new Date() &lt; dbUser.limited_until) {<br>            const answerMessage = `Sorry, you have exceeded the daily limit.` +<br>                `\nIt is max ${settings.max_questions_per_day} messages per day, ` +<br>                `and max ${settings.max_voice_messages_length_per_day} seconds for the voice messages.` +<br>                `\n\nPlease try again tomorrow! \u{1F609}`;<br><br>            // send text to user<br>            await ctx.reply(answerMessage);<br><br>            // add a message to DB<br>            await handlers.createMessage({<br>                user: dbUser._id,<br>                question_type: &#39;voice&#39;,<br>                question_voice_url: questionVoiceFile.href,<br>                answer_status: 4,<br>            });<br><br>            return;<br>        }<br><br>        // check if user folder exists named userTelegramId, if not create it<br>        utils.folderStructureSync(folderPath);<br><br>        // download .oga file<br>        await utils.download(questionVoiceFile.href, `${folderPath}/${fileName}.oga`);<br><br>        // convert .oga to .wav<br>        const resConvert = await utils.convert(`${folderPath}/${fileName}.oga`, `${folderPath}/${fileName}.wav`);<br>        if (!resConvert) {<br>            // add a message to DB<br>            await handlers.createMessage({<br>                user: dbUser._id,<br>                question_type: &#39;voice&#39;,<br>                question_voice_url: questionVoiceFile.href,<br>                answer_status: 3,<br>            });<br><br>            // empty folder<br>            await utils.emptyFolder(folderPath);<br><br>            // send text to user<br>            await ctx.reply(resConvert.message || &quot;Sorry. We couldn&#39;t convert your voice message!&quot;);<br><br>            return;<br>        }<br><br>        // set this timeout to wait for converting (1000-3000ms is enough)<br>        setTimeout(async () =&gt; {<br>            // extract text from .wav<br>            const resSpeechToText = await utils.speechToText(`./../voices/${userTelegramId}/${fileName}.wav`);<br>            if (!resSpeechToText || !resSpeechToText.success) {<br>                // add a message to DB<br>                await handlers.createMessage({<br>                    user: dbUser._id,<br>                    question_type: &#39;voice&#39;,<br>                    question_voice_url: questionVoiceFile.href,<br>                    answer_status: 3,<br>                });<br><br>                // empty folder<br>                await utils.emptyFolder(folderPath);<br><br>                // send text to user<br>                await ctx.reply(resSpeechToText.message || &quot;Sorry. We couldn&#39;t extract text from your voice message!&quot;);<br><br>                return;<br>            }<br><br>            // empty folder<br>            await utils.emptyFolder(folderPath);<br><br>            // get answer from Google Bard<br>            const resAnswerText = await utils.getAnswer(resSpeechToText.message);<br>            if (!resAnswerText || !resAnswerText.success) {<br>                // add a message to DB<br>                await handlers.createMessage({<br>                    user: dbUser._id,<br>                    question_type: &#39;voice&#39;,<br>                    question_text: resSpeechToText.message || &#39;&#39;,<br>                    question_voice_url: questionVoiceFile.href,<br>                    answer_status: 3,<br>                });<br><br>                // send text to user<br>                await ctx.reply(resAnswerText.message || &quot;Sorry. We couldn&#39;t get answer!&quot;);<br><br>                return;<br>            }<br><br>            // filter/modify the response answer got from Google Bard<br>            const filteredAnswer = await utils.filterAnswer(resAnswerText.message.content);<br><br>            // text to voice<br>            const resTextToVoice = await utils.textToVoice(filteredAnswer, folderPath);<br>            if (!resTextToVoice || !resTextToVoice.success) {<br>                await handlers.createMessage({<br>                    user: dbUser._id,<br>                    question_type: &#39;voice&#39;,<br>                    question_text: resSpeechToText.message || &#39;&#39;,<br>                    question_voice_url: questionVoiceFile.href,<br>                    answer_text: resAnswerText.message?.content || &#39;&#39;,<br>                    answer_status: 3,<br>                });<br><br>                // send text to user<br>                await ctx.reply(filteredAnswer); // || &quot;Sorry. We couldn&#39;t convert answer to voice!&quot;<br><br>                return;<br>            }<br><br>            // send voice to user<br>            const answerVoice = await ctx.replyWithVoice({<br>                source: resTextToVoice.file_path,<br>            });<br><br>            // get answer voice url<br>            const answerVoiceFile = await ctx.telegram.getFileLink(answerVoice.voice.file_id);<br><br>            await handlers.createMessage({<br>                user: dbUser._id,<br>                question_type: &#39;voice&#39;,<br>                question_text: resSpeechToText.message || &#39;&#39;,<br>                question_voice_url: questionVoiceFile.href,<br>                question_voice_duration: ctx.message.voice.duration,<br>                answer_type: &#39;voice&#39;,<br>                answer_text: resAnswerText.message?.content || &#39;&#39;,<br>                answer_voice_url: answerVoiceFile.href || &#39;&#39;,<br>                answer_status: 1,<br>            });<br><br>            // empty folder<br>            await utils.emptyFolder(folderPath);<br><br>            if (!dbUser.isAdmin) {<br>                // review user limit<br>                const limitExceeded = await handlers.isLimitExceeded(dbUser._id);<br>                if (limitExceeded) {<br>                    const limit = new Date().getTime() + 24 * 60 * 60 * 1000;<br>                    await handlers.updateUser(dbUser._id, {<br>                        limit_exceeded: true,<br>                        limited_until: new Date(limit),<br>                    });<br>                } else {<br>                    await handlers.updateUser(dbUser._id, {<br>                        limit_exceeded: false,<br>                        limited_until: null,<br>                    });<br>                }<br>            }<br>        }, 3000);<br>    } catch (err) {<br>        console.log(err.message || &quot;Something went wrong!&quot;);<br>    }<br>});</pre><p>For make sure your python scripts will work successfully, install these dependencies (you may use <em>pip3</em> instead of <em>pip</em> for your case):</p><pre>pip install SpeechRecognition<br>pip install bardapi<br>pip install --upgrade GoogleBard</pre><p>In the end, create a <strong><em>voices/.gitignore</em></strong> file which will make you sure to have a directory for temporary audio files, which will be downloaded from Telegram API:</p><pre>*<br>!.gitignore</pre><p>Now your project is probably ready for work. To check that, just run:</p><pre>npm run start</pre><p>If you want to check the project in a development mode, then run:</p><pre>npm run dev</pre><p>If you want to deploy your project on a real server to run that 24/7, then you can use some wildely used dependency like <a href="https://www.npmjs.com/package/forever"><em>forever</em></a>. Make sure to have it installed. You can install it globally:</p><pre>npm i forever -g</pre><p>Run the project like so:</p><pre>forever start -c &quot;npm run start&quot; ./</pre><p>For listing all the forever-processes, run:</p><pre>forever list</pre><p>For stopping the runing project you can use the command below with the actual process-ID:</p><pre>forever stop &lt;PID&gt;</pre><p><strong>That’s it!</strong></p><p>You can check out the demo on this Telegram channel: <a href="https://t.me/sqca_bot"><strong>sqca_bot</strong></a>.<br>Here’s the ⭐ <a href="https://github.com/boolfalse/simple-question-complex-answer"><strong>GitHub repository</strong></a>, where you can find the code.</p><p>Feel free to ask any questions you may have about this article.<br>If you liked this article, please feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more information, you can visit my website: <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you !!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d3572ed5272" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Visualized radio-streaming with React / Vite / Node / Socket.io]]></title>
            <link>https://medium.com/@boolfalse/visualized-radio-streaming-with-react-vite-node-ffmpeg-socket-io-9ed6feb6fcc3?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/9ed6feb6fcc3</guid>
            <category><![CDATA[vitejs]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[socketio]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[radio]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Tue, 31 Oct 2023 02:12:12 GMT</pubDate>
            <atom:updated>2023-10-31T12:47:31.333Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MHgJFli-1Bz5cJeR7PMU5Q.png" /></figure><p>As an intro: <a href="https://coderadio.freecodecamp.org/"><strong><em>coderadio.freecodecamp.org</em></strong></a> was an inspiration for me to make an app and write an article about it.</p><blockquote>There are files in this article where there is a lot of code and no comments or explanations, but I think it will not be difficult to understand the logic written in them. However, if there are any questions about individual parts, feel free to ask some questions here, I will try to respond as soon as possible.</blockquote><p>Initially, I wanted to build an online radio web app that would be able to play random audio tracks of my choice. But I ran into a problem where the server had very limited memory to keep too many audio files.</p><p>The idea to avoid that problem in this article is the following:<br>The app gets audio file information from the remote editable document, downloads the appropriate audio file from the cloud, and continuously adds it to the stream. After the song is finished, it is deleted from the server. And repeats this process over and over by selecting some random track from the tracklist.</p><p>As the owner of the remote tracklist document, you can modify it to change the tracklist.<br>You can also change the next-track selection logic, so it’s up to you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kMw0QPYqY9ksRPq6vnIldw.jpeg" /></figure><p>The remote document containing all the audio file information in my case will be a <a href="https://gist.github.com/boolfalse/6b66a0065c70a33f95e0e831cb0c7e9f#file-tracks-json">GitHub Gist file</a>.<br>The cloud storage in my case will be <a href="https://drive.google.com/drive/folders/1F0Nw6B_aTuwXXy3VcqQOZlZInTgjghbL?usp=sharing">Google Drive</a>, where we’ll keep our audio files with open access.</p><p>At the end of this article, you will have a single-page app that will play the music you want with visualization.</p><p>If you want to check out the ready project now, here’s the ⭐ <a href="https://github.com/boolfalse/radio-streaming-project"><strong>GitHub repository</strong></a> for that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VKlFUjf29Qe1lNxNy3HUpg.gif" /></figure><p>Below is the main tech-stack, that will be used:</p><ul><li><a href="https://react.dev/"><strong>React.js</strong></a><strong> </strong>— as a frontend library</li><li><a href="https://vitejs.dev/"><strong>Vite.js</strong></a> — as a frontend build tool</li><li><a href="https://nodejs.org/en"><strong>Node.js</strong></a> — as a backend for streaming audio</li><li><a href="https://www.ffmpeg.org/"><strong>FFmpeg</strong></a> — as a streaming software</li><li><a href="https://www.typescriptlang.org/"><strong>TypeScript</strong></a> — for better coding experience</li><li><a href="https://socket.io/"><strong>Socket.io</strong></a> — a library for real-time communications</li></ul><blockquote><strong>Requirements:</strong> Make sure you have <a href="https://nodejs.org/en/download">Node.js</a> &amp; <a href="https://ffmpeg.org/download.html">ffmpeg</a> installed on your machine.</blockquote><p>At first, make sure you have Node.js installed on your machine. In my case, there’s Node.js v20.9.0 installed on my machine.</p><p>Let’s start building the project with the installation of Vite:</p><pre>npm create vite@latest</pre><p>It will prompt you to choose some variants for your project. Type the project name, select <strong><em>React</em></strong> with <strong><em>TypeScript</em></strong>. After the project installation, navigate to the created project and install the dependencies:</p><pre>npm i</pre><p>Make sure it’s working as expected by running:</p><pre>npm run dev</pre><p>You can see the result on the default 5173 port: <a href="http://localhost:5173">http://localhost:5173</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/607/1*FhccnN5kwVaHPyy_eipfzg.png" /></figure><p>Before starting the actual development, modify your <strong><em>.gitignore</em></strong> file to be sure that your project is ready for working with Git as well.<br>Here’s a <a href="https://www.toptal.com/developers/gitignore/api/webstorm+all,intellij+all,jetbrains+all,visualstudiocode,node,react">link</a> you can get the content from.<br>Just copy the content and overwrite your existing <strong><em>.gitignore</em></strong>.</p><p>For the frontend, install some necessary packages by running:</p><pre>npm i tsx dotenv cors concurrently axios rc-progress socket.io-client</pre><ul><li><a href="https://www.npmjs.com/package/tsx"><strong><em>tsx</em></strong></a> — for executing TypeScript</li><li><a href="https://npmjs.com/package/dotenv"><strong><em>dotenv</em></strong></a> — for loading environment variables from a “.env” file</li><li><a href="https://www.npmjs.com/package/cors"><strong><em>cors</em></strong></a> — for using CORS capabilities</li><li><a href="https://www.npmjs.com/package/concurrently"><strong><em>concurrently</em></strong></a> — for running backend and frontend by a single command</li><li><a href="https://www.npmjs.com/package/axios"><strong><em>axios</em></strong></a> — for the HTTPS requests</li><li><a href="https://www.npmjs.com/package/rc-progress"><strong><em>rc-progress</em></strong></a> — for the HTML progress bar</li><li><a href="https://www.npmjs.com/package/socket.io-client"><strong>socket.io-client</strong></a><strong> </strong>— client-side library for real-time communication between server and client</li></ul><p>Now let’s do some server-side development.</p><p>Create a new file <strong><em>server/index.ts</em></strong> in a new <strong><em>server</em></strong> directory:</p><pre>console.log(&quot;Hello from Server!&quot;);</pre><p>Add a new run command in <strong><em>package.json</em></strong> in the <em>“scripts” </em>object:</p><pre>&quot;server&quot;: &quot;tsx server/index.ts&quot;,</pre><p>So you can run the server with this:</p><pre>npm run server</pre><p>Now, as you can run the backend successfully, you can work on it.</p><p>Add a new run command in <strong><em>package.json </em></strong>in the <em>“scripts”</em> object:</p><pre>&quot;project&quot;: &quot;concurrently \&quot;npm run server\&quot; \&quot;npm run dev\&quot;&quot;,</pre><p>So you can run backend and frontend by a single command:</p><pre>npm run project</pre><p>Create <strong><em>.env</em></strong> and <strong><em>.env.example</em></strong> files in a root of your project with the following content:</p><pre>APP_ENV=&quot;development&quot;<br>VITE_BACKEND_PORT=3001</pre><p><em>VITE_BACKEND_PORT</em> will be the port for the backend server. You can set that as you want, but I will leave it <em>3001</em>.</p><p>Add <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> features to the <strong><em>vite.config.ts</em></strong>, and get environment variables there by using <em>dotenv</em> package. So it will be like this:</p><pre>import { defineConfig } from &#39;vite&#39;<br>import react from &#39;@vitejs/plugin-react&#39;<br>import &#39;dotenv/config&#39;;<br><br>export default defineConfig({<br>  build: {<br>    minify: process.env.APP_ENV === &#39;production&#39; ? &#39;esbuild&#39; : false,<br>    cssMinify: process.env.APP_ENV === &#39;production&#39;,<br>  },<br>  plugins: [react()],<br>  server: {<br>    https: process.env.APP_ENV === &#39;production&#39;,<br>    proxy: {<br>      &#39;/api&#39;: {<br>        target: `http://localhost:${process.env.VITE_BACKEND_PORT || 3001}`,<br>        changeOrigin: true,<br>        secure: true,<br>      },<br>      &#39;/socket.io&#39;: {<br>        target: `http://localhost:${process.env.VITE_BACKEND_PORT || 3001}`,<br>        ws: true,<br>      },<br>    }<br>  },<br>});</pre><p>Let’s do some improvements and add asset files.</p><p>Add the default track image <strong><em>public/default.png</em></strong>. I will take it from <a href="https://avatars.githubusercontent.com/u/22226570">here</a>.</p><p>Add the background GIF <strong><em>public/moving-clouds.gif</em></strong>. I will take it from <a href="https://i.imgur.com/xCIr6kN.gif">here</a>.</p><p>Now let’s create some types &amp; interfaces for later work.</p><p>Create a new file <strong><em>src/types/trackInfoType.ts</em></strong>:</p><pre>type trackInfoType = {<br>    title: string;<br>    image: string;<br>    duration: number;<br>    time: string;<br>    difference_in_seconds: number;<br>}<br><br>export default trackInfoType;</pre><p>Create a new file <strong><em>src/interfaces/InfoInterface.ts</em></strong>:</p><pre>import trackInfoType from &quot;../types/trackInfoType&quot;;<br><br>export default interface InfoInterface {<br>    setCurrentTrackInfo: (trackInfo: trackInfoType) =&gt; void;<br>    setIsTrackChanged: (isTrackChanged: boolean) =&gt; void;<br>}</pre><p>Create a new file <strong><em>src/interfaces/PlayerInterface.ts</em></strong>:</p><pre>import trackInfoType from &quot;../types/trackInfoType&quot;;<br><br>export default interface PlayerInterface {<br>    defaultTrackInfo: trackInfoType;<br>    isPlaying: boolean;<br>    setIsPlaying: (isPlaying: boolean) =&gt; void;<br>    currentTrackInfo: trackInfoType;<br>    setCurrentTrackInfo: (trackInfo: trackInfoType) =&gt; void;<br>    isTrackChanged: boolean;<br>    setIsTrackChanged: (isTrackChanged: boolean) =&gt; void;<br>}</pre><p>Create a new file <strong><em>src/interfaces/VisualizerInterface.ts</em></strong>:</p><pre>export default interface VisualizerInterface {<br>    isPlaying: boolean;<br>    isTrackInfoReceived: boolean;<br>}</pre><p>Now it’s time to do some backend stuff.</p><p>In the server folder add some necessary packages:</p><pre>npm i --prefix=server/ axios cors dotenv express fluent-ffmpeg openradio socket.io<br>npm i --prefix=server/ -D @types/express @types/fluent-ffmpeg</pre><ul><li><a href="https://www.npmjs.com/package/fluent-ffmpeg"><strong><em>fluent-ffpeg</em></strong></a> — a fluent API to <a href="https://www.ffmpeg.org/">FFMPEG</a> (make sure it’s installed on your local machine)</li><li><a href="https://www.npmjs.com/package/openradio"><strong><em>openradio</em></strong></a> — a simple live streaming library</li><li><a href="https://www.npmjs.com/package/socket.io"><strong><em>socket.io</em></strong></a><strong><em> </em></strong>— server-side library for real-time communication between server and client</li></ul><p>Add <strong><em>server/utils.ts</em></strong> file for some functionality that you will need:</p><pre>const axios = require(&quot;axios&quot;);<br>const fs = require(&quot;fs&quot;);<br>const ffmpeg = require(&quot;fluent-ffmpeg&quot;);<br><br>module.exports = {<br>    downloadFileFromGoogleDrive: async (fileUrl, destinationFile) =&gt; {<br>        try {<br>            const response = await axios({<br>                url: fileUrl,<br>                method: &#39;GET&#39;,<br>                responseType: &#39;stream&#39;,<br>            });<br><br>            const totalSize = response.headers[&#39;content-length&#39;];<br>            let downloadedSize = 0;<br>            let previousProgress = 0;<br><br>            const writer = fs.createWriteStream(destinationFile);<br>            response.data.pipe(writer);<br><br>            response.data.on(&#39;data&#39;, (chunk) =&gt; {<br>                downloadedSize += chunk.length;<br>                const progress = Math.round((downloadedSize / totalSize) * 100);<br>                if (progress !== previousProgress) {<br>                    previousProgress = progress;<br>                    process.stdout.write(`\rDownloading file: ${progress}%; `);<br>                }<br>            });<br><br>            return new Promise((resolve, reject) =&gt; {<br>                writer.on(&#39;finish&#39;, resolve);<br>                writer.on(&#39;error&#39;, reject);<br>            });<br>        } catch (err) {<br>            console.error(err.message);<br>        }<br>    },<br>    getGistFileContent: async (gistId, fileName) =&gt; {<br>        try {<br>            const response = await axios.get(`https://api.github.com/gists/${gistId}`);<br>            const file = response.data.files[fileName];<br>            return file.content;<br>        } catch (err) {<br>            return null;<br>        }<br>    },<br>    getTrackDuration: (trackPath) =&gt; {<br>        return new Promise((resolve, reject) =&gt; {<br>            ffmpeg.ffprobe(trackPath, (err, metadata) =&gt; {<br>                if (err) {<br>                    console.error(err.message || &#39;Error while getting track duration!&#39;);<br>                    reject(err);<br>                }<br>                const trackDuration = metadata.format.duration;<br>                const duration = trackDuration ? Math.floor(trackDuration) : 0;<br><br>                resolve(duration);<br>            });<br>        })<br>    },<br>    exitHandler: (message = &#39;&#39;) =&gt; {<br>        console.log(message);<br>        process.exit(1);<br>    },<br>};</pre><p>Add these environment variables to your <strong><em>.env</em></strong> (it’s good to add to <strong><em>.env.example</em></strong> as well):</p><pre>VITE_SOCKET_PORT=3000<br>VITE_SOCKET_HOST=&quot;http://localhost&quot;<br><br>RADIO_GIST_ID=&quot;6b66a0065c70a33f95e0e831cb0c7e9f&quot;<br>RADIO_PLAYLIST_FILE=&quot;tracks&quot;</pre><p>Write the server logic in <strong><em>src/index.ts</em></strong>:</p><pre>// modules<br>import &#39;dotenv/config&#39;;<br>import fs from &#39;fs&#39;;<br>import path from &#39;path&#39;;<br>import http from &#39;http&#39;;<br>import express from &#39;express&#39;;<br>import openRadio from &#39;openradio&#39;;<br>import cors from &#39;cors&#39;;<br>import {Server as SocketIO} from &#39;socket.io&#39;;<br>import {<br>    downloadFileFromGoogleDrive,<br>    getGistFileContent,<br>    getTrackDuration,<br>    exitHandler<br>} from &#39;./utils&#39;;<br><br>// configs<br>const app = express();<br>const radio = openRadio();<br>app.use(cors());<br>app.use(&quot;/&quot;, express.static(path.join(__dirname, &quot;..&quot;, &quot;dist&quot;)));<br><br>// constants<br>const playlistFile = process.env.RADIO_PLAYLIST_FILE || &#39;tracks&#39;;<br>const backendPort = process.env.VITE_BACKEND_PORT || 3001;<br>const socketPort = process.env.VITE_SOCKET_PORT || 3000;<br>const trackPath = path.join(__dirname, &#39;..&#39;, &#39;public&#39;, &#39;radio.mp3&#39;);<br><br>// socket related<br>const socketServer = http.createServer(app);<br>const io = new SocketIO(socketServer, {<br>    cors: {<br>        origin: [<br>            `http://localhost:${backendPort}`,<br>        ],<br>    },<br>    transports: [&#39;websocket&#39;],<br>});<br>let trackInfo = {<br>    title: &#39;&#39;,<br>    image: &#39;&#39;,<br>    duration: 0,<br>    started_at: 0,<br>};<br>let listenersCount = 0;<br>const manualDelayTrackChangedEventSeconds = 0;<br><br>// routes<br>app.get(&quot;/&quot;, (req, res) =&gt; {<br>    res.setHeader(&quot;Content-Type&quot;, &quot;text/html&quot;);<br>    return res.sendFile(path.join(__dirname, &quot;..&quot;, &quot;dist&quot;, &quot;index.html&quot;));<br>});<br>app.get(&quot;/api&quot;, (req, res) =&gt; {<br>    return res.json({<br>        message: &quot;API URL-endpoint.&quot;,<br>    });<br>});<br>app.get(&quot;/api/track-info&quot;, (req, res) =&gt; {<br>    const differenceInSeconds = Math.floor(Date.now() / 1000) - trackInfo.started_at;<br><br>    return res.status(200).json({<br>        ...trackInfo,<br>        difference_in_seconds: differenceInSeconds,<br>    });<br>});<br>app.get(&#39;/stream&#39;, (req, res) =&gt; {<br>    res.setHeader(&quot;Content-Type&quot;, &quot;audio/mp3&quot;);<br>    radio.pipe(res);<br>});<br>app.get(&quot;/*&quot;, (_req, res) =&gt; {<br>    return res.json({<br>        message: &quot;API URL-endpoint not found!&quot;,<br>    });<br>});<br><br>// Handling errors for any other cases from whole application<br>app.use((err, req, res) =&gt; {<br>    return res.status(500).json({ error: &quot;Something went wrong!&quot; });<br>});<br><br>// create a server<br>http.createServer((req, res) =&gt; {<br>    res.setHeader(&quot;Content-Type&quot;, &quot;audio/mp3&quot;);<br>    radio.pipe(res);<br>});<br><br>// listen on port<br>socketServer.listen(socketPort, () =&gt; {<br>    console.log(`Socket server running at: \x1b[36mhttp://localhost:\x1b[1m${socketPort}/\x1b[0m`);<br>});<br><br>// socket.io<br>io.on(&#39;connection&#39;, (socket) =&gt; {<br>    listenersCount++;<br>    io.emit(&#39;listeners_count&#39;, listenersCount);<br>    socket.on(&#39;disconnect&#39;, () =&gt; {<br>        listenersCount--;<br>        io.emit(&#39;listeners_count&#39;, listenersCount);<br>    });<br>    if (trackInfo.duration &gt; 0) {<br>        setTimeout(() =&gt; {<br>            // console.log(`Playing track: ${trackInfo.title}`);<br>            socket.emit(&#39;track_changed&#39;, trackInfo);<br>        }, manualDelayTrackChangedEventSeconds * 1000);<br>    }<br>});<br><br>app.listen(backendPort, () =&gt; {<br>    console.log(`Backend running at: \x1b[36mhttp://localhost:\x1b[1m${backendPort}/\x1b[0m`);<br>});<br><br>// play track function<br>const playTrack = () =&gt; {<br>    // check if old track exists, delete it<br>    if (fs.existsSync(trackPath)) {<br>        fs.unlinkSync(trackPath);<br>    }<br>    // get playlist from gist<br>    getGistFileContent(process.env.RADIO_GIST_ID, `${playlistFile}.json`)<br>        .then((data) =&gt; {<br>            if (!data) {<br>                exitHandler(&#39;Playlist file is empty!&#39;);<br>            }<br><br>            const playlist = JSON.parse(data);<br>            const randomNumber = Math.floor(Math.random() * playlist.length);<br>            // download random track from the source<br>            downloadFileFromGoogleDrive(playlist[randomNumber].file, trackPath)<br>                .then(async () =&gt; {<br>                    const duration = await getTrackDuration(trackPath);<br>                    if (duration === 0) {<br>                        console.log(&#39;Track duration is 0, skipping...&#39;);<br>                        playTrack();<br>                        return;<br>                    }<br><br>                    radio.play(fs.createReadStream(trackPath));<br><br>                    trackInfo.title = playlist[randomNumber].title;<br>                    trackInfo.image = playlist[randomNumber].image;<br>                    trackInfo.duration = duration; // playlist[randomNumber].duration;<br>                    trackInfo.started_at = Math.floor(Date.now() / 1000);<br><br>                    setTimeout(() =&gt; {<br>                        console.log(`Playing track: ${trackInfo.title}`);<br>                        io.sockets.emit(&#39;track_changed&#39;, trackInfo);<br>                    }, manualDelayTrackChangedEventSeconds * 1000);<br>                })<br>                .catch((err) =&gt; {<br>                    exitHandler(err.message || &#39;Error while downloading track!&#39;);<br>                });<br>        })<br>        .catch((err) =&gt; {<br>            exitHandler(err.message || &#39;Error while getting playlist!&#39;);<br>        });<br>}<br><br>// play track on start<br>playTrack();<br>// play next track when current track ends<br>radio.on(&#39;finish&#39;, () =&gt; {<br>    playTrack();<br>});</pre><p>As you can see in <strong><em>index.ts</em></strong> there is a line:</p><pre>const trackPath = path.join(__dirname, &#39;..&#39;, &#39;public&#39;, &#39;radio.mp3&#39;);</pre><p>It means, you have to have some <strong><em>public/radio.mp3</em></strong>, which always will be sitting in the <strong><em>public</em></strong> folder and will be constantly changing. So ignore it from the Git-visibility by creating a <strong><em>public/.gitignore</em></strong> file:</p><pre>*.mp3</pre><p>Now, let’s work on frontend.</p><p>Create these components in the new <strong><em>src/components</em></strong> folder:</p><ul><li><strong><em>src/components/Info.tsx</em></strong></li></ul><pre>import React from &#39;react&#39;;<br>import {io, Socket} from &#39;socket.io-client&#39;;<br>import InfoInterface from &#39;./../interfaces/InfoInterface.ts&#39;<br>import trackInfoType from &quot;../types/trackInfoType.ts&quot;;<br><br>const socketHost: string = import.meta.env.VITE_SOCKET_HOST;<br>const socketPort: string = import.meta.env.VITE_SOCKET_PORT;<br>const socket: Socket = io(`${socketHost}:${socketPort}`, {<br>    transports: [&#39;websocket&#39;],<br>});<br><br>function Info({<br>                  setCurrentTrackInfo,<br>                  setIsTrackChanged<br>}: InfoInterface) {<br>    const [listenersCount, setListenersCount] = React.useState(0);<br><br>    React.useEffect(() =&gt; {<br>        socket.on(&#39;listeners_count&#39;, (count: number) =&gt; {<br>            setListenersCount(count);<br>        });<br>        socket.on(&#39;track_changed&#39;, (data: trackInfoType) =&gt; {<br>            setCurrentTrackInfo(data);<br>            setIsTrackChanged(true);<br>        });<br>    });<br><br>    return (<br>        &lt;div id=&quot;info_container&quot;&gt;<br>            &lt;p&gt;📻 Listeners: &lt;strong&gt;{listenersCount}&lt;/strong&gt;&lt;/p&gt;<br>            &lt;h2&gt;Online Radio by &lt;a href=&#39;https://boolfalse.com/&#39;&gt;@BoolFalse&lt;/a&gt;&lt;/h2&gt;<br>            &lt;p&gt;This is a simple online radio station. It plays random songs from a defined playlist, which can be updated.&lt;/p&gt;<br>            &lt;p&gt;You can find the source code of this project at GitHub: ⭐ &lt;a href=&quot;https://github.com/boolfalse/radio-streaming-project&quot;&gt;boolfalse/radio-streaming-project&lt;/a&gt;&lt;/p&gt;<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default Info;</pre><ul><li><strong><em>src/components/Player.tsx</em></strong></li></ul><pre>import React from &quot;react&quot;;<br>import { Line } from &#39;rc-progress&#39;;<br>import PlayerInterface from &#39;./../interfaces/PlayerInterface.ts&#39;<br>import {getErrorMessage, getTrackInfo, timeFormat} from &quot;../utils.ts&quot;;<br>import trackInfoType from &quot;../types/trackInfoType.ts&quot;;<br><br>function Player({<br>                    defaultTrackInfo,<br>                    isPlaying,<br>                    setIsPlaying,<br>                    currentTrackInfo,<br>                    setCurrentTrackInfo,<br>                    isTrackChanged,<br>                    setIsTrackChanged,<br>}: PlayerInterface) {<br>    const [firstLoad, setFirstLoad] = React.useState(true);<br>    const progressIntervalSeconds = 1;<br>    const [startTiming, setStartTiming] = React.useState(defaultTrackInfo.time); // start timing (for example, &#39;1:04&#39;)<br>    const [trackTiming, setTrackTiming] = React.useState(defaultTrackInfo.time); // the current timing (for example, &#39;1:12&#39;)<br>    const [trackDuration, setTrackDuration] = React.useState(defaultTrackInfo.time); // formatted duration (for example, &#39;3:45&#39;)<br><br>    const [progressPercent, setProgressPercent] = React.useState(0);<br>    const [btnPlayDisplay, setBtnPlayDisplay] = React.useState(true);<br><br>    const [cubeDegree, setCubeDegree] = React.useState(0);<br>    const cubeRef = React.useRef&lt;HTMLDivElement&gt;(null);<br><br>    const startProgress = () =&gt; {<br>        if (currentTrackInfo.duration === 0) {<br>            return;<br>        }<br>        // here the progressPercent already calculated and set<br>        // here currentTrackInfo.duration already set<br>        const intervalId = setInterval(() =&gt; {<br>            setProgressPercent((prevProgressPercent) =&gt; {<br>                const newProgressPercent = Math.floor((prevProgressPercent + progressIntervalSeconds / currentTrackInfo.duration * 100) * 100) / 100;<br>                if (newProgressPercent &gt;= 100) {<br>                    clearInterval(intervalId);<br>                    // Progress finished!<br>                    return 0;<br>                }<br>                return newProgressPercent;<br>            });<br>        }, progressIntervalSeconds * 1000);<br>    }<br>    const rotateCube = (trackImage: string) =&gt; {<br>        const newCubeDegree = (cubeDegree - 90) % 360;<br>        const nextImage = cubeRef.current!.querySelector(`.pos-${newCubeDegree * -1}`);<br>        nextImage!.querySelector(&#39;img&#39;)!.src = trackImage; // currentTrackInfo.image<br>        setTimeout(() =&gt; {<br>            cubeRef.current!.style.transform = `rotateY(${newCubeDegree}deg)`;<br>            setCubeDegree(newCubeDegree);<br>        }, 0.1 * 1000); // manual delay<br>    }<br><br>    const handlePlay = (play: boolean) =&gt; {<br>        setIsPlaying(play); setBtnPlayDisplay(!play);<br>    };<br><br>    React.useEffect(() =&gt; {<br>        if (isTrackChanged) {<br>            getTrackInfo().then((data: {<br>                                     success: boolean,<br>                                     message: string,<br>                                     track: trackInfoType,<br>            }) =&gt; {<br>                if (data.track.duration &gt; 0) {<br>                    setTrackDuration(timeFormat(data.track.duration));<br>                    setStartTiming(timeFormat(data.track.difference_in_seconds));<br>                    setTrackTiming(timeFormat(data.track.difference_in_seconds));<br><br>                    const percent = Math.floor(data.track.difference_in_seconds / data.track.duration * 100);<br>                    setProgressPercent(percent);<br><br>                    setCurrentTrackInfo(data.track);<br>                    startProgress();<br><br>                    if (firstLoad) {<br>                        const firstImage = cubeRef.current!.querySelector(`.pos-0`);<br>                        firstImage!.querySelector(&#39;img&#39;)!.src = data.track.image;<br>                    }<br><br>                    rotateCube(data.track.image);<br>                } else {<br>                    console.error(&quot;Track duration is 0!&quot;);<br>                }<br>            }).catch((error: Error) =&gt; {<br>                console.error(getErrorMessage(error));<br>            });<br><br>            if (firstLoad) {<br>                setFirstLoad(false);<br>            }<br>            setIsTrackChanged(false);<br>        }<br>        // eslint-disable-next-line react-hooks/exhaustive-deps<br>    }, [isTrackChanged]);<br><br>    React.useEffect(() =&gt; {<br>        setTrackTiming(startTiming === defaultTrackInfo.time ? defaultTrackInfo.time : startTiming);<br>        // eslint-disable-next-line react-hooks/exhaustive-deps<br>    }, [startTiming]);<br>    React.useEffect(() =&gt; {<br>        if (currentTrackInfo.time !== defaultTrackInfo.time) {<br>            const interval = setInterval(() =&gt; {<br>                const [currentMin, currentSec] = trackTiming.split(&#39;:&#39;).map(Number);<br>                const [durationMin, durationSec] = trackDuration.split(&#39;:&#39;).map(Number);<br>                if (currentMin === durationMin &amp;&amp; currentSec === durationSec) {<br>                    clearInterval(interval);<br>                    // Track ended!<br>                } else {<br>                    let newSec = currentSec + 1;<br>                    let newMin = currentMin;<br>                    if (newSec &gt;= 60) {<br>                        newSec = 0;<br>                        newMin += 1;<br>                    }<br>                    const formattedSec = (newSec &lt; 10) ? `0${newSec}` : newSec;<br>                    const formattedMin = (newMin &lt; 10) ? `0${newMin}` : newMin;<br>                    setTrackTiming(`${formattedMin}:${formattedSec}`);<br>                }<br>            }, 1000);<br><br>            return () =&gt; clearInterval(interval);<br>        }<br>        // eslint-disable-next-line react-hooks/exhaustive-deps<br>    }, [trackTiming, trackDuration]);<br><br>    return &lt;&gt;<br>        &lt;div id=&quot;bg_image&quot;&gt;&lt;/div&gt;<br>        &lt;div id=&quot;bg_shadow&quot;&gt;&lt;/div&gt;<br>        &lt;main&gt;<br>            &lt;div id=&quot;image_wrapper&quot;&gt;<br>                &lt;div className=&quot;container&quot;&gt;<br>                    &lt;div className=&quot;image-cube&quot; ref={cubeRef}&gt;<br>                        &lt;div className=&quot;pos-0&quot;&gt;<br>                            &lt;img src={defaultTrackInfo.image} alt=&quot;Track&quot;/&gt;<br>                        &lt;/div&gt;<br>                        &lt;div className=&quot;pos-90&quot;&gt;<br>                            &lt;img src={defaultTrackInfo.image} alt=&quot;Track&quot;/&gt;<br>                        &lt;/div&gt;<br>                        &lt;div className=&quot;pos-180&quot;&gt;<br>                            &lt;img src={defaultTrackInfo.image} alt=&quot;Track&quot;/&gt;<br>                        &lt;/div&gt;<br>                        &lt;div className=&quot;pos-270&quot;&gt;<br>                            &lt;img src={defaultTrackInfo.image} alt=&quot;Track&quot;/&gt;<br>                        &lt;/div&gt;<br>                    &lt;/div&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>            &lt;div id=&quot;track_container&quot;&gt;<br>                &lt;div id=&quot;track&quot; style={{display: (isPlaying ? &#39;block&#39; : &#39;none&#39;)}}&gt;<br>                    &lt;div id=&quot;progress&quot;&gt;<br>                        &lt;Line percent={progressPercent} strokeWidth={2} strokeColor=&quot;#D3D3D3&quot; /&gt;<br>                    &lt;/div&gt;<br>                    &lt;div id=&quot;title&quot;&gt;<br>                        &lt;div id=&quot;track_title&quot;&gt;{currentTrackInfo.title}&lt;/div&gt;<br>                    &lt;/div&gt;<br>                    &lt;div id=&quot;duration_timing&quot;&gt;<br>                        &lt;span id=&quot;track_duration&quot;&gt;{trackDuration}&lt;/span&gt; / &lt;span id=&quot;track_timing&quot;&gt;{trackTiming}&lt;/span&gt;<br>                    &lt;/div&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>            &lt;div id=&quot;controls_container&quot;&gt;<br>                &lt;div className=&quot;control&quot; id=&quot;btn_controls&quot;&gt;<br>                    &lt;i id=&quot;btn_play&quot;<br>                       aria-hidden=&quot;true&quot;<br>                       onClick={() =&gt; handlePlay(true)}<br>                       style={{display: btnPlayDisplay ? &#39;block&#39; : &#39;none&#39;}}<br>                       className=&quot;material-icons icon&quot;&gt;&amp;#xE037;&lt;/i&gt;<br>                    &lt;i id=&quot;btn_pause&quot;<br>                       aria-hidden=&quot;true&quot;<br>                       onClick={() =&gt; handlePlay(false)}<br>                       style={{display: btnPlayDisplay ? &#39;none&#39; : &#39;block&#39;}}<br>                       className=&quot;material-icons icon&quot;&gt;&amp;#xE034;&lt;/i&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        &lt;/main&gt;<br>    &lt;/&gt;<br>}<br><br>export default Player;</pre><ul><li><strong><em>src/components/Visualizer.tsx</em></strong></li></ul><pre>import React from &#39;react&#39;;<br>import VisualizerInterface from &#39;./../interfaces/VisualizerInterface.ts&#39;;<br>import {getErrorMessage} from &quot;../utils.ts&quot;;<br><br>function Visualizer({<br>                        isPlaying,<br>                        isTrackInfoReceived<br>}: VisualizerInterface) {<br>    const [isAudioPlaying, setIsAudioPlaying] = React.useState(isPlaying);<br>    const streamUrl = `http://localhost:${import.meta.env.VITE_BACKEND_PORT}/stream`;<br><br>    const [audioElement, setAudioElement] = React.useState&lt;HTMLAudioElement | null&gt;(null);<br>    const [analyser, setAnalyser] = React.useState&lt;AnalyserNode | null&gt;(null);<br>    const [dataArray, setDataArray] = React.useState&lt;Uint8Array&gt;(new Uint8Array([]));<br>    const visualizerRef = React.useRef&lt;HTMLCanvasElement | null&gt;(null);<br>    const audioContextRef = React.useRef&lt;AudioContext | null&gt;(null);<br><br>    React.useEffect(() =&gt; {<br>        if (isTrackInfoReceived) {<br>            audioContextRef.current = new AudioContext();<br>            const analyserNode = audioContextRef.current?.createAnalyser();<br>            analyserNode.fftSize = 128;<br>            const bufferLength = analyserNode.frequencyBinCount;<br>            const dataArray = new Uint8Array(bufferLength);<br><br>            if (&quot;destination&quot; in audioContextRef.current) {<br>                analyserNode.connect(audioContextRef.current.destination);<br>            }<br>            setAnalyser(analyserNode);<br>            setDataArray(dataArray);<br><br>            return () =&gt; {<br>                analyserNode.disconnect();<br>            };<br>        }<br>    }, [isTrackInfoReceived]);<br><br>    React.useEffect(() =&gt; {<br>        if (audioElement &amp;&amp; analyser) {<br>            if (audioContextRef.current &amp;&amp; &quot;createMediaElementSource&quot; in audioContextRef.current) {<br>                const sourceNode = audioContextRef.current.createMediaElementSource(audioElement);<br>                sourceNode.connect(analyser);<br>            }<br>        }<br>    }, [audioElement, analyser]);<br><br>    React.useEffect(() =&gt; {<br>        if (analyser) {<br>            const renderVisualization = () =&gt; {<br>                if (visualizerRef.current) {<br>                    const canvas: HTMLCanvasElement | null = visualizerRef.current;<br>                    if (&quot;getContext&quot; in canvas) {<br>                        const canvasContext = canvas.getContext(&#39;2d&#39;);<br><br>                        if (canvasContext) {<br>                            const { width, height } = canvas;<br><br>                            analyser.getByteFrequencyData(dataArray);<br><br>                            canvasContext.clearRect(0, 0, width, height);<br><br>                            const barWidth = width / dataArray.length;<br>                            const barHeightMultiplier = height / 255;<br>                            canvasContext.globalAlpha = 0.5;<br>                            for (let i = 0; i &lt; dataArray.length; i++) {<br>                                const barHeight = dataArray[i] * barHeightMultiplier;<br>                                const x = i * barWidth;<br>                                const y = 0;<br>                                canvasContext.fillStyle = `hsl(${i * 2}, 100%, 50%)`;<br>                                canvasContext.fillRect(x, y, barWidth, barHeight);<br>                            }<br><br>                            requestAnimationFrame(renderVisualization);<br>                        }<br>                    }<br>                }<br>            };<br><br>            renderVisualization();<br>        }<br>    }, [analyser, dataArray]);<br><br>    React.useEffect(() =&gt; {<br>        handleTogglePlay().then(r =&gt; r).catch(err =&gt; {<br>            console.error(getErrorMessage(err));<br>        });<br>        // eslint-disable-next-line react-hooks/exhaustive-deps<br>    }, [isPlaying]);<br><br>    const handleTogglePlay = async () =&gt; {<br>        await audioContextRef.current?.resume();<br>        if (audioElement) {<br>            if (isAudioPlaying) {<br>                audioElement.pause();<br>            } else {<br>                await audioElement.play();<br>            }<br>            setIsAudioPlaying(!isAudioPlaying);<br>        }<br>    };<br><br>    return (<br>        &lt;div&gt;<br>            &lt;div id=&quot;visualizer-container&quot;&gt;<br>                &lt;canvas id=&quot;visualizer&quot; ref={visualizerRef} style={{position: &quot;fixed&quot;, zIndex: 1}} /&gt;<br>            &lt;/div&gt;<br>            {isTrackInfoReceived &amp;&amp; (<br>                &lt;audio crossOrigin=&#39;anonymous&#39; ref={setAudioElement} controls style={{ display: &#39;none&#39; }}&gt;<br>                    &lt;source src={streamUrl} type=&#39;audio/mpeg&#39; /&gt;<br>                    &lt;track kind=&#39;captions&#39; /&gt;<br>                &lt;/audio&gt;<br>            )}<br>        &lt;/div&gt;<br>    );<br>}<br><br>export default Visualizer;</pre><p>Create <strong><em>src/utils.ts</em></strong> file for helper functions available in frontend:</p><pre>import axios from &quot;axios&quot;;<br>import trackInfoType from &quot;./types/trackInfoType.ts&quot;;<br><br>export const getErrorMessage = (error: unknown): string =&gt; {<br>    if (error instanceof Error) return error.message<br>    return String(error)<br>}<br><br>export const timeFormat = (duration: number): string =&gt; {<br>    const minutes = Math.floor(duration / 60);<br>    const seconds = duration % 60;<br>    const formattedSeconds = (seconds &lt; 10) ? `0${seconds}` : seconds;<br>    const formattedMinutes = (minutes &lt; 10) ? `0${minutes}` : minutes;<br><br>    return `${formattedMinutes}:${formattedSeconds}`;<br>}<br><br>export const getTrackInfo = async (): Promise&lt;{<br>    success: boolean;<br>    message: string;<br>    track: trackInfoType;<br>}&gt; =&gt; {<br>    try {<br>        const response: {<br>            data: trackInfoType<br>        } = await axios.get(&#39;/api/track-info&#39;);<br><br>        return {<br>            success: true,<br>            message: &quot;Track info fetched successfully.&quot;,<br>            track: response.data, // { title, image, duration, difference_in_seconds, time }<br>        };<br>    } catch (err) {<br>        return {<br>            success: false,<br>            message: getErrorMessage(err),<br>            track: { title: &#39;&#39;, image: &#39;&#39;, duration: 0, difference_in_seconds: 0, time: &#39;&#39; },<br>        }<br>    }<br>}</pre><p>Modify <strong><em>src/vite-env.d.ts</em></strong> to be able to access vite-specific environment variables․</p><pre>/// &lt;reference types=&quot;vite/client&quot; /&gt;<br><br>interface ImportMetaEnv {<br>    readonly VITE_SOCKET_PORT: string,<br>    readonly VITE_BACKEND_PORT: string,<br>    readonly VITE_SOCKET_HOST: string,<br>}<br><br>interface ImportMeta {<br>    readonly env: ImportMetaEnv<br>}</pre><p>Delete <strong><em>src/App.css</em></strong>.</p><p>Modify <strong><em>src/index.css</em></strong>:</p><pre>body {background-color: whitesmoke;color: rgba(255, 255, 255, 0.7);font-family: &quot;Montserrat&quot;, sans-serif;cursor: default;margin: auto 0;}<br>#bg_image {background: url(&#39;/moving-clouds.gif&#39;) 0/cover fixed;position: fixed;top: 0;right: 0;bottom: 0;left: 0;box-shadow: inset 0 0 200px #000;filter: blur(20px);animation: blurAnimation 24s infinite;}<br>#bg_shadow {position: fixed;top: 0;right: 0;bottom: 0;left: 0;background-color: rgba(0, 0, 0, 0.6);animation: blurAnimation 18s infinite;}<br><br>main {position: absolute;top: 2rem;right: 0;left: 0;bottom: 2rem;padding: 0 calc(50% - 8rem);text-align: center;}<br>#track_container {margin-top: 20px;height: 96px;}<br><br>#image_wrapper {border: 1px solid #ffffff;height: 160px;width: 210px; /*position: absolute;*/ margin: auto;left: 0;right: 0;top: 0;bottom: 0;}<br>#image_wrapper .container {height: 100%;width: 100%;display: flex;justify-content: center;align-items: center;perspective: 800px;perspective-origin: 50%;}<br>#image_wrapper .image-cube {width: 210px;height: 160px;transform-style: preserve-3d;position: relative;transition: 2s;}<br>#image_wrapper .image-cube div {height: 160px;width: 210px;position: absolute;}<br>#image_wrapper img {width: 100%;height: 100%;transform: translateZ(0);}<br>#image_wrapper .pos-0 {transform: translateZ(105px);}<br>#image_wrapper .pos-90 {transform: rotateY(-270deg) translateX(105px);transform-origin: 100% 0;}<br>#image_wrapper .pos-180 {transform: translateZ(-105px) rotateY(180deg);}<br>#image_wrapper .pos-270 {transform: rotateY(270deg) translateX(-105px);transform-origin: 0 50%;}<br><br>#progress {width: 100%;background-color: rgba(142, 166, 208, 0.2);}<br>#title {margin: 1rem 0;width: 100%;overflow: hidden;height: 1.1rem;}<br>#track_title {animation: textAnimation 16s linear infinite;}<br><br>#duration_timing {margin: 0;width: 100%;text-align: center;}<br>#controls_container {display: flex;align-items: center;justify-content: center;}<br><br>.control {width: 2.7rem;height: 2.7rem;border-radius: 50%;border: 1px solid;cursor: pointer;display: inline-block;margin: 1px;transition: transform 0.3s, box-shadow 0.5s, text-shadow 0.5s;}<br>.control:hover, .control:focus {color: #fff;transform: scale(1.05);box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);text-shadow: 0 0 8px rgba(0, 0, 0, 0.5);}<br>.control:active {transform: scale(0.9);box-shadow: none;text-shadow: none;}<br>.control .icon {font-size: 2.7rem;}<br><br>#btn_controls {width: 4rem;height: 4rem;font-size: 4rem;}<br>#btn_controls .icon {font-size: 4rem;}<br><br>#visualizer-container {position: fixed;top: 0;left: 0;width: 100%;height: 200px;background-color: transparent;overflow: hidden;z-index: 1;}<br>#visualizer {width: 100%;height: 200px;display: flex;justify-content: center;align-items: flex-start;transition: opacity 1s ease;}<br><br>#info_container {position: absolute;width: 100%;bottom: 0;z-index: 1;color: white;text-align: center;}<br>#info_container a {color: white;text-decoration: none;}<br><br>.ss-rotate-90 {display: inline-block;transform: rotate(90deg);}<br>.ss-ml-10 {margin-left: 10px;}<br><br>@keyframes blurAnimation {0% {filter: blur(20px);} 25% {filter: blur(5px);} 50% {filter: blur(15px);} 75% {filter: blur(0);} 100% {filter: blur(20px);} }<br>@keyframes textAnimation {from { transform: translateX(100%);} to {transform: translateX(-100%);} }</pre><p>Modify <strong><em>src/App.tsx</em></strong>:</p><pre>import Player from &quot;./components/Player.tsx&quot;;<br>import Visualizer from &quot;./components/Visualizer.tsx&quot;;<br>import React from &quot;react&quot;;<br>import Info from &quot;./components/Info.tsx&quot;;<br><br>function App() {<br>    const [isPlaying, setIsPlaying] = React.useState(false);<br>    const [isTrackChanged, setIsTrackChanged] = React.useState(false);<br>    const defaultTrackInfo = {<br>        title: &#39;Artist - Title&#39;,<br>        image: &#39;/default.png&#39;,<br>        duration: 0,<br>        time: &#39;0:00&#39;,<br>        difference_in_seconds: 0,<br>    };<br>    const [currentTrackInfo, setCurrentTrackInfo] = React.useState(defaultTrackInfo);<br><br>    return (<br>        &lt;&gt;<br>            &lt;Visualizer isPlaying={isPlaying}<br>                        isTrackInfoReceived={currentTrackInfo.duration !== defaultTrackInfo.duration}<br>            /&gt;<br>            &lt;Player defaultTrackInfo={defaultTrackInfo}<br>                    isPlaying={isPlaying}<br>                    setIsPlaying={setIsPlaying}<br>                    currentTrackInfo={currentTrackInfo}<br>                    setCurrentTrackInfo={setCurrentTrackInfo}<br>                    isTrackChanged={isTrackChanged}<br>                    setIsTrackChanged={setIsTrackChanged}<br>            /&gt;<br>            &lt;Info setCurrentTrackInfo={setCurrentTrackInfo}<br>                  setIsTrackChanged={setIsTrackChanged}<br>            /&gt;<br>        &lt;/&gt;<br>    )<br>}<br><br>export default App</pre><p>Lastly, do some small changes in <strong><em>index.html</em></strong>, so you will have something like this:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;en&quot;&gt;<br>&lt;head&gt;<br>  &lt;meta charset=&quot;UTF-8&quot; /&gt;<br>  &lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&gt;<br>  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;<br>  &lt;title&gt;Radio Streaming Project&lt;/title&gt;<br>  &lt;link rel=&quot;stylesheet&quot; href=&quot;https://fonts.googleapis.com/css?family=Montserrat&quot;&gt;<br>  &lt;link rel=&quot;stylesheet&quot; href=&quot;https://fonts.googleapis.com/icon?family=Material+Icons&quot;&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;<br>&lt;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&gt;&lt;/script&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Now, your backend and frontend are ready.</p><p>Let’s build the frontend using Vite, which will generate a <strong><em>dist </em></strong>folder in the root of the project:</p><pre>npm run build</pre><p>Run the project:</p><pre>npm run project</pre><p>You can check the result at <a href="http://localhost:3000/"><em>http://localhost:3000/</em></a>.</p><p>So you have done all the things you wanted.</p><p><strong>That’s it!</strong></p><p>Here’s the ⭐ <a href="https://github.com/boolfalse/radio-streaming-project"><strong>GitHub repository</strong></a>, where you can find the appropriate project. So you can check that out by following the steps provided there.</p><p>Feel free to ask any questions you may have about this article.<br>If you liked this article, please feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more information, you can visit my website: <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you !!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ed6feb6fcc3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setup Laravel (PHP, MySQL, Node.js) on server with a bash script]]></title>
            <link>https://medium.com/@boolfalse/setup-laravel-php-mysql-node-js-on-server-with-a-bash-script-ad2110f6abb3?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad2110f6abb3</guid>
            <category><![CDATA[aws-ec2]]></category>
            <category><![CDATA[digitalocean]]></category>
            <category><![CDATA[laravel]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Fri, 27 Oct 2023 23:08:51 GMT</pubDate>
            <atom:updated>2023-10-28T05:36:02.403Z</atom:updated>
            <content:encoded><![CDATA[<p>Setting up the app on the server is the next important task. In my years of experience, I&#39;ve a lot of cases when I needed to deploy my Laravel app to the server. It can be AWS EC2, DigitalOcean, Linode or any other platform. Every time I looked for the commands to install this or that software part, which took some time.<br>For that reason, I&#39;ve made some ready-made stuff to do that without doing repetitive problem-solving.</p><p>In this article, I will share a single bash script that will deploy not only a Laravel app on the server, but install and setup some other related technologies for you as well.<br>I have tested the script using on AWS EC2, but you can use it elsewhere.</p><p>If you want to check out the ready script now, here&#39;s the ⭐ <a href="https://gist.github.com/boolfalse/87969129638e77db031a40b49ddf7aea"><strong>GitHub Gist</strong></a> for that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/759/1*dfmEtHesjOLoFmn_EnKcUQ.png" /><figcaption>Setup Laravel with PHP, MySQL, Node.js and ElasticSearch on a server (Ubuntu 22.04)</figcaption></figure><blockquote><strong>STEPS:</strong></blockquote><p>Below are the steps that will be implemented during running the custom bash script:</p><ul><li>Install Node.js (v20.x in the example).</li><li>Install Apache.</li><li>Install MySQL (v8.* in the example).</li><li>Setup MySQL user, role, privileges, database.</li><li>Install PHP (v8.2 in the example).</li><li>Install Composer.</li><li>Setup the project.</li><li>Setup server.</li><li>Setup project&#39;s files-directories permissions/ownerships.</li><li>Setup ElasticSearch (v7.x in the example).</li></ul><blockquote><strong>HOW-TO-USE INSTRUCTIONS:</strong></blockquote><p>For using a ready script for your own, you need to use these instructions:</p><ul><li>Login to your server (instance):</li></ul><pre>ssh -i ~/.ssh/private-key.pem ubuntu@&lt;SERVER_IPv4&gt;</pre><ul><li>Create or place the <a href="https://gist.githubusercontent.com/boolfalse/87969129638e77db031a40b49ddf7aea/raw/setup.sh"><strong><em>setup.sh</em></strong></a> file with the content provided somewhere in your server. It’s recommended to have that in the <strong><em>home</em></strong> directory:</li></ul><pre>cd ~</pre><ul><li>Allow <strong><em>setup.sh</em></strong> for execution permission:</li></ul><pre>chmod +x setup.sh</pre><ul><li>Run shell script using project&#39;s remote repo (GitHub repo is in our case) URL as a parameter:</li></ul><pre>./setup.sh &quot;git@github.com:&lt;USERNAME&gt;/&lt;REPO&gt;.git&quot;</pre><ul><li><strong>That&#39;s it.</strong> At this point you can check the public URL of the instance after the script is done.</li></ul><blockquote><strong>SNIPPETS:</strong></blockquote><p>Now I will write all the snippets for each step, so in case you want to use them separately:</p><ul><li><strong>Install Node.js 20.x</strong></li></ul><pre>echo &quot;1. *** INSTALL NODE 20.x&quot;<br>curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -<br>sudo apt update<br>sudo apt-get install -y nodejs</pre><ul><li><strong>Install Apache</strong></li></ul><pre>echo &quot;2. *** INSTALL APACHE&quot;<br>sudo apt update<br>sudo apt install -y apache2</pre><ul><li><strong>Install and Setup MySQL</strong></li></ul><pre>echo &quot;3. *** INSTALL AND SETUP MYSQL&quot;<br>sudo apt update<br>sudo apt install -y mysql-server<br>MYSQL_PASSWORD=$(tr -dc &#39;A-Za-z0-9!&quot;#$%&amp;&#39;\&#39;&#39;()*+,-./:;&lt;=&gt;?@[\]^_`{|}~&#39; &lt;/dev/urandom | head -c 13  ; echo)<br>echo &quot;Please use the generated password below as a MySQL password !!!&quot;<br>echo &quot;***************************************************************&quot;<br>echo &quot;***************************************************************&quot;<br>echo &quot;********************                       ********************&quot;<br>echo &quot;********************     ${MYSQL_PASSWORD}     ********************&quot;<br>echo &quot;********************                       ********************&quot;<br>echo &quot;***************************************************************&quot;<br>echo &quot;***************************************************************&quot;<br>echo &quot;Please use the generated password above as a MySQL password !!!&quot;<br><br># No - (Would you like to setup VALIDATE PASSWORD component?)<br># $MYSQL_PASSWORD<br># $MYSQL_PASSWORD<br># y - Remove anonymous users?<br># No - Disallow root login remotely?<br># No - Remove test database and access to it?<br># y - Reload privilege tables now?<br><br>sudo mysql_secure_installation &lt;&lt;EOF<br>No<br>$MYSQL_PASSWORD<br>$MYSQL_PASSWORD<br>y<br>No<br>y<br>y<br>EOF</pre><ul><li><strong>Working with MySQL</strong></li></ul><pre>echo &quot;4. *** WORKING WITH MYSQL&quot;<br>touch mysql.sql<br>echo &quot; &quot; &gt; mysql.sql<br>sed -i &quot;1i FLUSH PRIVILEGES;&quot; mysql.sql<br>sed -i &quot;1i GRANT ALL PRIVILEGES ON * . * TO &#39;project&#39;@&#39;localhost&#39;;&quot; mysql.sql<br>sed -i &quot;1i CREATE USER &#39;project&#39;@&#39;localhost&#39; IDENTIFIED BY \&quot;${MYSQL_PASSWORD}\&quot;;&quot; mysql.sql<br>sudo mysql &lt; &quot;mysql.sql&quot;<br>rm mysql.sql<br>mysql -u project -p$MYSQL_PASSWORD -Bse &quot;CREATE DATABASE project&quot;</pre><ul><li><strong>Install PHP 8.2</strong></li></ul><pre>echo &quot;5. *** INSTALL PHP-8.2&quot;<br>sudo apt update<br>sudo apt -y install software-properties-common<br>sudo add-apt-repository ppa:ondrej/php<br># [ENTER]<br>sudo apt update<br>sudo apt -y install php8.2<br>sudo apt update<br>sudo apt install -y php8.2-cli php8.2-json php8.2-common php8.2-mysql php8.2-zip php8.2-gd php8.2-mbstring php8.2-curl php8.2-xml php8.2-bcmath php8.2-soap<br>sudo apt update<br>sudo apt install unzip</pre><ul><li><strong>Install Composer</strong></li></ul><pre>echo &quot;6. *** INSTALL COMPOSER&quot;<br>curl -sS https://getcomposer.org/installer -o composer-setup.php<br>HASH=&quot;curl -sS https://composer.github.io/installer.sig&quot;<br>php -r &quot;if (hash_file(&#39;SHA384&#39;, &#39;composer-setup.php&#39;) === &#39;$HASH&#39;) { echo &#39;Installer verified&#39;; } else { echo &#39;Installer corrupt&#39;; unlink(&#39;composer-setup.php&#39;); } echo PHP_EOL;&quot;<br>sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer<br>rm composer-setup.php</pre><ul><li><strong>Setup the Project</strong></li></ul><pre>echo &quot;7. *** SETUP THE PROJECT&quot;<br>cd /var/www/<br>sudo mkdir web<br>sudo chown -R $USER:$USER web/<br>cd web/<br>git clone $1 project<br>cd project/<br>composer install<br>cp .env.example .env<br>sed -i -e &quot;s/DB_DATABASE=laravel/DB_DATABASE=project/g&quot; .env<br>sed -i -e &quot;s/DB_USERNAME=root/DB_USERNAME=project/g&quot; .env<br>sed -i -e &quot;s/DB_PASSWORD=/DB_PASSWORD=\&quot;${MYSQL_PASSWORD}\&quot;/g&quot; .env<br>php artisan key:generate<br>php artisan storage:link<br>php artisan route:clear<br>php artisan config:cache<br>php artisan optimize<br>php artisan migrate<br>php artisan db:seed</pre><ul><li><strong>Setup Apache Configs (as known as Virtual Server)</strong></li></ul><pre>echo &quot;8. *** SETUP VIRTUAL SERVER&quot;<br>touch 000-default.conf<br>echo &quot;&lt;VirtualHost *:80&gt;<br>        #ServerName project.site<br>        DocumentRoot /var/www/web/project/public<br>        &lt;Directory &quot;/var/www/web/project&quot;&gt;<br>                Options Indexes FollowSymLinks<br>                AllowOverride all<br>                Require all granted<br>        &lt;/Directory&gt;<br>        ErrorLog ${APACHE_LOG_DIR}/error.log<br>        CustomLog ${APACHE_LOG_DIR}/access.log combined<br>&lt;/VirtualHost&gt;&quot; &gt; 000-default.conf<br>sudo chmod 644 000-default.conf<br>sudo chown root:root 000-default.conf<br>sudo mv /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/000-default_backup.conf<br>sudo mv 000-default.conf /etc/apache2/sites-available/<br>sudo a2enmod rewrite<br>sudo systemctl restart apache2</pre><ul><li><strong>Setup Project files-folders Permissions/Ownerships</strong></li></ul><pre>echo &quot;9. *** SETUP PROJECT FILES-FOLDERS PERMISSIONS/OWNERSHIPS&quot;<br># APACHE_USERNAME=$(ps -ef | egrep &#39;(httpd|apache2|apache)&#39; | grep -v `whoami` | grep -v root | head -n1 | awk &#39;{print $1}&#39;)<br>APACHE_USERNAME=&quot;www-data&quot;<br>sudo usermod -a -G $APACHE_USERNAME $USER<br>cd /var/www/web/project<br>sudo chown -R $USER:$APACHE_USERNAME storage/ bootstrap/cache/<br>sudo chgrp -R $APACHE_USERNAME storage bootstrap/cache/<br>sudo chmod -R ug+rwx storage bootstrap/cache/<br>git checkout .</pre><ul><li><strong>Setup ElasticSearch 7.x</strong></li></ul><pre>echo &quot;10. *** SETUP ELASTICSEARCH 7.x&quot;<br>cd ~<br>curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -<br>echo &quot;deb https://artifacts.elastic.co/packages/7.x/apt stable main&quot; | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list<br>sudo apt update<br>sudo apt install elasticsearch<br>#sudo systemctl start elasticsearch<br>#sudo systemctl enable elasticsearch</pre><ul><li><strong>All done!</strong></li></ul><p>If you liked this article, please feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more info you can visit my website <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you !!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad2110f6abb3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AmeriaBank & IDram V-POS integration to your Laravel (PHP) app]]></title>
            <link>https://medium.com/@boolfalse/ameriabank-idram-v-pos-integration-to-your-laravel-php-app-ba51816e6acb?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/ba51816e6acb</guid>
            <category><![CDATA[ameria]]></category>
            <category><![CDATA[ameriabank]]></category>
            <category><![CDATA[armenia]]></category>
            <category><![CDATA[idram]]></category>
            <category><![CDATA[laravel]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Fri, 27 Oct 2023 09:11:52 GMT</pubDate>
            <atom:updated>2023-10-28T10:56:37.700Z</atom:updated>
            <content:encoded><![CDATA[<p>Often, online services do not have convenient documentation for developers. And sometimes difficulties arise when following the existing documentation. In this article, I will share the information I have and my experience about two online services that can help you to have some initial insights and integrate them in your app more easily.</p><p>I will describe the steps you need to follow for these two Virtual-POS (point of sale) systems to integrate in your Laravel app:</p><ul><li><a href="https://ameriabank.am/en/"><strong>AmeriaBank</strong></a> vPOS integration.</li><li><a href="https://www.idram.am/"><strong>IDram</strong></a> V-POS integration.</li></ul><p>Both of these are frequently used online methods in recent years in the Armenian e-market and not only.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lybyIeDEHrQTDxaLAdaCAg.png" /></figure><p><strong>BEFORE THE BEGINNING:</strong></p><p>Before starting to integrate any of these pieces of software, contact the official representative of the respective department through one of the contact methods listed on their official website (recommended by phone), who will provide you the credentials and the necessary information you may need.</p><p>In case of Amerabank representative’s agreement, they will create a test environment for your app and you will receive an e-mail of the following form from the employee։</p><p><strong><em>AmeriaBank Credentails Email:</em></strong></p><pre>Below is the real environment data of Ameriabank vPOS payment system integration:<br><br>Main address:<br><a href="https://services.ameriabank.am/VPOS/"><strong><em>https://services.ameriabank.am/VPOS/</em></strong></a><br><br>Address for receiving information about transactions:<br><a href="https://payments.ameriabank.am/Admin/webservice/TransactionsInformationService.svc?wsdl"><strong><em>https://payments.ameriabank.am/Admin/webservice/TransactionsInformationService.svc?wsdl</em></strong></a><br><br><strong>ClientID</strong>: ********-****-****-****-************<br><strong>ClientUsr</strong>: **********<br><strong>ClientPass</strong>: ****************<br><br>You can see the transactions made on your website with this link:<br><a href="https://payments.ameriabank.am/admin/clients/"><strong><em>https://payments.ameriabank.am/admin/clients/</em></strong></a><br><br>Credentials:<br><strong>Username</strong>: ************<br><strong>Password</strong>: ************<br><br>With respect,<br>vPOS support team</pre><p><strong><em>AmeriaBank Information Email:</em></strong></p><pre>## vPOS Testing Credentials:<br><br>The <strong>ConfirmPayment</strong> request is required for two-step payments for the implementation of the second stage and withdrawal of money from the buyer&#39;s (cardholder&#39;s) account.<br>If you plan to make payments in one round, then there is no need to perform the mentioned function.<br>Please make the appropriate correction and make test payments.<br><br>We inform you that the AmeriaBank VPOS system test environment has been created for you.<br>A brief description of system integration is attached.<br>You can also see the mentioned information at the following link:<br><a href="https://servicestest.ameriabank.am/VPOS/help"><strong><em>https://servicestest.ameriabank.am/VPOS/help</em></strong></a><br><br>In the test environment, the <strong>OrderID</strong> should be selected from the range <strong>2350301-2350400</strong>, and the amount should be <strong>10 AMD</strong>.<br>In the default test system, the ability to make one-step payments, <strong>Refund</strong> and <strong>Cancel</strong> functions are provided.<br>In order to make 2-step payments and related transactions, it is necessary to inform additionally in order to obtain the relevant authority.<br>It should be noted that the testing is considered carried out, if made through the Rest protocol and <strong>at least 5 successful payments</strong> with completed status are recorded.<br>Please confirm with a return email that you have received the data.<br>And in case of technical questions, you can call or write to us.<br><br>## vPOS minimum requirements and technical instructions:<br><br>The list of minimum requirements and technical guide for installing vPOS on the website are presented on the website:<br><a href="https://ecommerce.ameriabank.am/"><strong><em>https://ecommerce.ameriabank.am/</em></strong></a>.<br><br>Please also provide a demo version of the site that can be viewed.</pre><p><strong><em>AmeriaBank Documents Email:</em></strong></p><ul><li><a href="https://mega.nz/file/bxIVnSxA#btQLXFP-iEKz5GDrSzqYSW7bJS0Y7tHwGo7kevl95aU">vPOS minimum requirements</a></li><li>AmeriaBank vPOS 3.0: <a href="https://mega.nz/file/v0ZiSR7Z#fx0A5kIfvN3Gff-VFvpoBLah-4ptVIw8B77XxKYFNHg">API Protocol Description (Eng)</a></li><li>AmeriaBank vPOS 3.0: <a href="https://mega.nz/file/r9YQULAK#TWz0JjnU6Jz-jAq7hhDsNrEi-qXG_Q2mDR7aLf_LQcM">API նկարագրություն (Arm)</a></li><li>Ameriabank CJSC POS Service: <a href="https://ameriabank.am/Portals/0/files/Business/pos/POS_Terms_and_Conditions_eng.pdf">Terms and Conditions</a></li></ul><p>Let’s integrate <strong>AmeriaBank vPOS 3.0</strong>.</p><p><a href="https://ameriabank.am/en/business/micro/more/other-services/ecommerce">AmeriaBank vPOS</a> (virtual POS terminal) is a free software tool, which enables the merchant to avoid issues with time-consuming development, configuration, certification and further modification of software. It gives an opportunity to have a fast and secure solution for accepting online card payments.</p><p>At first, add some environment variables to your <strong><em>.env</em></strong> file:</p><pre>AMERIABANK_CLIENT_ID=&quot;********-****-****-****-************&quot;<br>AMERIABANK_USERNAME=&quot;**********&quot;<br>AMERIABANK_PASSWORD=&quot;******&quot;<br>AMERIABANK_SUBDOMAIN=&quot;testpayments&quot;<br><br>AMERIABANK_TEST_CARD_NUMBER=&quot;****************&quot;<br>AMERIABANK_TEST_CARD_CARDHOLDER=&quot;TEST CARD VPOS&quot;<br>AMERIABANK_TEST_CARD_EXP_DATE=&quot;06/26&quot;<br>AMERIABANK_TEST_CARD_CVV=&quot;***&quot;</pre><p>AMERIABANK_SUBDOMAINmay be one of these:</p><ul><li>testpayments — for testing</li><li>services — for production (recommended for live)</li><li>payments — another subdomain</li></ul><p>Create a custom <strong><em>config/payment.php</em></strong> payment-specific configurations file:</p><pre>&#39;ameria&#39; =&gt; [<br>    &#39;client_id&#39; =&gt; env(&#39;AMERIABANK_CLIENT_ID&#39;),<br>    &#39;username&#39; =&gt; env(&#39;AMERIABANK_USERNAME&#39;),<br>    &#39;password&#39; =&gt; env(&#39;AMERIABANK_PASSWORD&#39;),<br>    &#39;subdomain&#39; =&gt; env(&#39;AMERIABANK_SUBDOMAIN&#39;, &#39;testpayments&#39;),<br>],</pre><p>Add a webhook route to your <strong><em>routes/web.php</em></strong>:</p><pre>Route::post(&#39;/payment/ameria/{code}&#39;, &#39;PaymentController@ameria&#39;);</pre><p><strong>Important:</strong> Don’t forget to disable any firewalls or middlewares that could block the incoming webhook-requests to your app.</p><p>Refresh caches after modifying configuration files and routes:</p><pre>php artisan optimize</pre><p>Below is the <strong><em>app/Http/Controllers/PaymentController</em></strong> snippet which can be used to accept payment webhook:</p><pre>public function ameria(Request $request, string $code)<br>{<br>    $sale_data = [<br>        &#39;payment_method&#39; =&gt; &#39;card&#39;,<br>        &#39;payment_provider&#39; =&gt; &#39;ameria&#39;,<br>        &#39;payment_status&#39; =&gt; &#39;pending&#39;,<br>        &#39;payment_json&#39; =&gt; [<br>            &#39;request&#39; =&gt; null,<br>            &#39;response&#39; =&gt; null,<br>        ],<br>    ];<br>    $error = false;<br><br>    if ($request-&gt;has(&#39;orderID&#39;))<br>    {<br>        /* This block is just an example of getting the SALE record<br>        you need to be already have created in your app after successful checkout */<br>        $sale = Sale::where(&#39;payment_method&#39;, &#39;card&#39;)<br>            -&gt;where(&#39;code&#39;, $request[&#39;orderID&#39;])<br>            -&gt;first();<br>        if (empty($sale)) {<br>            return redirect()-&gt;route(&#39;errors.404&#39;);<br>        }<br><br>        $sale_data[&#39;payment_json&#39;][&#39;request&#39;] = $request-&gt;all();<br>        if ($request-&gt;has(&#39;paymentid&#39;))<br>        {<br>            $ch = curl_init();<br>            curl_setopt($ch, CURLOPT_URL, &quot;https://&quot; . config(&#39;payment.ameria.subdomain&#39;) . &quot;.ameriabank.am/VPOS/api/VPOS/GetPaymentDetails&quot;);<br>            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);<br>            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([<br>                &quot;PaymentID&quot; =&gt; $request-&gt;get(&#39;paymentid&#39;),<br>                &quot;Username&quot; =&gt; config(&#39;payment.ameria.username&#39;),<br>                &quot;Password&quot; =&gt; config(&#39;payment.ameria.password&#39;),<br>            ], JSON_UNESCAPED_UNICODE));<br>            curl_setopt($ch, CURLOPT_HTTPHEADER, [&#39;Content-Type: application/json&#39;]);<br>            $response = curl_exec($ch);<br>            $curlError = curl_error($ch);<br>            curl_close($ch);<br><br>            $sale_data[&#39;payment_json&#39;][&#39;response&#39;] = $response;<br><br>            if ($curlError) {<br>                $error = true;<br>            } else {<br>                switch ($request-&gt;get(&#39;respcode&#39;)) {<br>                    case &quot;00&quot;:<br>                        break;<br>                    default:<br>                        $error = true;<br>                }<br>            }<br>        } else {<br>            $error = true;<br>        }<br><br>        $sale_data[&#39;payment_status&#39;] = $error ? &#39;failed&#39; : &#39;paid&#39;;<br><br>        // This block is just an example of updating the SALE record <br>        $sale-&gt;update($sale_data);<br><br>        if ($error) {<br>            // You can return and show some error message<br>            return redirect()-&gt;route(&#39;front.main&#39;);<br>        } else {<br>            // This is just an example of creating some PDF for report<br>            $pdf = $this-&gt;paymentService-&gt;generateSaleInvoice($sale);<br><br>            // This is just an example of emailing the report<br>            $this-&gt;paymentService-&gt;sendSaleEmail($sale, $pdf);<br><br>            return redirect()-&gt;route(&#39;front.order_success&#39;, [&#39;code&#39; =&gt; $sale-&gt;code]);<br>        }<br>    } else {<br>        return redirect()-&gt;route(&#39;errors.404&#39;);<br>    }<br>}</pre><p>You can read more about the system logic on <a href="https://servicestest.ameriabank.am/VPOS/help"><strong><em>VPOS Web API Help Page</em></strong></a>.</p><p>In the end, just in case I will put here some links that may be useful:</p><ul><li><a href="https://payments.ameriabank.am/webservice/PaymentService.svc">PaymentService Service</a></li><li><a href="https://github.com/ayvazyan10/AmeriaBankvpos">AmeriaBank VPOS Laravel Package</a></li><li><a href="https://gist.github.com/hos/4ed551472aeb6be0ab4b29170d3fd726">Response codes of Ameria Bank REST API. Maybe also can be used for ARCA response codes</a></li><li><a href="https://github.com/vahanspetrosyan/omnipay-vpos-ameriabank">Ameria Bank vPos API</a></li><li><a href="https://github.com/hos/ameria-sdk-js">The VPOS SDK for Ameria Bank</a></li></ul><p>Now let’s integrate <strong>IDram</strong>.</p><p><a href="https://idbank.am/en/business/instruments/trade-finance/v-pos-virtual-pos-terminal-0/"><strong><em>IDram V-POS (virtual POS terminal)</em></strong></a><strong><em> </em></strong>enables trading points to accept card payments online quickly and securely. V-POS terminals enable the customer to expand product or service sales by accepting payments in the Internet environment 24/7 from anywhere in the world.</p><p>At first, add some environment variables to your <strong><em>.env</em></strong> file:</p><pre>IDRAM_SECRET_KEY=&quot;**************************************&quot;<br>IDRAM_EDP_REC_ACCOUNT=&quot;*********&quot;</pre><p>Add necessary webhook routes to your <strong><em>routes/web.php</em></strong>:</p><pre>Route::match([&#39;GET&#39;, &#39;POST&#39;, &#39;PUT&#39;], &#39;/payment/idram&#39;, &#39;PaymentController@idram&#39;);</pre><p><strong>Important:</strong> Don’t forget to disable any firewalls or middlewares that could block the incoming webhook-requests to your app.</p><p>In the <strong><em>config/payment.php</em></strong> add these payment-specific configurations:</p><pre>&#39;idram&#39; =&gt; [<br>    &#39;secret_key&#39; =&gt; env(&#39;IDRAM_SECRET_KEY&#39;),<br>    &#39;edp_rec_account&#39; =&gt; env(&#39;IDRAM_EDP_REC_ACCOUNT&#39;),<br>],</pre><p>Refresh caches after modifying configuration files and routes:</p><pre>php artisan optimize</pre><p>Below is the <strong><em>app/Http/Controllers/PaymentController</em></strong> snippet which can be used to accept payment webhooks:</p><pre>public function idram(Request $request)<br>{<br>    /*<br>    $request_body = [];<br>    // for both requests<br>    $request_body[&#39;EDP_BILL_NO&#39;] = isset($_REQUEST[&#39;EDP_BILL_NO&#39;]) ? $_REQUEST[&#39;EDP_BILL_NO&#39;] : null;<br>    $request_body[&#39;EDP_REC_ACCOUNT&#39;] = isset($_REQUEST[&#39;EDP_REC_ACCOUNT&#39;]) ? $_REQUEST[&#39;EDP_REC_ACCOUNT&#39;] : null;<br>    $request_body[&#39;EDP_AMOUNT&#39;] = isset($_REQUEST[&#39;EDP_AMOUNT&#39;]) ? $_REQUEST[&#39;EDP_AMOUNT&#39;] : null;<br>    // 1. Order Authenticity<br>    $request_body[&#39;EDP_PRECHECK&#39;] = isset($_REQUEST[&#39;EDP_PRECHECK&#39;]) ? $_REQUEST[&#39;EDP_PRECHECK&#39;] : null;<br>    // 2. Payment Confirmation<br>    $request_body[&#39;EDP_PAYER_ACCOUNT&#39;] = isset($_REQUEST[&#39;EDP_PAYER_ACCOUNT&#39;]) ? $_REQUEST[&#39;EDP_PAYER_ACCOUNT&#39;] : null;<br>    $request_body[&#39;EDP_TRANS_ID&#39;] = isset($_REQUEST[&#39;EDP_TRANS_ID&#39;]) ? $_REQUEST[&#39;EDP_TRANS_ID&#39;] : null;<br>    $request_body[&#39;EDP_TRANS_DATE&#39;] = isset($_REQUEST[&#39;EDP_TRANS_DATE&#39;]) ? $_REQUEST[&#39;EDP_TRANS_DATE&#39;] : null;<br>    $request_body[&#39;EDP_CHECKSUM&#39;] = isset($_REQUEST[&#39;EDP_CHECKSUM&#39;]) ? $_REQUEST[&#39;EDP_CHECKSUM&#39;] : null;<br>    */<br>    <br>    // Idram Payment System provide it<br>    $secret = config(&#39;payment.idram.secret_key&#39;);<br>    // Idram Payment System provide it<br>    $edp_rec_account = config(&#39;payment.idram.edp_rec_account&#39;);<br><br>    // 1. Order Authenticity<br>    if(isset($_REQUEST[&#39;EDP_PRECHECK&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_BILL_NO&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_REC_ACCOUNT&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_AMOUNT&#39;]))<br>    {<br>        if($_REQUEST[&#39;EDP_PRECHECK&#39;] == &quot;YES&quot;) {<br>            if($_REQUEST[&#39;EDP_REC_ACCOUNT&#39;] == $edp_rec_account) {<br>                $bill_no = $_REQUEST[&#39;EDP_BILL_NO&#39;];<br>                // this code checks if $bill_no exists in your system orders if exists then echo OK otherwise nothing<br><br>                /* This block is just an example of getting the SALE record<br>                you need to be already have created in your app after successful checkout */<br>                $sale = Sale::where(&#39;code&#39;, $bill_no)<br>                    // -&gt;where(&#39;payment_provider&#39;, &#39;idram&#39;)<br>                    -&gt;whereIn(&#39;payment_status&#39;, [&#39;defined&#39;, &#39;failed&#39;, &#39;pending&#39;]) // pending - when already someone want to pay with idram<br>                    -&gt;first();<br><br>                if ($sale) {<br>                    $sale-&gt;update([<br>                        &#39;payment_method&#39; =&gt; &#39;idram&#39;,<br>                        &#39;payment_provider&#39; =&gt; &#39;idram&#39;,<br>                        &#39;payment_status&#39; =&gt; &#39;pending&#39;,<br>                        &#39;payment_json&#39; =&gt; $request-&gt;all(),<br>                    ]);<br><br>                    echo &quot;OK&quot;; die;<br>                }<br>            }<br>        }<br>    }<br><br>    // 2. Payment Confirmation<br>    if(isset($_REQUEST[&#39;EDP_PAYER_ACCOUNT&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_BILL_NO&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_REC_ACCOUNT&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_AMOUNT&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_TRANS_ID&#39;]) &amp;&amp;<br>        isset($_REQUEST[&#39;EDP_CHECKSUM&#39;]))<br>    {<br>        $txtToHash =<br>            $edp_rec_account . &quot;:&quot; .<br>            $_REQUEST[&#39;EDP_AMOUNT&#39;] . &quot;:&quot; .<br>            $secret . &quot;:&quot; .<br>            $_REQUEST[&#39;EDP_BILL_NO&#39;] . &quot;:&quot; .<br>            $_REQUEST[&#39;EDP_PAYER_ACCOUNT&#39;] . &quot;:&quot; .<br>            $_REQUEST[&#39;EDP_TRANS_ID&#39;] . &quot;:&quot; .<br>            $_REQUEST[&#39;EDP_TRANS_DATE&#39;];<br><br>        if(strtoupper($_REQUEST[&#39;EDP_CHECKSUM&#39;]) != strtoupper(md5($txtToHash)))<br>        {<br>            $bill_no = $_REQUEST[&#39;EDP_BILL_NO&#39;];<br>            <br>            // Write your code here to handle the payment fail<br><br>            /* This block is just an example of getting the SALE record<br>            you need to be already have created in your app after successful checkout */<br>            Sale::where([<br>                [&#39;code&#39;, &#39;=&#39;, $bill_no],<br>                [&#39;payment_status&#39;, &#39;=&#39;, &#39;pending&#39;],<br>                // [&#39;payment_provider&#39;, &#39;=&#39;, &#39;idram&#39;],<br>            ])-&gt;update([<br>                &#39;payment_method&#39; =&gt; &#39;idram&#39;,<br>                &#39;payment_provider&#39; =&gt; &#39;idram&#39;,<br>                &#39;payment_status&#39; =&gt; &#39;failed&#39;,<br>                &#39;payment_json&#39; =&gt; $request-&gt;all(),<br>            ]);<br><br>            // echo(&quot;FAIL!&quot;);<br>        }<br>        else<br>        {<br>            $bill_no = $_REQUEST[&#39;EDP_BILL_NO&#39;];<br>            // please, write your code here to handle the payment success<br><br>            /* This block is just an example of getting the SALE record<br>            you need to be already have created in your app after successful checkout */<br>            $sale = Sale::where([<br>                [&#39;code&#39;, &#39;=&#39;, $bill_no],<br>                [&#39;payment_status&#39;, &#39;=&#39;, &#39;pending&#39;],<br>                // &#39;payment_provider&#39; =&gt; &#39;idram&#39;,<br>            ])-&gt;first();<br><br>            if ($sale) {<br>                // This block is just an example of updating the SALE record<br>                $sale-&gt;update([<br>                    &#39;payment_method&#39; =&gt; &#39;idram&#39;,<br>                    &#39;payment_provider&#39; =&gt; &#39;idram&#39;,<br>                    &#39;payment_status&#39; =&gt; &#39;paid&#39;,<br>                    &#39;payment_json&#39; =&gt; $request-&gt;all(),<br>                ]);<br><br>                // This is just an example of creating some PDF for report<br>                $pdf = $this-&gt;paymentService-&gt;generateSaleInvoice($sale);<br>                <br>                // This is just an example of emailing the report<br>                $this-&gt;paymentService-&gt;sendSaleEmail($sale, $pdf);<br><br>                echo &quot;OK&quot;; die;<br>            }<br>        }<br>    }<br><br>    if (isset($_REQUEST[&#39;EDP_BILL_NO&#39;])) {<br>        $sale = Sale::where([<br>            [&#39;code&#39;, &#39;=&#39;, $_REQUEST[&#39;EDP_BILL_NO&#39;]],<br>            [&#39;payment_status&#39;, &#39;=&#39;, &#39;paid&#39;],<br>        ])-&gt;first();<br><br>        if ($sale) {<br>            return redirect()-&gt;route(&#39;front.order_success&#39;, [&#39;code&#39; =&gt; $sale-&gt;code]);<br>        } else {<br>            // Redirect to error page with reason message<br>            return redirect()-&gt;route(&#39;front.main&#39;);<br>        }<br>    }<br>}</pre><p>As you may have already noticed, in the <strong><em>idram</em></strong> method we have implemented 2 separate logics for 2 cases: <em>Order Authenticity</em> and <em>Payment Confirmation</em>, so that we can accept that two main incoming requests during the payment process. You can read more about that system logic on Idram Payment System <a href="https://www.slideshare.net/iDramAPI/idram-merchant-api-idram-payment-system-merchant-interface-description"><strong>documentation</strong></a> official document.<br>Alternatively, check for this <a href="https://mega.nz/file/79411ZKT#CvlbhEgMRCMiQgBN0Rjij_RasB1RRoijo1lKlqes_QM"><strong>link</strong></a>.</p><p>In the end, just in case I will put here some links that may be useful:</p><ul><li><a href="https://github.com/karapetyangevorg/IdramMerchantPayment">IdramMerchantPayment</a> (the old one <a href="https://github.com/gagikmartirosyan/IdramMerchantPayment">here</a>)</li><li><a href="https://github.com/ptuchik/omnipay-idram">Omnipay iDram</a></li></ul><p><strong>That’s it.</strong></p><p>If you liked this article, feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more info you can visit my website <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you !!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ba51816e6acb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Quick way to solve Git problem: Permission denied]]></title>
            <link>https://medium.com/@boolfalse/quick-way-to-solve-git-problem-permission-denied-b13051546f8f?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/b13051546f8f</guid>
            <category><![CDATA[permission-denied]]></category>
            <category><![CDATA[git]]></category>
            <category><![CDATA[gitlab]]></category>
            <category><![CDATA[gi̇thub]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Thu, 26 Oct 2023 07:55:48 GMT</pubDate>
            <atom:updated>2023-10-26T07:55:48.402Z</atom:updated>
            <content:encoded><![CDATA[<p><em>This was tested with non-root user and works for GitHub &amp; GitLab (for 2FA enabled as well).</em></p><p>You may encounter the following problem while your development:</p><pre>git@gitlab.com: Permission denied (publickey,keyboard-interactive).</pre><p>In this article I will provide a way to solve this, and not only.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/940/1*uCamVfJtJaRXJq1M7a_WTA.jpeg" /></figure><p>If you already have setup SSH key on your remote platform (such as GitHub), just skip this part.</p><blockquote><strong>Adding SSH to your Git account.</strong></blockquote><p>Run this command to generate public &amp; private key files:</p><pre># during creation of SSH files you can leave passphrase empty<br># here you can set the private-key &amp; public-key file names/paths<br>ssh-keygen -t ed25519 -C &quot;&lt;GIT_ACCOUNT_EMAIL&gt;&quot;</pre><p>Print the output of public SSH file and copy the output content.</p><pre># let&#39;s assume we have setup default &quot;id_ed25519.pub&quot;<br>cat ~/.ssh/id_ed25519.pub</pre><p>We will do this for GitHub as an instance. But practically you can do the same for the other Git platforms as well.</p><p>Login to your account and go to the <strong><em>“Add SSH key”</em></strong> section. Use the copied public-key content for adding as a new SSH key into your account.</p><p>Test the SSH authentication for GitHub:</p><pre># for GitHub<br>ssh -T git@github.com<br><br># for GitLab<br>ssh -T git@gitlab.com</pre><blockquote><strong>Fixing the issue:</strong></blockquote><p><strong><em>Local cause:<br></em></strong>Sometimes SSH authentication test fails, and you might get something like this:</p><pre>git@gitlab.com: Permission denied (publickey,keyboard-interactive).</pre><p>For avoiding this, you can follow to these steps:</p><pre># cd into &quot;.ssh&quot; directory<br>cd ~/.ssh<br><br># make sure you have private-key file octal permission as 600<br>stat -c &quot;%a %n&quot; *<br><br># if not, then you can change the permission like this<br># in our case we might have this file: &quot;id_ed25519-gitlab&quot;<br>sudo chmod 600 id_ed25519-gitlab<br><br># [FREQUENTLY USED] if the permissions are correct, start your SSH agent<br>eval `ssh-agent -s`<br># this will output something like this:<br>&quot;Agent pid 21590&quot;<br><br># [FREQUENTLY USED] now add your SSH private-key as the authorized user<br>ssh-add ~/.ssh/id_ed25519-gitlab<br># you should get something like this<br>&quot;Identity added: /home/&lt;LOCAL_USER&gt;/.ssh/id_ed25519-gitlab (&lt;ACCOUNT_EMAIL&gt;)&quot;<br><br># now test the SSH authentication again<br># you should get some success message, something like this:<br>&quot;Welcome to GitLab, @&lt;USERNAME&gt;!&quot;</pre><p><strong><em>Connection cause:<br></em></strong>Sometimes connection not working between your machine and the Git server, but you may want to push your code somewhere remotely.<br>In this kind of cases you can just add the alternative/another remote and push the project there:</p><pre>git remote rename origin upstream<br>git remote add origin git@gitlab.com:&lt;USERNAME&gt;/&lt;REPO_NAME&gt;.git<br>git push origin &lt;BRANCH&gt;</pre><p>For existing repos (with HTTPS usage) change remote origin as SSH if need:</p><pre>git remote set-url origin git@github.com:&lt;USERNAME&gt;/&lt;REPO_NAME&gt;.git</pre><p><strong>That’s it.</strong></p><p>In the end, just in case here’s some useful resources which you may use:</p><ul><li><a href="https://stackoverflow.com/questions/5181845/git-push-existing-repo-to-a-new-and-different-remote-repo-server">Git push existing repo to a new and different remote repo server?</a></li><li><a href="https://stackoverflow.com/questions/849308/how-can-i-pull-push-from-multiple-remote-locations">How can I pull/push from multiple remote locations?</a></li><li><a href="https://askubuntu.com/questions/802812/git-ssh-not-working-in-ssh-session">Git SSH not working in SSH session</a></li><li><a href="https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out">ssh: connect to host github.com port 22: Connection timed out</a></li><li><a href="https://stackoverflow.com/questions/17220325/git-ssh-is-not-working">GIT SSH is not Working</a></li></ul><p>If you liked this article, feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more info you can visit my website <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you !!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b13051546f8f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A solid way to add multi-language support to your Laravel app]]></title>
            <link>https://medium.com/@boolfalse/a-solid-way-to-add-multi-language-support-to-your-laravel-app-3c34d7e44600?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/3c34d7e44600</guid>
            <category><![CDATA[localization]]></category>
            <category><![CDATA[middleware]]></category>
            <category><![CDATA[multilanguage]]></category>
            <category><![CDATA[laravel]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Tue, 24 Oct 2023 13:01:09 GMT</pubDate>
            <atom:updated>2023-10-29T04:09:30.690Z</atom:updated>
            <content:encoded><![CDATA[<h3>Using custom middleware to intercept each request and validate the URI prefix as a current app locale.</h3><p>There are often cases when developers need to solve the same problem within different projects.<br>In the course of my many years of experience, one of those problems has been localization in Laravel projects. For various projects, I have used a method that I have prepared according to the requirements.</p><p>You might think, then why didn’t I make a separate package to solve that problem?<br>Well, of course, it would be possible to create a package, but usually in the case of different projects, there are so many differences between them, especially in the front-end part, that making a separate package for only one project becomes ineffective.</p><p>So, now I will share the <em>old-but-gold</em> way I have used in many of my Laravel projects, and I hope it may help you.</p><p>If you just want to check the ready project now, I will put here the ⭐ <a href="https://github.com/boolfalse/laravel-localization"><strong>GitHub repo</strong></a> for the project you are about to build.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*AdusCl9w5o-WOW2vZWiTTQ.jpeg" /></figure><p>For the whole lifecycle, let’s <a href="https://laravel.com/docs/installation#your-first-laravel-project">install</a> a fresh Laravel app.<br>You can integrate this method into your existing Laravel project as well.</p><pre>composer create-project laravel/laravel localization &amp;&amp; cd localization</pre><p>In the latest Laravel versions, the <strong><em>.env</em></strong> file will be created and setup automatically.</p><p>Now you have a default Laravel app installed on our machine, so you can quickly test that via the built-in opportunity:</p><pre>php artisan serve</pre><p>And you can see the result by opening <a href="http://localhost:8000"><strong><em>http://localhost:8000</em></strong></a> (by default, 8000 is the port for serving a Laravel app).</p><p>Often, Laravel apps have some routes for customers and some separated protected routes for admins/managers. For the second case, you may have to have some URI prefix only for admins/managers, for example, <em>“dashboard”</em>.<br>Let’s assume you need to develop something like described. For instance, you have</p><ul><li>3 views/pages/routes for customers: <em>“home”</em>, <em>“about”</em>, <em>“contact”</em></li><li>and an admin view/page/route: <em>“dashboard”</em></li></ul><p>Our <strong><em>routes/web.php</em></strong> would be like this:</p><pre>&lt;?php<br><br>use Illuminate\Support\Facades\Route;<br><br>Route::group([<br>    &#39;prefix&#39; =&gt; &#39;{locale?}&#39;,<br>], function () {<br>    Route::get(&#39;/&#39;, function () { return view(&#39;pages.home&#39;); })-&gt;name(&#39;home&#39;);<br>    Route::get(&#39;/about&#39;, function () { return view(&#39;pages.about&#39;); })-&gt;name(&#39;about&#39;);<br>    Route::get(&#39;/contact&#39;, function () { return view(&#39;pages.contact&#39;); })-&gt;name(&#39;contact&#39;);<br>});<br><br>Route::group([<br>    &#39;prefix&#39; =&gt; &#39;{locale?}/dashboard&#39;,<br>], function () {<br>    Route::get(&#39;/&#39;, function () { return view(&#39;pages.dashboard&#39;); })-&gt;name(&#39;dashboard&#39;);<br>});</pre><p>Set up appropriate blade views for testing.</p><ul><li>overwrite <strong><em>resources/views/welcome.blade.php</em></strong>:</li></ul><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;{{ config(&#39;localization.locale_lang.&#39; . app()-&gt;getLocale()) }}&quot;&gt;<br>&lt;head&gt;<br>    &lt;meta charset=&quot;utf-8&quot;&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;<br>    &lt;title&gt;Laravel&lt;/title&gt;<br><br>    &lt;link href=&quot;{{ asset(&#39;styles.css&#39;) }}&quot; rel=&quot;stylesheet&quot;&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br><br>&lt;nav&gt;<br>    &lt;div class=&quot;centered-text&quot;&gt;<br>        &lt;h1&gt;Laravel Localization&lt;/h1&gt;<br>        &lt;p&gt;{{ __(&#39;page.hello_world&#39;) }}&lt;/p&gt;<br>    &lt;/div&gt;<br>    &lt;ul class=&quot;drop-down closed&quot;&gt;<br>        &lt;li class=&quot;cursor-pointer&quot;&gt;<br>            &lt;p class=&quot;nav-button&quot;&gt;Change Language&lt;/p&gt;<br>        &lt;/li&gt;<br>        @foreach(config(&#39;localization.locales&#39;) as $locale)<br>            &lt;li data-locale=&quot;{{ $locale }}&quot; class=&quot;ss-change-locale&quot;&gt;<br>                &lt;a href=&quot;javascript:void(0)&quot;&gt;<br>                    {{ __(&#39;page.locales.name.&#39; . $locale) }}<br>                &lt;/a&gt;<br>            &lt;/li&gt;<br>        @endforeach<br>    &lt;/ul&gt;<br>    &lt;div class=&quot;page-content&quot;&gt;<br>        @yield(&#39;content&#39;)<br>    &lt;/div&gt;<br>&lt;/nav&gt;<br><br>&lt;script src=&quot;{{ asset(&#39;scripts.js&#39;) }}&quot;&gt;&lt;/script&gt;<br>&lt;script&gt;<br>    // SWITCH LANGUAGE<br>    var currentUri = window.location.pathname;<br>    var locales = &quot;{{ implode(&#39;|&#39;, config(&#39;localization.locales&#39;)) }}&quot;;<br>    var currentLocale = &quot;{{ app()-&gt;getLocale() }}&quot;;<br>    var elements = document.querySelectorAll(&#39;.ss-change-locale&#39;);<br>    for (let i = 0; i &lt; elements.length; i++) {<br>        elements[i].addEventListener(&#39;click&#39;, function () {<br>            let locale = this.getAttribute(&#39;data-locale&#39;);<br>            if (locale === currentLocale) {<br>                return;<br>            }<br>            let newUri = currentUri.replace(new RegExp(&#39;^\/(&#39; + locales + &#39;)&#39;), &#39;/&#39; + locale);<br>            let paramsIndex = window.location.href.indexOf(&#39;?&#39;);<br>            let params = (paramsIndex !== -1 ? window.location.href.slice(paramsIndex) : &#39;&#39;)<br>            let port = window.location.port ? &#39;:&#39; + window.location.port : &#39;&#39;;<br>            window.location.href = window.location.protocol + &#39;//&#39; + window.location.hostname + port + newUri + params;<br>        });<br>    }<br>&lt;/script&gt;<br><br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>As you can see, you are iterating over all the languages (that you have setup in our <strong><em>config/localization.php</em></strong> configuration file) and listing them with LI tags. And at the bottom, there is a JavaScript snippet that is responsible for picking up the language selected by the user and redirecting to the appropriate page with the appropriate language prefix.<br>You can easily modify the code snippet to suit your needs, as it is just a way to show how it could be used.</p><p>Then create a <strong><em>pages</em></strong> directory in the <strong><em>resources/views</em></strong> directory to have some partials.</p><ul><li><strong><em>resources/views/pages/home.blade.php</em></strong> default component for Home:</li></ul><pre>@extends(&#39;welcome&#39;)<br>@section(&#39;content&#39;)<br>    &lt;p class=&quot;centered-text&quot;&gt;{{ __(&#39;page.home&#39;) }}&lt;/p&gt;<br>    &lt;div class=&quot;centered-text&quot;&gt;<br>        &lt;a href=&quot;{{ route(&#39;about&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.about&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;contact&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.contact&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;dashboard&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.dashboard&#39;) }}&lt;/a&gt;<br>    &lt;/div&gt;<br>@endsection</pre><ul><li><strong><em>resources/views/pages/about.blade.php</em></strong> component for About:</li></ul><pre>@extends(&#39;welcome&#39;)<br>@section(&#39;content&#39;)<br>    &lt;p class=&quot;centered-text&quot;&gt;{{ __(&#39;page.about&#39;) }}&lt;/p&gt;<br>    &lt;div class=&quot;centered-text&quot;&gt;<br>        &lt;a href=&quot;{{ route(&#39;home&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.home&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;contact&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.contact&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;dashboard&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.dashboard&#39;) }}&lt;/a&gt;<br>    &lt;/div&gt;<br>@endsection</pre><ul><li><strong><em>resources/views/pages/contact.blade.php</em></strong> component for Contact:</li></ul><pre>@extends(&#39;welcome&#39;)<br>@section(&#39;content&#39;)<br>    &lt;p class=&quot;centered-text&quot;&gt;{{ __(&#39;page.contact&#39;) }}&lt;/p&gt;<br>    &lt;div class=&quot;centered-text&quot;&gt;<br>        &lt;a href=&quot;{{ route(&#39;home&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.home&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;about&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.about&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;dashboard&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.dashboard&#39;) }}&lt;/a&gt;<br>    &lt;/div&gt;<br>@endsection</pre><ul><li><strong><em>resources/views/pages/dashboard.blade.php</em></strong> one more component for Dashboard. Later, you can add some blades like this:</li></ul><pre>@extends(&#39;welcome&#39;)<br>@section(&#39;content&#39;)<br>    &lt;p class=&quot;centered-text&quot;&gt;{{ __(&#39;page.dashboard&#39;) }}&lt;/p&gt;<br>    &lt;div class=&quot;centered-text&quot;&gt;<br>        &lt;a href=&quot;{{ route(&#39;home&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.home&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;about&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.about&#39;) }}&lt;/a&gt;<br>        &lt;a href=&quot;{{ route(&#39;contact&#39;) }}&quot; class=&quot;button&quot;&gt;{{ __(&#39;page.contact&#39;) }}&lt;/a&gt;<br>    &lt;/div&gt;<br>@endsection</pre><p>For a little bit of UI, you can use some ready-made styles from <a href="https://codepen.io/dg1234uk/pen/wGyPRP">CodePen</a>.<br>I will modify those assets a bit, so you can get those <strong>styles.css</strong> and <strong>scripts.js</strong> files from the <strong><em>public</em></strong> folder <a href="https://github.com/boolfalse/laravel-localization/tree/master/public">here</a>.</p><p>At this point, as you have all the routes and views setup, let’s create a config file specifically for the localization-related stuff, create a custom middleware, register that middleware in the kernel to intercept all the web requests, and add some translations.</p><ul><li>Create <strong><em>app/Http/Middleware/Localization.php</em></strong>:</li></ul><pre>&lt;?php<br><br>namespace App\Http\Middleware;<br><br>use Closure;<br>use Illuminate\Http\Request;<br>use Illuminate\Support\Facades\App;<br>use Illuminate\Support\Facades\Redirect;<br>use Illuminate\Support\Facades\URL;<br><br>class Localization<br>{<br>    public function handle(Request $request, Closure $next)<br>    {<br>        $first_segment = $request-&gt;segment(1);<br>        if (in_array($first_segment, config(&#39;localization.locales&#39;))) {<br>            App::setLocale($first_segment);<br>            URL::defaults([&#39;locale&#39; =&gt; $first_segment]);<br><br>            return $next($request);<br>        } else {<br>            $fallback_locale = config(&#39;app.fallback_locale&#39;);<br>            $segments = $request-&gt;segments();<br>            array_unshift($segments, $fallback_locale);<br><br>            return Redirect::to(implode(&#39;/&#39;, $segments));<br>        }<br>    }<br>}</pre><p>This custom middleware, called <strong>Localization</strong>, is responsible for handling and setting the application’s locale (language) based on the first segment of the URL.</p><ol><li>It extracts the first segment from the URL. For example, <strong><em>“en”</em></strong> in <em>“example.com/</em><strong><em>en</em></strong><em>/page”</em>.</li><li>It checks if this first segment corresponds to a supported locale as defined in the configuration config(&#39;localization.locales&#39;).</li><li>If the first segment is a valid locale, it sets the application’s locale using App::setLocale($first_segment), ensuring that the rest of the application operates in that language.</li><li>It also updates the URL to include the selected locale as a default for future generated URLs using URL::defaults([&#39;locale&#39; =&gt; $first_segment]).</li><li>If the first segment is not a valid locale, it redirects the user to the same URL but with the default (fallback) locale by prepending the fallback locale to the URL segments, as defined in the <strong><em>config/app.php</em></strong> fallback_locale field. Of course, don’t forget to clear caches after each configuration change by running php artisan optimize.</li></ol><p>Shortly, this custom middleware determines the application’s current language (locale) based on the first URL segment, setting it to a valid locale or redirecting to the default locale if the segment is not recognized.</p><p>So if you want to change the default locale (language) in our app, you need to change the fallback locale in <strong><em>config/app.php</em></strong>:<br>&#39;fallback_locale&#39; =&gt; &#39;en&#39;to &#39;fallback_locale&#39; =&gt; &#39;&lt;ANOTHER_LOCALE&gt;&#39;.</p><p>Register the middleware in the <strong><em>web</em></strong> middleware group by adding the Localization class as the last array element of the <em>“web”</em> sub-array of the $middlewareGroups array:</p><pre>protected $middlewareGroups = [<br>    &#39;web&#39; =&gt; [<br>        // EXISTING ONES<br>        \App\Http\Middleware\Localization::class, // APPEND THIS HERE<br>    ],<br>    &#39;api&#39; =&gt; [<br>        // EXISTING ONES<br>    ],<br>];</pre><p>The reason we did that only for the <strong><em>web</em></strong> middleware group is that we don’t want to affect other existing routes, such as API endpoints, which could be presented in our app.</p><p>Now, to have dynamic code in our app, you need to have a configuration file where you can save all our locales (languages). Create some <strong><em>config/localization.php</em></strong> specifically for the localization-related stuff:</p><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;en&#39;,<br>        &#39;cn&#39;,<br>    ],<br>    &#39;locale_lang&#39; =&gt; [<br>        &#39;en&#39; =&gt; &#39;en_US&#39;,<br>        &#39;cn&#39; =&gt; &#39;zh_CN&#39;,<br>    ],<br>];</pre><p>As you may already notice, in our app you will have English as a default (fallback) language and Chinese as a secondary language. And you just use the <em>“locales”</em> field in our dropdown HTML, to iterate over and list all the locales.</p><p>Let’s create some translations as well, which you can use in our application.</p><ul><li>Create <strong><em>lang/en/page.php</em></strong> (for Laravel 9 and old versions, create <em>resources/lang/en/page.php</em>):</li></ul><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;code&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;EN&quot;,<br>            &#39;cn&#39; =&gt; &quot;中国人&quot;,<br>        ],<br>        &#39;name&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;English&quot;,<br>            &#39;cn&#39; =&gt; &quot;中国人&quot;,<br>        ],<br>    ],<br><br>    &#39;hello_world&#39; =&gt; &quot;Hello World&quot;,<br>    &#39;change_language&#39; =&gt; &quot;Change Language&quot;,<br><br>    &#39;home&#39; =&gt; &quot;Home&quot;,<br>    &#39;about&#39; =&gt; &quot;About&quot;,<br>    &#39;contact&#39; =&gt; &quot;Contact&quot;,<br>    &#39;dashboard&#39; =&gt; &quot;Dashboard&quot;,<br>];</pre><ul><li>Create <strong><em>lang/cn/page.php</em></strong> (for Laravel 9 and old versions, create <em>resources/lang/cn/page.php</em>).</li></ul><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;code&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;中文&quot;,<br>            &#39;cn&#39; =&gt; &quot;CN&quot;,<br>        ],<br>        &#39;name&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;English&quot;,<br>            &#39;cn&#39; =&gt; &quot;中文&quot;,<br>        ],<br>    ],<br><br>    &#39;hello_world&#39; =&gt; &quot;你好世界&quot;,<br>    &#39;change_language&#39; =&gt; &quot;更改语言&quot;,<br><br>    &#39;home&#39; =&gt; &quot;家&quot;,<br>    &#39;about&#39; =&gt; &quot;关于&quot;,<br>    &#39;contact&#39; =&gt; &quot;联系&quot;,<br>    &#39;dashboard&#39; =&gt; &quot;仪表板&quot;,<br>];</pre><p>At this point, you have all the necessary things setup to have our app working. Just last time, clear the caches and run the server:</p><pre># Make sure to always refresh all the caches after each config file changes.<br>php artisan optimize<br><br># A quick way to run and test our app.<br>php artisan serve</pre><p>You can now check out our app in the browser at <a href="http://localhost:8000"><strong><em>http://localhost:8000</em></strong></a>.</p><p>Make sure you have disabled automatic translation extensions (like the Google Translate extension as in the picture):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/776/1*SfpGlCnzI_WcjO-DSNWH4w.png" /></figure><p>That’s all for the setup.<br>Later, if you want to add some more translations, the one thing you just need to do, is add your stuff in the translation files located in the <strong><em>lang</em></strong> directory, as you have already done.</p><p>Sometimes things happen when you need to add a completely new language to our existing app that will be supported as well as other languages.<br>The good part of this article is that you have developed a way to dynamically add new locales/languages.<br>In my test case, I will add Armenian as a third language.</p><p>The only two steps you need to take to add a new language are:</p><ul><li>Add a language code in the localization-specific configuration file (<strong><em>config/localization.php</em></strong> in our case):</li></ul><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;en&#39;,<br>        &#39;cn&#39;,<br>        &#39;am&#39;, // NEW LANGUAGE<br>    ],<br>    &#39;locale_lang&#39; =&gt; [ // OPTIONAL STUFF<br>        &#39;en&#39; =&gt; &#39;en_US&#39;,<br>        &#39;cn&#39; =&gt; &#39;zh_CN&#39;,<br>        &#39;am&#39; =&gt; &#39;hy_AM&#39;, // NEW LANGUAGE<br>    ],<br>];</pre><ul><li>Add the appropriate lang file, like you did before. In my case, I will create <strong><em>lang/am/page.php</em></strong> (for Laravel 9 and old versions, create <em>resources/lang/am/page.php</em>):</li></ul><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;code&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;中文&quot;,<br>            &#39;cn&#39; =&gt; &quot;CN&quot;,<br>            &#39;am&#39; =&gt; &quot;ՀԱՅ&quot;,<br>        ],<br>        &#39;name&#39; =&gt; [<br>            &#39;en&#39; =&gt; &quot;English&quot;,<br>            &#39;cn&#39; =&gt; &quot;中文&quot;,<br>            &#39;am&#39; =&gt; &quot;Հայերեն&quot;,<br>        ],<br>    ],<br><br>    &#39;hello_world&#39; =&gt; &quot;Բարև աշխարհ&quot;,<br>    &#39;change_language&#39; =&gt; &quot;Փոխել լեզուն&quot;,<br><br>    &#39;home&#39; =&gt; &quot;Տուն&quot;,<br>    &#39;about&#39; =&gt; &quot;Մասին&quot;,<br>    &#39;contact&#39; =&gt; &quot;Կապ&quot;,<br>    &#39;dashboard&#39; =&gt; &quot;Վահանակ&quot;,<br>];</pre><p>You may optionally modify your existing lang files, as I need to do in my case, which you can notice in the GitHub repo, but it depends on your case.<br>In my case, I will make some small additions in <strong><em>***/en/page.php</em></strong> and <strong><em>***/cn/page.php</em></strong>:</p><pre>&lt;?php<br><br>return [<br>    &#39;locales&#39; =&gt; [<br>        &#39;code&#39; =&gt; [<br>            // EXISTING CODE<br>            &#39;am&#39; =&gt; &quot;ՀԱՅ&quot;, // ADDING THIS<br>        ],<br>        &#39;name&#39; =&gt; [<br>            // EXISTING CODE<br>            &#39;am&#39; =&gt; &quot;Հայերեն&quot;, // ADDING THIS<br>        ],<br>    ],<br>    // EXISTING CODE<br>];</pre><p>Finally!!! Again, don’t forget to clear the caches with the artisan command.</p><p><strong>That’s it.</strong><br>Now you have integrated a custom and solid way to have Laravel multi-language support, as well as a way to add new languages dynamically.</p><p>In the end, I will put here the <a href="https://github.com/boolfalse/laravel-localization"><strong>GitHub repo</strong></a><strong> </strong>⭐ for the project you built.</p><p>If you liked this article, feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more information, you can visit my website <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><p>Thank you!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3c34d7e44600" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Practice English tenses with examples]]></title>
            <link>https://medium.com/@boolfalse/practice-english-tenses-with-examples-c41f67129f15?source=rss-2952bff55059------2</link>
            <guid isPermaLink="false">https://medium.com/p/c41f67129f15</guid>
            <category><![CDATA[learn-english]]></category>
            <category><![CDATA[learn-english-speaking]]></category>
            <category><![CDATA[english-tense]]></category>
            <category><![CDATA[tenses]]></category>
            <dc:creator><![CDATA[Bool False]]></dc:creator>
            <pubDate>Fri, 20 Oct 2023 08:58:20 GMT</pubDate>
            <atom:updated>2023-10-20T09:01:24.131Z</atom:updated>
            <content:encoded><![CDATA[<h3>12 English tenses, 10 real-life examples for each</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e-g6wmdryCLPrQwUTIY4Zg.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@impatrickt?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Patrick Tomasso</a> on <a href="https://unsplash.com/photos/open-book-lot-Oaqk7qqNh_c?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p>Originally this was not a classic article, but this could be a good sheet to learn, use and practice English tenses. I got this in February 2023 via <a href="https://chat.openai.com/">OpenAI’s ChatGPT</a> (v3) to use and practice some English tenses in my daily life.</p><p>So below is all the answers collected from ChatGPT.</p><p>There are 12 tenses in English grammar:</p><pre>1. Simple present tense<br>2. Present continuous tense<br>3. Present perfect tense<br>4. Present perfect continuous tense<br>5. Simple past tense<br>6. Past continuous tense<br>7. Past perfect tense<br>8. Past perfect continuous tense<br>9. Simple future tense<br>10. Future continuous tense<br>11. Future perfect tense<br>12. Future perfect continuous tense</pre><p>Each of these tenses can be further classified into four aspects: simple, continuous, perfect, and perfect continuous. Together, these aspects provide a more nuanced understanding of when an action took place, how long it lasted, and whether it was completed or ongoing at a particular point in time.</p><p><strong>1. Simple present</strong> tense:</p><ul><li>I eat breakfast every morning.</li><li>She plays tennis on weekends.</li><li>He works at a bank.</li><li>They speak Spanish fluently.</li><li>The sun rises in the east.</li><li>Cats hate water.</li><li>We live in New York City.</li><li>The teacher explains the lesson.</li><li>The company produces high-quality products.</li><li>The bus arrives at 7 am.</li></ul><p><strong>2. Present continuous</strong> tense:</p><ul><li>I am currently studying for my exams.</li><li>She is playing the piano at the moment.</li><li>He is talking on the phone with his friend.</li><li>They are watching a movie right now.</li><li>We are learning a new language.</li><li>The kids are playing in the park.</li><li>She is cooking dinner for her family.</li><li>He is exercising at the gym.</li><li>The teacher is writing on the whiteboard.</li><li>The company is currently expanding its operations.</li></ul><p><strong>3. Present perfect</strong> tense:</p><ul><li>I have finished my homework.</li><li>She has traveled to many countries.</li><li>He has lived in this city for 10 years.</li><li>They have studied English for two years.</li><li>We have seen that movie before.</li><li>She has just eaten lunch.</li><li>He has already completed the project.</li><li>The company has launched a new product.</li><li>They have not yet made a decision.</li><li>We have experienced some technical difficulties.</li></ul><p><strong>4. Present perfect continuous</strong> tense:</p><ul><li>I have been studying for three hours.</li><li>She has been practicing the violin since this morning.</li><li>He has been working on the project all week.</li><li>They have been learning French for six months.</li><li>We have been waiting for the bus for 20 minutes.</li><li>She has been painting the house for two days.</li><li>He has been jogging for an hour.</li><li>The company has been investing in renewable energy for several years.</li><li>They have been discussing the issue for a while now.</li><li>We have been experiencing some technical difficulties for the past few days.</li></ul><p><strong>5. Simple past</strong> tense:</p><ul><li>I went to the store yesterday.</li><li>She studied hard for the exam last night.</li><li>He played soccer with his friends yesterday.</li><li>They visited their grandparents last weekend.</li><li>We watched a movie last night.</li><li>She cooked dinner for her family yesterday.</li><li>He finished the book yesterday.</li><li>The company launched a new product last year.</li><li>They traveled to Europe last summer.</li><li>We met our friends at the coffee shop yesterday.</li></ul><p><strong>6. Past continuous</strong> tense:</p><ul><li>I was studying when the phone rang.</li><li>She was playing the piano when we arrived.</li><li>He was working on the project when the power went out.</li><li>They were watching a movie when I called.</li><li>We were walking in the park when it started raining.</li><li>She was cooking dinner when the guests arrived.</li><li>He was jogging when he twisted his ankle.</li><li>The company was expanding its operations when the pandemic hit.</li><li>They were discussing the issue when the boss walked in.</li><li>We were driving to the beach when the car broke down.</li></ul><p><strong>7. Past perfect</strong> tense:</p><ul><li>I had finished my homework before I went to bed.</li><li>She had traveled to many countries before she settled down.</li><li>He had lived in this city for 10 years before he moved away.</li><li>They had studied English for two years before they took the proficiency test.</li><li>We had seen that movie before we went to the cinema again.</li><li>She had just eaten lunch when her friend called.</li><li>He had already completed the project before the deadline.</li><li>The company had launched a new product before the market crashed.</li><li>They had not yet made a decision before the meeting started.</li><li>We had experienced some technical difficulties before the IT team fixed the issue.</li></ul><p><strong>8. Past perfect continuous</strong> tense:</p><ul><li>I had been studying for three hours when my friend called.</li><li>She had been practicing the violin for two hours before she took a break.</li><li>He had been working on the project all week before he submitted it.</li><li>They had been learning French for six months before they traveled to France.</li><li>We had been waiting for the bus for 20 minutes before it finally arrived.</li><li>She had been painting the house for two days before she ran out of paint.</li><li>He had been jogging for an hour before he stopped to catch his breath.</li><li>The company had been investing in renewable energy for several years before it became mainstream.</li><li>They had been discussing the issue for a while before they reached a decision.</li><li>We had been experiencing some technical difficulties for the past few days before we called the IT team.</li></ul><p><strong>9. Simple future</strong> tense:</p><ul><li>I will go to the gym tomorrow.</li><li>She will attend the conference next week.</li><li>He will buy a new car next month.</li><li>They will travel to Europe next summer.</li><li>We will have a meeting with the client next Monday.</li><li>She will cook dinner for her family tonight.</li><li>He will take the exam next month.</li><li>The company will launch a new product next year.</li><li>They will celebrate their anniversary next week.</li><li>We will move to a new house next month.</li></ul><p><strong>10. Future continuous</strong> tense:</p><ul><li>I will be studying when you arrive.</li><li>She will be attending the conference at 10 am tomorrow.</li><li>He will be working on the project all day tomorrow.</li><li>They will be traveling to Europe this time next year.</li><li>We will be having a meeting with the CEO at 3 pm tomorrow.</li><li>She will be cooking dinner for her family when her guests arrive.</li><li>He will be jogging in the park at 6 am tomorrow.</li><li>The company will be expanding its operations next year.</li><li>They will be discussing the issue at the meeting next week.</li><li>We will be driving to the beach this time tomorrow.</li></ul><p><strong>11. Future perfect</strong> tense:</p><ul><li>I will have finished my work by the time you arrive.</li><li>She will have graduated by the end of the year.</li><li>He will have completed the project by the deadline.</li><li>They will have learned English for two years by the time they take the proficiency test.</li><li>We will have seen that movie by the time it leaves the theaters.</li><li>She will have cooked dinner by the time her guests arrive.</li><li>He will have been jogging for an hour by the time he reaches home.</li><li>The company will have launched a new product by the end of the year.</li><li>They will have made a decision by the time the meeting ends.</li><li>We will have experienced some technical difficulties before the IT team arrives.</li></ul><p><strong>12. Future perfect continuous</strong> tense:</p><ul><li>I will have been studying for three hours by the time you arrive.</li><li>She will have been practicing the violin for two hours by the time the concert starts.</li><li>He will have been working on the project for a week by the deadline.</li><li>They will have been learning French for six months by the time they travel to France.</li><li>We will have been waiting for the bus for 20 minutes by the time it arrives.</li><li>She will have been painting the house for two days by the time she finishes.</li><li>He will have been jogging for an hour by the time he reaches the park.</li><li>The company will have been investing in renewable energy for several years by the time it becomes mainstream.</li><li>They will have been discussing the issue for a while by the time they reach a decision.</li><li>We will have been experiencing some technical difficulties for a few days by the time the IT team arrives.</li></ul><h3>All the examples are checked and verified using online tools :)</h3><p>If you liked this article, feel free to follow me here. 😇</p><p>To explore projects working with various modern technologies, you can follow me on <a href="https://github.com/boolfalse"><strong>GitHub</strong></a>, where I actively publicize much of my work.</p><p>For more info you can visit my website <a href="https://boolfalse.com/"><strong>boolfalse.com</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c41f67129f15" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>