<?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"><channel><title><![CDATA[Kirill Inoz's Articles]]></title><description><![CDATA[Self-taught web developer sharing his experience, tips and writing with the world. I write about CSS and JavaScript, as well as about topics closely related to ]]></description><link>https://blog.kirillinoz.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1705593115095/uYpTzR7ks.png</url><title>Kirill Inoz&apos;s Articles</title><link>https://blog.kirillinoz.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 18:27:59 GMT</lastBuildDate><atom:link href="https://blog.kirillinoz.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Tutorial Hell and How to Get Out]]></title><description><![CDATA[Every beginner who wants to get into development must start somewhere. Although every beginner has a certain goal in mind like “getting a job in tech”, there are multiple options on where to start. Whether it's a bootcamp, Udemy course, university or...]]></description><link>https://blog.kirillinoz.com/tutorial-hell-and-how-to-get-out</link><guid isPermaLink="true">https://blog.kirillinoz.com/tutorial-hell-and-how-to-get-out</guid><category><![CDATA[tutorial hell]]></category><category><![CDATA[development]]></category><category><![CDATA[tech ]]></category><category><![CDATA[courses]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Mon, 30 Dec 2024 15:00:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735373841935/77d9dcc7-6f5c-42aa-8b88-239d752e8076.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every beginner who wants to get into development must start somewhere. Although every beginner has a certain goal in mind like “getting a job in tech”, there are multiple options on where to start. Whether it's a bootcamp, Udemy course, university or crash course, you won't get past watching and following tutorials. But there's a thin line between following a tutorial to learn something or watching a tutorial to stay in your comfort zone. When you are past that line, you are in <strong>tutorial hell</strong>.</p>
<h2 id="heading-what-is-tutorial-hell"><strong>What is tutorial hell?</strong></h2>
<p>Every beginner seeks new skills or answers to their questions through tutorials. That isn't a bad thing, this is great. It shows that you are eager to learn. It only becomes bad when you get stuck in that phase. Imagine you are learning a new technology through a tutorial. You learn the basics, you build a project with the instructor, but now that you are done, instead of applying your newly learned skills, you watch another tutorial.</p>
<h2 id="heading-are-you-in-tutorial-hell"><strong>Are you in tutorial hell?</strong></h2>
<p>With every completed tutorial, you get a feeling of accomplishment. And it makes sense, a completed tutorial equals to a step forward. But is it really? It's hard to realize that you are in tutorial hell. Most people realize it only when they get out, but here are some hints that should help you understand if you are stuck in tutorial hell.</p>
<p>Basically, tutorial hell is like a loop. If you can look back on your current and past learnings and recognize this pattern in your learning path, you are or were in tutorial hell:</p>
<ol>
<li><p>You complete a tutorial and learn a new skill (A).</p>
</li>
<li><p>You try building something using your newly acquired skill (A). Suddenly, you realize you are missing another skill (B).</p>
</li>
<li><p>You watch another tutorial. This time to learn the new skill (B).</p>
</li>
<li><p>You try building something using your newly acquired skill (B)… <em>and so on</em></p>
</li>
</ol>
<h2 id="heading-how-to-escape-tutorial-hell"><strong>How to escape tutorial hell?</strong></h2>
<p>As previously mentioned, people often recognize that they are in tutorial hell, only when they already got out. If you were able to recognize it ahead of time, here are some tips on how to get out.</p>
<p><strong>Practice makes the master</strong>: build something, whatever you want, anything. If you followed a tutorial on how to design a website, design another website. If you watched a tutorial on how to build a to-do app, build a scheduler. Basically, use your newly acquired skill to refine it by building something similar, but make it a bit more complicated by adding a new feature or upgrading the existing design. So, in the end, you are also learning something new by taking on that challenge.</p>
<p><strong>Use other resources</strong>: watching tutorials is great to get started, but to improve or to quickly learn something new, you should look at other resources such as documentation, blogs, or e-books. Often, especially when you're working for a company or a client, you just don't have time to watch tutorials. You need to find an explanation quickly, that's where written resources come into play, better start using those as early as possible.</p>
<p>I was myself in tutorial hell, but I realized it only after I got out. Thankfully, already very early on I started adding new features to the tutorial projects that I have built, so at some point I felt comfortable pursuing an idea that I had for a project without following any videos. I didn't know everything, of course, but I learned how to deal with issues by doing research and reading documentation. If I could do that, you can do it too!</p>
<hr />
<p>Thanks for reading my article! <strong>♥</strong> If you enjoyed it and want to see more tech-related content from me, follow me on <a target="_blank" href="https://bsky.app/profile/kirillinoz.com">BlueSky</a> and <a target="_blank" href="https://x.com/kirillinoz">X</a>. To ensure you don't miss any future articles, subscribe to my <a target="_blank" href="https://blog.kirillinoz.com/newsletter">Hashnode newsletter</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Build An Image CMS with Supabase and Lovable]]></title><description><![CDATA[In one of my previous blog posts, I demonstrated how to build a MidJourney alternative using Supabase and Hugging Face in a few minutes. This is great for personal use, but what are you going to do with these generated images? And what if you want to...]]></description><link>https://blog.kirillinoz.com/build-an-image-cms-with-supabase-and-lovable</link><guid isPermaLink="true">https://blog.kirillinoz.com/build-an-image-cms-with-supabase-and-lovable</guid><category><![CDATA[supabase]]></category><category><![CDATA[lovable]]></category><category><![CDATA[huggingface]]></category><category><![CDATA[cms]]></category><category><![CDATA[gpt]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 17 Dec 2024 17:23:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284517786/59bf97b4-96bc-49c8-acc6-54378d8994a1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In one of my previous blog posts, I demonstrated how to build a <a target="_blank" href="https://blog.kirillinoz.com/create-midjourney-alternative-with-supabase-and-hugging-face-in-minutes">MidJourney alternative using Supabase and Hugging Face</a> in a few minutes. This is great for personal use, but what are you going to do with these generated images? And what if you want to share it with your friends or customers? I decided to take things further from that article and create a more comprehensive application — a CMS-like tool for managing and generating AI-powered as well as normal images. That’s what ImageVault accomplishes. It allows users to upload, store, create and share these images with a user-friendly interface.</p>
<p>This project was powered by a combination of tools: <a target="_blank" href="https://lovable.dev/">Lovable</a> (GPT Engineer) allowed me to generate and refine code, <a target="_blank" href="https://supabase.com/">Supabase</a> gave this project the backend functionalities it needed to store and manage the images. Where Lovable struggled, I utilized Visual Studio Code and my software engineering skills to fix the issues. Spoiler, I didn’t have to use VSCode that much. I will show you the journey from the first prompt to a functioning application, as well as the challenges and lessons learned when working with such a GPT Engineer like Lovable.</p>
<h2 id="heading-laying-the-groundwork">Laying the Groundwork</h2>
<p><strong>The Importance of a Solid Plan</strong></p>
<p>When experimenting with Lovable, I’ve found that creating a detailed initial prompt is crucial. Lovable excels when it comes to implementing large and cohesive features, but often struggles with more fine-tuned, piecemeal fixes. Therefore, I would advise planning out your first prompt by for example utilizing ChatGPT to give your prompt a structure and fill any gaps it might have. Once I had my solid plan with key functionalities and some good to have additions for ImageVault, I uploaded it to Lovable. It took some time to process, but the result was mind-blowing, we had a working skeleton application in seconds.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734442968673/234361c8-0173-490d-8d45-3ba214d9e54b.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>The First Prompt and Loading Screen in Lovable</p>
</blockquote>
<p><strong>Getting Started with ImageVault</strong></p>
<p>Lovable provided me with some components such as an upload are for images, basic authentication pages, simple dashboard to display uploaded images. After seeing the initial results, I exported the code to GitHub and decided to review it in VSCode. This allowed me to become more familiar with the generated code, so I would be able to make any adjustments later on and have an overview of what minor or major changes Lovable might make down the line.</p>
<p>An interesting, unexpected touch was Lovable actually giving the tool the name ImageVault itself. I thought the name perfectly captures the concept we are after with this image storage platform, so why not to keep this sense of identity from the start, right.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734443021317/ddc20c7f-ef30-46fb-a02c-aa3eb7785b63.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>The Application Skeleton Lovable Created</p>
</blockquote>
<h2 id="heading-integrating-supabase">Integrating Supabase</h2>
<p><strong>Connecting Supabase</strong></p>
<p>As the next step, it was a good idea to integrate Supabase to handle backend operation like authentication and image storage. I started by creating a Supabase project and setting up a test user in the Authentication tab. Then, I used Lovable’s built-in “Connect to a project” functionality to seamlessly link Supabase to the application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734443620304/37ba764e-2410-438a-84bf-ca727e09dcaa.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Connect Supabase Project</p>
</blockquote>
<p><strong>Adding Authentication</strong></p>
<p>With Supabase connected, I added authentication functionality. Even though the process of creating authentication in Supabase is documented in tons of articles and videos, Lovable made it even easier than easy. I simply typed “Add authentication“. First Lovable suggests to you any SQL changes it wants to do, giving you the final decision on any major changes. Secondly it updated the sign-in and sign-up pages with suitable functionalities. After testing the test user’s credentials, I found out that it didn’t miss a single thing in this setup. It worked perfectly. Just wow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734443998265/81859dcc-369e-47ac-aef8-c2bc6b837e64.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Sign-In Page in ImageVault</p>
</blockquote>
<p><strong>Fixing the Image Feed</strong></p>
<p>While the upload functionality worked, the images didn’t display properly in the feed. Initially, I asked Lovable to fetch and display the images, but it struggled to render them correctly. I was about fix this issue manually as I thought it would take less time, but I decided to stick to Lovable and instead try to make more precise prompts. I started giving it code snippets, entering my ideas and thoughts on how to fix it and guiding it towards the solution. After some back and forth we solved the issue and the images were visible, the links were public and copyible and the images can be deleted as well. This iterative process proved the improtance of small and focused prompts when working with Lovable later on.</p>
<h2 id="heading-enhancing-the-ui">Enhancing the UI</h2>
<p>While testing the UI, I noticed some minor inconsistencies in the padding and margins, especially after adding the input and button for generating images and comparing the image grid to the rest of the UI. It was nothing major what couldn’t be fixed inside of VSCode, I decided not do it for now to show you what you get when working with Lovable in the final demo video later on in the article.</p>
<h2 id="heading-adding-hugging-face-integration">Adding Hugging Face Integration</h2>
<p>With the feed functioning, I brought back the image generation feautre from my earlier project, integrating Hugging Face into the app. I had difficulties integrating Edge Functions, after one year my blog post seemed to be a bit outdated, especially when it comes to the new Supabase UI and some functionalities. After struggling for a while, I decided to integrate the generation into the frontend. Basically we generate the image and upload it to the Supabase Storage.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [prompt, setPrompt] = useState(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> [isGenerating, setIsGenerating] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> { toast } = useToast();
<span class="hljs-keyword">const</span> queryClient = useQueryClient();

<span class="hljs-keyword">const</span> handleGenerate = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!prompt.trim()) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">try</span> {
      setIsGenerating(<span class="hljs-literal">true</span>);

      <span class="hljs-comment">// Get the current user</span>
      <span class="hljs-keyword">const</span> {
        data: { user },
      } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
      <span class="hljs-keyword">if</span> (!user) {
        toast({
          title: <span class="hljs-string">"Error"</span>,
          description: <span class="hljs-string">"You must be logged in to generate images"</span>,
          variant: <span class="hljs-string">"destructive"</span>,
        });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-comment">// Initialize HuggingFace client</span>
      <span class="hljs-keyword">const</span> hf = <span class="hljs-keyword">new</span> HfInference(<span class="hljs-keyword">import</span>.meta.env.VITE_HUGGINGFACE_ACCESS_TOKEN);

      <span class="hljs-comment">// Generate image using HuggingFace</span>
      <span class="hljs-keyword">const</span> image = <span class="hljs-keyword">await</span> hf.textToImage({
        inputs: prompt,
        model: <span class="hljs-string">"stabilityai/stable-diffusion-2"</span>,
        parameters: {
          negative_prompt: <span class="hljs-string">"blurry"</span>,
        },
      });

      <span class="hljs-comment">// Convert blob to File for upload</span>
      <span class="hljs-keyword">const</span> imageFile = <span class="hljs-keyword">new</span> File([image], <span class="hljs-string">"generated-image.png"</span>, {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"image/png"</span>,
      });

      <span class="hljs-comment">// Create a unique filename</span>
      <span class="hljs-keyword">const</span> fileName = <span class="hljs-string">`<span class="hljs-subst">${user.id}</span>/<span class="hljs-subst">${crypto.randomUUID()}</span>.png`</span>;

      <span class="hljs-comment">// Upload to Supabase Storage</span>
      <span class="hljs-keyword">const</span> { error: uploadError } = <span class="hljs-keyword">await</span> supabase.storage
        .from(<span class="hljs-string">"images"</span>)
        .upload(fileName, imageFile, {
          contentType: <span class="hljs-string">"image/png"</span>,
          upsert: <span class="hljs-literal">false</span>,
        });

      <span class="hljs-keyword">if</span> (uploadError) <span class="hljs-keyword">throw</span> uploadError;

      <span class="hljs-comment">// Create database entry</span>
      <span class="hljs-keyword">const</span> { error: imageError } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"images"</span>).insert({
        storage_path: fileName,
        title: prompt,
        user_id: user.id,
        is_ai_gen: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Set the is_ai_gen field to true</span>
      });

      <span class="hljs-keyword">if</span> (imageError) <span class="hljs-keyword">throw</span> imageError;

      <span class="hljs-comment">// Invalidate and refetch images query</span>
      <span class="hljs-keyword">await</span> queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">"images"</span>] });

      toast({
        title: <span class="hljs-string">"Success"</span>,
        description: <span class="hljs-string">"Image generated successfully"</span>,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Generation error:"</span>, error);
      toast({
        title: <span class="hljs-string">"Error"</span>,
        description: error.message || <span class="hljs-string">"Failed to generate image"</span>,
        variant: <span class="hljs-string">"destructive"</span>,
      });
    } <span class="hljs-keyword">finally</span> {
      setIsGenerating(<span class="hljs-literal">false</span>);
    }
  };
</code></pre>
<p>Lovable was able to adapt existing tables and components to the new requirements. Even though it has some great features like Edit or Revert, I noticed that I didn’t use them that much. Especially Revert was a bit disappointing because it only reverted the code but the Supabase changes to the tables stayed the same which caused me some headache in the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734445151789/4d611b2f-69a8-48dc-b285-491af38b3758.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734445183250/0106a294-0175-4dfb-b6e5-c3f2f9015095.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Edit and Revert Functionalities</p>
</blockquote>
<p>Finally there were some finishing touches, like creating a landing page, creating a full-view modal and adding a robot icon to AI-generated images, so they can be differentiated from uploaded images. Here I made the mistake of putting all of these things into one prompt thinking these were some smaller changes, so what could go wrong. That’s where the revert function came to be handy. In my attempts of fixing everything what went wrong in image generation and route handling after entering this prompt, I only broke the implementation even more. Luckily after reverting and doing some manuals changes in VSCode I was able to get it back working for the demo. But before the demo…</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734446555809/9ddca832-581f-4d8f-86a0-8bfddf42b07c.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Landing Page Lovable Created</p>
</blockquote>
<h2 id="heading-challenges-and-lessons-learned">Challenges and Lessons Learned</h2>
<p><strong>Version Restore in Lovable</strong></p>
<p>As already mentioned previously it’s a very very needed functionality in such a GPT Engineer, I don’t know how I could’ve escaped those debugging loops without manually going into the code. One things, it would be nice to have an option to revert Supabase changes as well.</p>
<p><strong>Prompting Tips</strong></p>
<p>Through this process, I learned a few valuable tips for working with Lovable:</p>
<ul>
<li><p>Start with a detailed, well-structued inital prompt</p>
</li>
<li><p>For subsequent changes, focus on one feature at a time to avoid breaking existing functionality.</p>
</li>
</ul>
<h2 id="heading-demo">Demo</h2>
<p>I present to you ImageVault - a comprehensive CMS for uploading, storing and generating AI-powered images. It includes features like user authentication, secure storage, real-time updates, shareable links, all wrapped in an average user-friendly interface. All-in-all I liked working with Lovable and I can see myself using it for quick demos or MVPs for my future projects. I’m very eager to see how it’s going to evolve and if it could replace software engineers in the future, but until then it still has some issues to overcome.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/2FsNOCAY710">https://youtu.be/2FsNOCAY710</a></div>
<p> </p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the Content Storm which celebrates <a target="_blank" href="https://supabase.com/launch-week"><strong>Supabase Launch Week 13</strong></a>. If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.kirillinoz.com/"><strong>Hashnode</strong></a>, <a target="_blank" href="https://bsky.app/profile/kirillinoz.com">BlueSky</a> and <a target="_blank" href="https://x.com/kirillinoz">X</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Multi-User Note App with Supabase Realtime]]></title><description><![CDATA[This blog post is all about creating a Multi-User Note App with Next.js and Supabase. We'll walk through structuring a dynamic database, and building cool interactive features for a smooth note-taking experience. Join us on this journey to build a sp...]]></description><link>https://blog.kirillinoz.com/multi-user-note-app-with-supabase-realtime</link><guid isPermaLink="true">https://blog.kirillinoz.com/multi-user-note-app-with-supabase-realtime</guid><category><![CDATA[notes]]></category><category><![CDATA[notesapp]]></category><category><![CDATA[supabase]]></category><category><![CDATA[realtime]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Fri, 22 Dec 2023 18:03:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735285209860/73e21aa0-45c7-4014-a4a4-50d3474f12b1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This blog post is all about creating a Multi-User Note App with Next.js and Supabase. We'll walk through structuring a dynamic database, and building cool interactive features for a smooth note-taking experience. Join us on this journey to build a space where users can create, join, and manage notes together in real time. Excited? Let's jump in and explore the magic of real-time collaboration!</p>
<h3 id="heading-setup">Setup</h3>
<p>To better differentiate and keep track of our users, let's use Supabase Auth. Luckily, Supabase has a “Next.js x Supabase” template which comes with an email and password authentication out of the box. To install it, run:</p>
<pre><code class="lang-bash">npx create-next-app -e with-supabase
</code></pre>
<p>Next up, create a new Supabase project and fill out the <code>env.local.example</code> file with the necessary information. Don't forget to rename <code>env.local.example</code> to <code>env.local</code> afterward.</p>
<p>Now let's remove all placeholder components that come with this template. The only thing left in the <code>components</code> directory should be <code>AuthButton.tsx</code>. Of course, you will also need to adjust <code>page.tsx</code> accordingly.</p>
<h3 id="heading-database">Database</h3>
<p>Let's get our database setup. We will create two tables. One table will store our rooms with its specific admin. Another table will store the notes of each room. This means once a room is deleted, we should also delete the notes. Our SQL query will look something like this:</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span>
  rooms (
    id <span class="hljs-type">uuid</span> <span class="hljs-keyword">primary key</span> <span class="hljs-keyword">default</span> uuid_generate_v4 (),
    user_email <span class="hljs-type">text</span> <span class="hljs-keyword">unique</span>
    joined_emails <span class="hljs-type">jsonb</span>
  );

<span class="hljs-comment">-- RLS</span>

<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span>
  notes (
    id <span class="hljs-type">uuid</span> <span class="hljs-keyword">primary key</span> <span class="hljs-keyword">default</span> uuid_generate_v4 (),
    room_id <span class="hljs-type">uuid</span> <span class="hljs-keyword">references</span> rooms (id) <span class="hljs-keyword">on</span> <span class="hljs-keyword">delete</span> <span class="hljs-keyword">cascade</span>,
    created_at <span class="hljs-type">timestamp</span> <span class="hljs-type">with time zone</span> <span class="hljs-keyword">default</span> <span class="hljs-built_in">current_timestamp</span>,
    <span class="hljs-type">text</span> <span class="hljs-type">text</span>
  );

<span class="hljs-comment">-- RLS</span>
</code></pre>
<p>Once we ran our query, we should activate the Realtime mode for our “notes” table. For that, head to the table and click on “Realtime off”. Once you confirm, it should switch to “Realtime on”. Now we can move on to our code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703262165399/b4cd58f7-f6d9-4765-8e4e-fdea0bbbba05.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-components">Components</h3>
<h4 id="heading-join-or-create-room">Join or Create Room</h4>
<p>Let's create a component called <code>JoinOrCreateRoom.tsx</code>. Once our users have signed up and signed in. We will display this component, allowing our users to decide if they want to create a new room or join an existing one to view, add or delete notes. If you joined a room, you can quit the room and join another one. If you created a room, you have to delete your room to join or create another one. For these actions, let's create three functions:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleCreateRoom = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> userEmail = user.email;
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"rooms"</span>)
      .insert({ user_email: userEmail });

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// Get room</span>
    <span class="hljs-keyword">const</span> { data: roomData, error: roomError } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"rooms"</span>)
      .select()
      .eq(<span class="hljs-string">"user_email"</span>, userEmail)
      .single();
    setRoom(roomData);

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (roomError) {
      <span class="hljs-built_in">console</span>.log(roomError);
      <span class="hljs-keyword">return</span>;
    }
  };
</code></pre>
<p><code>handleCreateRoom</code> adds a new object to our <code>rooms</code> table, including user's email. The <code>id</code> for the room is generated automatically. That's why we need to get that ID and set our <code>room</code> state (using <code>setRoom</code>) to the room.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [roomId, setRoomId] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

  <span class="hljs-keyword">const</span> handleJoinRoom = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"rooms"</span>)
      .select()
      .eq(<span class="hljs-string">"id"</span>, roomId)
      .single();
    <span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span>;
    setRoom(data);

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      <span class="hljs-keyword">return</span>;
    }
  };
</code></pre>
<p><code>handleJoinRoom</code> gets the <code>roomId</code> from the input and checks if such room exists. If so, we will set our <code>room</code> state (using <code>setRoom</code>) to the room.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleQuitRoom = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> userEmail = user.email;
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"rooms"</span>)
      .delete()
      .eq(<span class="hljs-string">"user_email"</span>, userEmail);
    setRoom(<span class="hljs-literal">null</span>);
    setNotes([]);

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      <span class="hljs-keyword">return</span>;
    }
  };
</code></pre>
<p>Finally, <code>handleQuitRoom</code> will delete the room only in that case if the room belongs to the user. In any case, it will always empty our notes and reset the room.</p>
<p>Here's how we will display it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> (
    &lt;div&gt;
      {user &amp;&amp;
        (!room ? (
          &lt;div&gt;
            &lt;input
              className=<span class="hljs-string">"input"</span>
              placeholder=<span class="hljs-string">"Enter RoomID"</span>
              <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
              value={roomId}
              onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setRoomId(e.target.value)}
            /&gt;
            &lt;button className=<span class="hljs-string">"btn ml-3"</span> onClick={handleJoinRoom}&gt;
              Join Room
            &lt;/button&gt;
            &lt;button className=<span class="hljs-string">"btn ml-3"</span> onClick={handleCreateRoom}&gt;
              Create Room
            &lt;/button&gt;
          &lt;/div&gt;
        ) : (
          &lt;div className=<span class="hljs-string">"flex items-center"</span>&gt;
            {room.user_email === user.email ? (
              &lt;button className=<span class="hljs-string">"btn"</span> onClick={handleQuitRoom}&gt;
                Delete Room
              &lt;/button&gt;
            ) : (
              &lt;button className=<span class="hljs-string">"btn"</span> onClick={handleQuitRoom}&gt;
                Quit Room
              &lt;/button&gt;
            )}
            &lt;p className=<span class="hljs-string">"ml-3"</span>&gt;
              RoomID: &lt;span className=<span class="hljs-string">"ml-3"</span>&gt;{room.id}&lt;/span&gt;
            &lt;/p&gt;
          &lt;/div&gt;
        ))}
    &lt;/div&gt;
  );
</code></pre>
<p>In the end, here are the props we will pass into that component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> Props = {
  user: User | <span class="hljs-literal">null</span>;
  room: <span class="hljs-built_in">any</span> | <span class="hljs-literal">null</span>;
  setRoom: React.Dispatch&lt;React.SetStateAction&lt;<span class="hljs-built_in">any</span> | <span class="hljs-literal">null</span>&gt;&gt;;
  setNotes: React.Dispatch&lt;React.SetStateAction&lt;Note[]&gt;&gt;;
};
</code></pre>
<h4 id="heading-noteblock">NoteBlock</h4>
<p>Not much to say to this component, besides that it's used for displaying and deleting out notes. Here how it looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/utils/supabase/client"</span>;

<span class="hljs-keyword">interface</span> Props {
  id: <span class="hljs-built_in">number</span>;
  text: <span class="hljs-built_in">string</span>;
  className?: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">const</span> NoteBlock: React.FC&lt;Props&gt; = <span class="hljs-function">(<span class="hljs-params">{ id, text, className }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [isDeleting, setIsDeleting] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> supabase = createClient();

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setIsDeleting(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"notes"</span>).delete().eq(<span class="hljs-string">"id"</span>, id);
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(error.message);
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error deleting note:"</span>, error);
    } <span class="hljs-keyword">finally</span> {
      setIsDeleting(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;li
      className={<span class="hljs-string">`flex justify-between border border-white px-6 py-3 rounded-lg <span class="hljs-subst">${className}</span>`</span>}
    &gt;
      &lt;p className=<span class="hljs-string">"w-full text-white border-r-2 border-white pr-6 mr-6"</span>&gt;
        {text}
      &lt;/p&gt;
      &lt;button className=<span class="hljs-string">"btn"</span> onClick={handleDelete} disabled={isDeleting}&gt;
        Delete
      &lt;/button&gt;
    &lt;/li&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> NoteBlock;
</code></pre>
<h3 id="heading-page">Page</h3>
<p>Finally, let's put our page together.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [user, setUser] = useState&lt;User | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> [room, setRoom] = useState&lt;<span class="hljs-built_in">any</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> [notes, setNotes] = useState&lt;Note[]&gt;([]);

<span class="hljs-keyword">const</span> [note, setNote] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
</code></pre>
<p>We will use the following states to track our user, room and notes data. As well, as the input for the note.</p>
<p>We will have two <code>useEffects</code> one will be used to get the room data of the admin if he decides to leave the page. Another one will be used to set up our Realtime connection.</p>
<pre><code class="lang-typescript">useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> getUser = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> userData = <span class="hljs-keyword">await</span> supabase.auth.getUser();
      setUser(userData.data.user);
      <span class="hljs-keyword">if</span> (!userData.data.user) <span class="hljs-keyword">return</span>;

      <span class="hljs-keyword">const</span> user = userData.data.user;

      <span class="hljs-comment">// Get room</span>

      <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">"rooms"</span>)
        .select()
        .eq(<span class="hljs-string">"user_email"</span>, user.email)
        .single();
      setRoom(data);
      getNotes();

      <span class="hljs-comment">//Check</span>
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
        <span class="hljs-keyword">return</span>;
      }
    };

    getUser();
  }, []);
</code></pre>
<pre><code class="lang-typescript">useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (room) {
      getNotes();
      supabase
        .channel(room.id)
        .on(
          <span class="hljs-string">"postgres_changes"</span>,
          {
            event: <span class="hljs-string">"*"</span>,
            schema: <span class="hljs-string">"public"</span>,
            table: <span class="hljs-string">"notes"</span>,
          },
          <span class="hljs-function">() =&gt;</span> {
            getNotes();
          }
        )
        .subscribe();
    }
  }, [room]);
</code></pre>
<p>First, we create a channel. As the identifier, we will use the ID of the room. Next, once we get an update in our “notes” table, whether it's INSERT, DELETE or UPDATE (that's why we use *) we will update our notes.</p>
<p>To update our notes, we use <code>getNotes</code> method:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getNotes = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!user || !room) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"notes"</span>)
      .select()
      .eq(<span class="hljs-string">"room_id"</span>, room.id);
    <span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span>;
    setNotes(data);

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      <span class="hljs-keyword">return</span>;
    }
  };
</code></pre>
<p>To save a note, we use <code>saveNote</code> method:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> saveNote = <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!user || !room) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"notes"</span>)
      .insert({ room_id: room.id, text: note });

    <span class="hljs-comment">//Check</span>
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      <span class="hljs-keyword">return</span>;
    }

    setNote(<span class="hljs-string">""</span>);
    getNotes();
  };
</code></pre>
<p>In the end, this is how we display our page:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex-1 w-full flex flex-col gap-20 items-center"</span>&gt;
      &lt;nav className=<span class="hljs-string">"w-full flex justify-center border-b border-b-foreground/10 h-16"</span>&gt;
        &lt;div className=<span class="hljs-string">"w-full max-w-4xl flex justify-between items-center p-3 text-sm"</span>&gt;
          &lt;JoinOrCreateRoom
            user={user}
            room={room}
            setRoom={setRoom}
            setNotes={setNotes}
          /&gt;
          {supabase &amp;&amp; &lt;AuthButton user={user} /&gt;}
        &lt;/div&gt;
      &lt;/nav&gt;

      &lt;div className=<span class="hljs-string">"animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3"</span>&gt;
        &lt;main className=<span class="hljs-string">"flex-1 flex flex-col gap-6"</span>&gt;
          {user &amp;&amp; room &amp;&amp; (
            &lt;div className=<span class="hljs-string">"flex flex-col gap-6"</span>&gt;
              &lt;div&gt;
                &lt;input
                  className=<span class="hljs-string">"input mr-3"</span>
                  onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setNote(e.target.value)}
                  value={note}
                  <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
                  placeholder=<span class="hljs-string">"Enter Note"</span>
                /&gt;
                &lt;button className=<span class="hljs-string">"btn"</span> onClick={saveNote()}&gt;
                  Save Note
                &lt;/button&gt;
              &lt;/div&gt;
              &lt;div className=<span class="hljs-string">"mt-6"</span>&gt;
                &lt;h1 className=<span class="hljs-string">"text-2xl font-bold"</span>&gt;Notes&lt;/h1&gt;

                &lt;ul className=<span class="hljs-string">"flex flex-col gap-2 mt-3"</span>&gt;
                  {notes.map(<span class="hljs-function">(<span class="hljs-params">note, index</span>) =&gt;</span> (
                    &lt;NoteBlock
                      className=<span class="hljs-string">"mt-3"</span>
                      key={index}
                      id={note.id}
                      text={note.text}
                    /&gt;
                  ))}
                &lt;/ul&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          )}
        &lt;/main&gt;
      &lt;/div&gt;

      &lt;footer className=<span class="hljs-string">"w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs"</span>&gt;
        &lt;p&gt;
          Powered by{<span class="hljs-string">" "</span>}
          &lt;a
            href=<span class="hljs-string">"https://supabase.com/?utm_source=create-next-app&amp;utm_medium=template&amp;utm_term=nextjs"</span>
            target=<span class="hljs-string">"_blank"</span>
            className=<span class="hljs-string">"font-bold hover:underline"</span>
            rel=<span class="hljs-string">"noreferrer"</span>
          &gt;
            Supabase
          &lt;/a&gt;
        &lt;/p&gt;
      &lt;/footer&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-demo">Demo</h3>
<p>We did it! Here is the demo of the final product. Enjoy! <sup>🎉</sup></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/NtcPciG9978">https://youtu.be/NtcPciG9978</a></div>
<p> </p>
<hr />
<p>Thanks for reading! ❤️ If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com/"><strong>Hashnode</strong></a> and <a target="_blank" href="https://twitter.com/kirillinoz"><strong>Twitter</strong></a>!</p>
]]></content:encoded></item><item><title><![CDATA[Create Midjourney Alternative with Supabase and Hugging Face in Minutes]]></title><description><![CDATA[There is no doubt that Midjourney is currently the leading tool for AI image generation out there. However, people who don't generate AI images daily may find the cost of their subscription a bit too much. So you can seek alternatives, but that's not...]]></description><link>https://blog.kirillinoz.com/create-midjourney-alternative-with-supabase-and-hugging-face-in-minutes</link><guid isPermaLink="true">https://blog.kirillinoz.com/create-midjourney-alternative-with-supabase-and-hugging-face-in-minutes</guid><category><![CDATA[supabase]]></category><category><![CDATA[midjourney]]></category><category><![CDATA[AI]]></category><category><![CDATA[huggingface]]></category><category><![CDATA[images]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 10 Oct 2023 15:00:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284488838/a0e559d1-44d9-4108-a909-f74d1f7cf6e8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is no doubt that Midjourney is currently the leading tool for AI image generation out there. However, people who don't generate AI images daily may find the cost of their subscription a bit too much. So you can seek alternatives, but that's not what real developers do. We build our very own alternatives! Luckily, using Supabase Storage, Supabase Edge Functions and Hugging Face we can build an alternative to Midjourney in minutes. Let me show you how.</p>
<h1 id="heading-setup">Setup</h1>
<p>1) Open your Supabase project dashboard or <a target="_blank" href="https://supabase.com/dashboard/projects">create a new project</a>.</p>
<p>2) <a target="_blank" href="https://supabase.com/dashboard/project/_/storage/buckets">Create a new bucket</a> called <code>images</code>.</p>
<p>3) Create a new database table called <code>image_generation</code>. You can make use of Supabase's integrated AI assistant by passing the prompt: “New table called image generation with id uuid, input text”. This is cool to get the proper SQL structure in place, you will probably need to adjust some labels and attributes, but that's what my SQL query looked like in the end.</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span>
  image_generation (
    id <span class="hljs-type">uuid</span> <span class="hljs-keyword">primary key</span> <span class="hljs-keyword">default</span> gen_random_uuid (),
    <span class="hljs-keyword">input</span> <span class="hljs-type">text</span>,
  );
</code></pre>
<p>4) Set up a directory for all of your future edge functions under <code>/supabase</code></p>
<p>5) Install <a target="_blank" href="https://github.com/supabase/cli">Supabase CLI</a> if you haven't done it yet</p>
<p>6) Create a new folder under <code>/supabase/huggingface-image-generation</code></p>
<p>7) Inside that folder, create <code>index.ts</code> (this is where our code is going) and generate TypeScript types from the remote database using the following command (replace <code>your-project-ref</code> with your project reference).</p>
<pre><code class="lang-bash">supabase gen types typescript --project-id=your-project-ref --schema=storage,public &gt; ./types.ts
</code></pre>
<h1 id="heading-hugging-face">Hugging Face</h1>
<p>Sign up on Hugging Face if you haven't done it yet. Generate a new Access Token under <code>Settings/Access Token</code>. Under <code>Edge Function/Manage Secrets</code> on Supabase add a new secret <code>HUGGINGFACE_ACCESS_TOKEN</code> and paste your secret there. As you can see, Supabase has already taken care of other secrets we will need in our application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696929241815/0f944c5c-0694-429c-a13c-044760c0ff1d.png" alt="Secrets Management" class="image--center mx-auto" /></p>
<h1 id="heading-code-breakdown">Code Breakdown</h1>
<p>First, we import all Deno modules we will need in our application, followed up by declaring our <code>hf</code> constant we will use to access Hugging Face models later.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/std@0.168.0/http/server.ts'</span>
<span class="hljs-keyword">import</span> { HfInference } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@huggingface/inference@2.3.2'</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2.7.1'</span>
<span class="hljs-keyword">import</span> { Database } <span class="hljs-keyword">from</span> <span class="hljs-string">'./types.ts'</span>

<span class="hljs-keyword">const</span> hf = <span class="hljs-keyword">new</span> HfInference(Deno.env.get(<span class="hljs-string">'HUGGINGFACE_ACCESS_TOKEN'</span>))
</code></pre>
<p>Next, we declare the interface of our webhook payload.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> inputRecord = Database[<span class="hljs-string">'public'</span>][<span class="hljs-string">'Tables'</span>][<span class="hljs-string">'image_generation'</span>][<span class="hljs-string">'Row'</span>]
<span class="hljs-keyword">interface</span> WebhookPayload {
  <span class="hljs-keyword">type</span>: <span class="hljs-string">'INSERT'</span> | <span class="hljs-string">'UPDATE'</span> | <span class="hljs-string">'DELETE'</span>
  table: <span class="hljs-built_in">string</span>
  record: inputRecord
  schema: <span class="hljs-string">'public'</span>
  old_record: <span class="hljs-literal">null</span> | inputRecord
}
</code></pre>
<p>Now let's get the input from our payload and set up the Supabase Client to interact with our database.</p>
<pre><code class="lang-typescript">serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">const</span> payload: WebhookPayload = <span class="hljs-keyword">await</span> req.json()
  <span class="hljs-keyword">const</span> inputRecord = payload.record
  <span class="hljs-keyword">const</span> supabaseAdminClient = createClient&lt;Database&gt;(
    Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>) ?? <span class="hljs-string">''</span>,
    Deno.env.get(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>) ?? <span class="hljs-string">''</span>
  )

<span class="hljs-comment">/* ... */</span>
</code></pre>
<p>Let's generate the image using the model <code>stabilityai/stable-diffusion-2</code>. Feel free to try out other models later on, Hugging Face has a lot of them.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* ... */</span>

  <span class="hljs-keyword">const</span> imgDesc = <span class="hljs-keyword">await</span> hf.textToImage({
    inputs: inputRecord.input,
    model: <span class="hljs-string">'stabilityai/stable-diffusion-2'</span>,
    parameters: {
        negative_prompt: <span class="hljs-string">'blurry'</span>,
    }
  })

<span class="hljs-comment">/* ... */</span>
</code></pre>
<p>In the end, we upload the image to Supabase Storage together with <code>inputRecord.id</code>, so we will have a reference to the input. Finally, we return a response. Our code looks in the end like this.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/std@0.168.0/http/server.ts'</span>
<span class="hljs-keyword">import</span> { HfInference } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@huggingface/inference@2.3.2'</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2.7.1'</span>
<span class="hljs-keyword">import</span> { Database } <span class="hljs-keyword">from</span> <span class="hljs-string">'./types.ts'</span>

<span class="hljs-keyword">const</span> hf = <span class="hljs-keyword">new</span> HfInference(Deno.env.get(<span class="hljs-string">'HUGGINGFACE_ACCESS_TOKEN'</span>))

<span class="hljs-comment">// Input WebHook</span>

<span class="hljs-keyword">type</span> inputRecord = Database[<span class="hljs-string">'public'</span>][<span class="hljs-string">'Tables'</span>][<span class="hljs-string">'image_generation'</span>][<span class="hljs-string">'Row'</span>]
<span class="hljs-keyword">interface</span> WebhookPayload {
  <span class="hljs-keyword">type</span>: <span class="hljs-string">'INSERT'</span> | <span class="hljs-string">'UPDATE'</span> | <span class="hljs-string">'DELETE'</span>
  table: <span class="hljs-built_in">string</span>
  record: inputRecord
  schema: <span class="hljs-string">'public'</span>
  old_record: <span class="hljs-literal">null</span> | inputRecord
}

<span class="hljs-comment">// Start the server</span>

serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">const</span> payload: WebhookPayload = <span class="hljs-keyword">await</span> req.json()
  <span class="hljs-keyword">const</span> inputRecord = payload.record
  <span class="hljs-keyword">const</span> supabaseAdminClient = createClient&lt;Database&gt;(
    Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>) ?? <span class="hljs-string">''</span>,
    Deno.env.get(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>) ?? <span class="hljs-string">''</span>
  )

  <span class="hljs-comment">// Generate the image using Hugging Face's API</span>

  <span class="hljs-keyword">const</span> imgDesc = <span class="hljs-keyword">await</span> hf.textToImage({
    inputs: inputRecord.input,
    model: <span class="hljs-string">'stabilityai/stable-diffusion-2'</span>,
    parameters: {
        negative_prompt: <span class="hljs-string">'blurry'</span>,
    }
  })

  <span class="hljs-comment">// Upload the image to Supabase Storage</span>

  <span class="hljs-keyword">const</span> {data, error} = <span class="hljs-keyword">await</span> supabaseAdminClient
    .storage
    .from(<span class="hljs-string">'images'</span>)
    .upload(inputRecord.id, imgDesc, {
      cacheControl: <span class="hljs-string">'3600'</span>,
      upsert: <span class="hljs-literal">true</span>
    })

  <span class="hljs-keyword">if</span> (error) <span class="hljs-built_in">console</span>.log(error)
  <span class="hljs-keyword">if</span> (data) <span class="hljs-built_in">console</span>.log(data)

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>)
})
</code></pre>
<h1 id="heading-deployment">Deployment</h1>
<p>To see our edge function in action, we have to deploy it to Supabase. For that, we run the following command.</p>
<pre><code class="lang-bash">supabase <span class="hljs-built_in">functions</span> deploy huggingface-image-generator
</code></pre>
<p>Last but not least, we have to set up the webhook we referenced in our code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696930979991/bda932a8-5a88-4c44-a938-54c39d7a0777.png" alt="Webhook Setup" class="image--center mx-auto" /></p>
<h1 id="heading-demo">Demo</h1>
<p>Now that it's done. Here's the demo of how it works. Sorry for the quality, I recorded it on a toaster.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=NOq2hRtWB-A">https://www.youtube.com/watch?v=NOq2hRtWB-A</a></div>
<p> </p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the AI Content Storm by <a target="_blank" href="https://supabase.com/">Supabase</a>. If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com/"><strong>Hashnode</strong></a> and <a target="_blank" href="https://twitter.com/kirillinoz"><strong>Twitter</strong></a>!</p>
]]></content:encoded></item><item><title><![CDATA[Build an AI Tool with Supabase Edge Functions]]></title><description><![CDATA[Artificial intelligence (AI) became very trendy in the last half a year. With the introduction of ChatGPT, OpenAI took the spotlight in the AI space. This wonders nobody, as apart from offering this useful tool to normal folks, OpenAI offers many AI ...]]></description><link>https://blog.kirillinoz.com/build-an-ai-tool-with-supabase-edge-functions</link><guid isPermaLink="true">https://blog.kirillinoz.com/build-an-ai-tool-with-supabase-edge-functions</guid><category><![CDATA[supabase]]></category><category><![CDATA[AI]]></category><category><![CDATA[Deno]]></category><category><![CDATA[Edge-Functions]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 25 Apr 2023 15:30:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735285242529/3299d35b-683c-421c-b1a7-a0393cbe8684.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Artificial intelligence (AI) became very trendy in the last half a year. With the introduction of ChatGPT, OpenAI took the spotlight in the AI space. This wonders nobody, as apart from offering this useful tool to normal folks, OpenAI offers many AI models to developers through their API. This means developers can utilize them to build any AI tool imaginable without needing to train their own AI model.</p>
<p>The introduction of self-hosted Deno Functions during the <a target="_blank" href="https://supabase.com/launch-week">Supabase Launch Week 7</a> got me thinking of how easy it now became to build that kind of AI application. In this article, we will take a look at how to host your own Edge Functions and build an endpoint to communicate with GPT-3 or GPT-4 using this new Supabase feature and some OpenAI magic.</p>
<h3 id="heading-setup">Setup</h3>
<ol>
<li><p>Sign up for a <a target="_blank" href="http://Fly.io">fly.io</a> account and install <a target="_blank" href="https://fly.io/docs/hands-on/install-flyctl/">flyctl</a>.</p>
<pre><code class="lang-bash"> brew install flyctl
</code></pre>
</li>
<li><p>Clone the <a target="_blank" href="https://github.com/supabase/self-hosted-edge-functions-demo">demo repository</a> to your machine.</p>
<pre><code class="lang-bash"> git <span class="hljs-built_in">clone</span> https://github.com/supabase/self-hosted-edge-functions-demo.git
</code></pre>
</li>
<li><p>Sign in to your <a target="_blank" href="http://Fly.io">fly.io</a> account.</p>
<pre><code class="lang-bash"> flyctl auth login
</code></pre>
</li>
</ol>
<h3 id="heading-functionality">Functionality</h3>
<p>Now we are all set to add functionality to our application. If you have existing Edge Functions, you can drop them into the <code>./functions</code> folder. If not, no worries, we will go through the process of writing such a function in the next part.</p>
<p>Our function will consist of 3 steps:</p>
<ol>
<li><p>Receiving a text</p>
</li>
<li><p>Prompting it to OpenAI</p>
</li>
<li><p>Returning a summary of the text</p>
</li>
</ol>
<p>Let's create a new file <code>./functions/summarize/index.ts</code>, where we will handle requests to our endpoint. Our endpoint results out of the combination of our project name and this path (in my case <code>https://supa-summarizer-demo.fly.dev/summarize</code>).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> reqPayload {
  text: <span class="hljs-built_in">string</span>;
  model: <span class="hljs-string">"gpt-3.5-turbo"</span> | <span class="hljs-string">"gpt-4"</span>;
}
</code></pre>
<p>First, we declare an interface for our payload, which is the text (<code>text</code>) and the GPT version we want to use (<code>model</code>).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { OpenAI } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/x/openai/mod.ts"</span>;

<span class="hljs-comment">/* ... */</span>

<span class="hljs-keyword">const</span> openAI = <span class="hljs-keyword">new</span> OpenAI(<span class="hljs-string">"OPENAI_API_KEY"</span>)
</code></pre>
<p>Next, we import the OpenAI library, which makes it much more convenient to interact with the API. As you can see from the import, Supabase Edge Functions utilize Deno which caches packages directly, so you don't need to install huge NPM packages.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/std@0.131.0/http/server.ts"</span>

<span class="hljs-comment">/* ... */</span>

serve(
  <span class="hljs-keyword">async</span> (req: Request) =&gt; {
    <span class="hljs-keyword">const</span> { text, model }: reqPayload = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`Summarize the following text using maximum of 300 characters: <span class="hljs-subst">${text}</span>`</span>;

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> openAI.createChatCompletion({
      model,
      messages: [{ role: <span class="hljs-string">"user"</span>, content: prompt }],
    });

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(response), {
      headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
                Connection: <span class="hljs-string">"keep-alive"</span> },
    });
  },
  { port: <span class="hljs-number">9005</span> }
);
</code></pre>
<p>Here we are utilizing the <code>serve()</code> function to handle our request. First, we destructure our request to get the <code>text</code> which we pass into our prompt. Then we pass our model and prompt into the <code>createChatCompletion()</code> function from the OpenAI library. Last but not least, we return the response, which will include a summary of our text.</p>
<p>Now the last thing to do is to launch our application. Use the command below in your terminal. You will be asked a bunch of questions, we will answer yes to copying our configuration. We will choose the name of our project, in my case it's <code>supa-summarizer-demo</code>. We don't need any databases for this project, and we want to launch it now.</p>
<pre><code class="lang-bash">fly launch
</code></pre>
<p>That's it! The application is ready to go. Now let's try it out with <a target="_blank" href="https://www.postman.com/">Postman</a>.</p>
<h3 id="heading-summarize">Summarize</h3>
<p>We will be using an excerpt from a New York Times article called <a target="_blank" href="https://www.nytimes.com/2023/04/22/opinion/jobs-ai-chatgpt.html">“<strong>It’s Not the End of Work. It’s the End of Boring Work.</strong></a>” about AI, a very suitable topic for our application. (You can try summarizing this article if you want, haha.)</p>
<blockquote>
<p>Despite all the dazzling digital advances, the trillions of dollars spent on computer technology have done almost nothing to make the world a more productive place. The economist Robert Solow, who identified this problem, called it the productivity paradox. In 1987, a decade into the computer revolution, he observed that productivity growth had actually slowed down. You can see the computer age everywhere, he wrote, but in the productivity statistics.</p>
</blockquote>
<p>We pass it into our request as <code>text</code> and we select <em>gpt-3.5-turbo</em> as our <code>model</code> because the fragment is very short, otherwise we could have used <em>gpt-4</em> for better results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682402529066/65dfe90a-363f-4443-a8ae-5522fcf596d1.png" alt class="image--center mx-auto" /></p>
<p>In return, we get the following summary.</p>
<blockquote>
<p>The trillions of dollars spent on computer technology have not translated into increased productivity, as noted by economist Robert Solow in 1987. This is known as the productivity paradox, where the computer age is evident everywhere except in productivity statistics.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682402601071/a88b0820-aaa6-4a27-a02c-8a5c185d3f98.png" alt class="image--center mx-auto" /></p>
<p>That's it! Now you can easily use GPT-4 without paying for the ChatGPT Plus subscription instead you pay for what you use. I will leave the app running until the end of April and cap it at $5. Feel free to use it if you want to try it out, but I encourage you to build your own implementation.</p>
<p>If you want to learn more about Supabase Edge Functions, check out this <a target="_blank" href="https://supabase.com/blog/edge-runtime-self-hosted-deno-functions">blog post</a> written by Supabase Team.</p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the Content Storm which celebrates <a target="_blank" href="https://supabase.com/launch-week">Supabase Launch Week 7</a>. If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com/"><strong>Hashnode</strong></a> and <a target="_blank" href="https://twitter.com/kirillinoz"><strong>Twitter</strong></a>!</p>
]]></content:encoded></item><item><title><![CDATA[Opportunity Meets Preparation | Dev Retro 2022]]></title><description><![CDATA[Do you know how they say success is where opportunity meets preparation? Well, I believe 2022 was the year when my preparations for the past two years finally met that opportunity. Actually, many opportunities. To understand where this year took me, ...]]></description><link>https://blog.kirillinoz.com/opportunity-meets-preparation</link><guid isPermaLink="true">https://blog.kirillinoz.com/opportunity-meets-preparation</guid><category><![CDATA[#DevRetro2022]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Technical writing ]]></category><category><![CDATA[hackathon]]></category><category><![CDATA[conference]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 20 Dec 2022 07:30:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284423161/e7b10e48-a54a-42d0-9d5d-e11f2ffc06d5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Do you know how they say success is where opportunity meets preparation? Well, I believe 2022 was the year when my preparations for the past two years finally met that opportunity. Actually, many opportunities. To understand where this year took me, we have to look back at where it all started.</p>
<h3 id="heading-main-events-before-2022">Main Events before 2022</h3>
<p>My web development journey started, as for most newcomers into tech, during the first lockdown of 2020. Ah, what a crazy time it was.</p>
<blockquote>
<p>“Why not make it even crazier and learn JavaScript, HTML, and CSS?” — I thought</p>
</blockquote>
<p>Just kidding, it was the best decision of mine, together with joining the Tech Twitter community at the end of 2020. Not only that, but at the end of January 2021, I decided to start blogging on DevTo and Hashnode to share things that I couldn't express in under 280 characters. At that time, my main goals were:</p>
<ul>
<li><p>Growing my Twitter audience</p>
</li>
<li><p>Learning and building cool stuff</p>
</li>
<li><p>Blogging about that cool stuff</p>
</li>
</ul>
<p>So, that was it until the end of 2021 when I started learning Web3, first with help of <strong>Buildspace</strong> and then <strong>LearnWeb3DAO</strong>. This leads us to April 2022 when puzzle pieces started to come together.</p>
<h3 id="heading-ethamsterdam">EthAmsterdam</h3>
<p>I found out about EthAmsterdam through one of <a class="user-mention" href="https://hashnode.com/@FrancescoCiulla">Francesco Ciulla</a>'s tweets.</p>
<p>It looked like an incredible opportunity to challenge my existing web development skills together with my newly acquired Web3 knowledge. So, I applied, and it didn't take long until I got the confirmation that I was accepted to participate in my first-ever hackathon.</p>
<p>Finding a team wasn't difficult. I found out later that these events have a big demand for front-end developers. I teamed up with three guys from Cardiff, two of which were experienced in blockchain development and the other one in the energy sector, while I took the responsibility for building the front-end for our application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671439693337/igCpyIMZJ.png" alt class="image--center mx-auto" /></p>
<p>The project that we have been working on is called “Flexergy”. It's about providing electricity to homes in demand of it. Instead of taking the electricity from big whales such as power plants and paying big bucks, the network provider buys the energy from locals with solar panels or batteries. Because the system is completely decentralized, the locals who provide energy can set the price themselves.</p>
<p>In the end, we got third place (tied) in the sponsored challenge by Mina, and we won some prize money from the Filecoin prize pool.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671439700487/m4Ar1JkrM.png" alt class="image--center mx-auto" /></p>
<p>Overall, it was a great experience and I learned a lot more about blockchain development and explored the beautiful city of Amsterdam.</p>
<h3 id="heading-first-tech-job">First Tech Job</h3>
<p>In June, I started my first part-time tech job as a working student at a legal online service firm called Juris. In the 4 months that I worked there, I experienced hands-on how it is to work at an office and improved both my soft and hard skills. I got very comfortable with React, as before I mostly used Vue for my side projects. Through code reviews and reading code, someone else wrote, my code style drastically improved. Sadly, it didn't last long, as I had to move in October because of my studies.</p>
<h3 id="heading-return-to-blogging">Return to Blogging</h3>
<p>If you scroll through my blog posts, you will see a massive gap between September 2021 and August 2022. This is because I stopped blogging for a while. Why?</p>
<p>At first, it was the beginning of my studies, but after it became more of a writer's block. I couldn't come up with ideas for blog posts I thought were good enough.</p>
<p>Finally, in August, I set a goal for myself to take on writing again in September. I failed... in a good way. I came back to writing already in August because of the Hashnode Writeathon. If you haven't heard about it, it's a challenge to incentivize bloggers on Hashnode to write 4 posts in a month, 1 post per week.</p>
<p>I completed the challenge and as you can see, it helped me overcome my writer's block and that's why I'm still blogging up to date. Even though I haven't won any prizes, I received an opportunity that I will mention later in this article.</p>
<h3 id="heading-first-tech-conference">First Tech Conference</h3>
<p>September was filled with activities. I went to a JavaScript meetup in Luxembourg, traveled to London, visited a few concerts and celebrated my birthday, but visiting my first tech conference in Croatia was a huge highlight.</p>
<p>Although, the arrival was a "rocky road" to say the least. I loved the city of Zadar and the Infobip Shift Conference. I met new people and people who I knew from Twitter, people who got me interested in web development. Not only that, but I listened to talks and stories of other developers. There were also cool stands and giveaways. Overall, it was a blast!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671303653575/wOZk6E1Fs.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-technical-writing">Technical Writing</h3>
<p>After participating in the Hashnode Writeathon I got an opportunity from <a target="_blank" href="https://nexten.io">nexten.io</a> to write technical articles all around development for their blog. From there, I also got an opportunity to collaborate with <a target="_blank" href="https://supabase.com">supabase.com</a> during their launch week. This was incredible because I used Supabase before and still use it for all my projects which require some kind of database or authentication. I like writing, and being able to make some additional money from it makes it even more special.</p>
<h3 id="heading-hackatum">HackaTUM</h3>
<p>So, here we are, the last big chapter of 2022.</p>
<blockquote>
<p>“It all begins and ends with a hackathon.” — Nobody ever said</p>
</blockquote>
<p>This hackathon was organized by my university and as I was new on the campus (I spent last year studying online), I decided it was a great opportunity to meet people and make new friends, maybe even win some prizes.</p>
<p>Do you remember how I said hackathons lack front-end developers and that it's easy to find a team? Well, that was the case again. I joined a team of other three 3rd semester students like myself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671439710811/yQ2l1JHBG.png" alt class="image--center mx-auto" /></p>
<p>This hackathon was somewhat different from the first one. Just one-fifth of the challenges were based on blockchain. Sponsored challenges were the main challenges, in EthAmsterdam sponsored challenges were more of a side quest.</p>
<p>After visiting a few workshops and discussing what we learned from them, my team and I decided to go with the challenge organized by Solana.</p>
<p>The project that we have been working on is called “Greeft”. We implemented two types of loyalty points into the Solana Pay app, which can redeem gift cards and rewards in our shop. One type of loyalty point is called a “green token”, it is given to the user when purchasing goods &amp; services which are environmentally friendly, e.g. a ticket for the metro. With this token, you can redeem vouchers and gift cards cheaper than with the other type, which is given for all other types of purchases.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671307580388/l2XXhFRh7.png" alt class="image--center mx-auto" /></p>
<p>We presented our project to the Solana team at the end of the hackathon. We got into the final, where we presented our project in front of all the other participants. And guess what, we WON!</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Looking back at 2022, it was an incredible ride. I never thought I could achieve and experience so many things at once. I never thought installing one social media app and one worldwide pandemic could force me to write this blog post and just think to myself “Wow, has this actually happened?”, but it happened, and I'm more than grateful for this.</p>
<p>Of course, I don't know if next year will be that amazing, and I don't have to know that. All I need is to stick to setting and completing my small goals. I just hope that in a year I will be able to look back at all those small goals, see an incredible big picture and think to myself “Wow, has this actually happened?”.</p>
<hr />
<p>Thanks for reading! ❤️ This blog post is part of Dev Retro 2022. This is my last blog post for this year, so I wish you happy holidays and a happy new year! If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com/"><strong>Hashnode</strong></a> and <a target="_blank" href="https://twitter.com/kirillinoz"><strong>Twitter</strong></a>!</p>
]]></content:encoded></item><item><title><![CDATA[How Supabase Powers Mockury]]></title><description><![CDATA[If you follow me on Twitter, you have probably heard about the project I'm working on called Mockury. I'm constantly posting updates about it on there as part of my second take on the #100DaysOfCode challenge. If you haven't heard about it, Mockury i...]]></description><link>https://blog.kirillinoz.com/how-supabase-powers-mockury</link><guid isPermaLink="true">https://blog.kirillinoz.com/how-supabase-powers-mockury</guid><category><![CDATA[supabase]]></category><category><![CDATA[authentication]]></category><category><![CDATA[database]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 06 Dec 2022 13:38:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284385137/c283c06c-0313-4345-b016-4fd7f4d234a4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you follow me on Twitter, you have probably heard about the project I'm working on called Mockury. I'm constantly posting updates about it on there as part of my second take on the <em>#100DaysOfCode</em> challenge. If you haven't heard about it, Mockury is the go-to place for mock-ups of digital products such as eBooks.</p>
<p>Mockury is one of my biggest side projects yet, so building a whole backend from scratch would cost a lot of time, which I could invest into other parts of this project. Gladly, Supabase takes care of the <strong>Database</strong> and <strong>Authentication</strong>. In this blog post, I will show you exactly what I mean by that and how you can implement them into your project.</p>
<h1 id="heading-authentication">Authentication</h1>
<p>Mockury is in its early days right now, as it hasn't even “officially” launched yet. This means it doesn't have any pricing plans, which would limit one or another feature where we would need to differentiate users. Although this is planned, there is a use case for authentication that is implemented into Mockury already now. I'm talking about the newsletter.</p>
<p>Using some basic JSX and Tailwind CSS, I designed two forms. One for the new users to sign up and another one for existing users to sign in. Here's how they look.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670304201485/aYWWk25Ji.png" alt="Sign-up and sign-in forms" class="image--center mx-auto" /></p>
<p>Next up, I installed the Supabase library and hooked it up to my application. This was very straightforward, as Supabase provides great documentation for a quick start with <a target="_blank" href="https://supabase.com/docs/guides/with-nextjs#building-the-app">Next</a> (the JavaScript framework that I'm using). Wait, you are using another framework? Don't worry, Supabase got you covered, whether you are using <a target="_blank" href="https://supabase.com/docs/guides/with-angular#building-the-app">Angular</a>, <a target="_blank" href="https://supabase.com/docs/guides/with-react#building-the-app">React</a>, <a target="_blank" href="https://supabase.com/docs/guides/with-vue-3#initialize-a-vue-3-app">Vue</a> or <a target="_blank" href="https://supabase.com/docs/guides/with-svelte#building-the-app">Svelte</a>.</p>
<p>In the quick start documentation, <a target="_blank" href="https://supabase.com/docs/guides/auth/auth-helpers/auth-ui">Supabase Auth UI</a> is utilized for creating sign-in and sign-up forms. It's quick and easy to implement. It has different themes and supports providers. In my case, I wanted to have a custom design, but if you're looking for a fast setup, you should give it a go.</p>
<p>Even though I'm utilizing a custom form design, it doesn't make things that much more complicated. We will be using three functions from the Supabase library: <code>signInWithPassword()</code>, <code>signUp()</code> and <code>signOut()</code>.</p>
<h3 id="heading-sign-up">Sign up</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [name, setName] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> [password, setPassword] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

<span class="hljs-keyword">const</span> [success, setSuccess] = useState(<span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> supabase = useSupabaseClient();

<span class="hljs-keyword">const</span> signUp = <span class="hljs-keyword">async</span> (e: FormEvent) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
      email,
      password,
      options: {
        data: {
          name,
        },
      },
    });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }

    <span class="hljs-keyword">if</span> (data) {
      setSuccess(<span class="hljs-literal">true</span>);
    }
};

<span class="hljs-keyword">return</span> (
    {<span class="hljs-comment">/* ... */</span>}
    &lt;form onSubmit={signUp}&gt;
        &lt;input
            id=<span class="hljs-string">"name"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setName(e.target.value)}
        /&gt;
        &lt;input
            id=<span class="hljs-string">"email"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setEmail(e.target.value)}
        /&gt;
        &lt;input
            id=<span class="hljs-string">"password"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span>
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setPassword(e.target.value)}
        /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> value=<span class="hljs-string">"Sign up"</span> /&gt;
    &lt;/form&gt;
    {<span class="hljs-comment">/* ... */</span>}
)
</code></pre>
<p>First, we initialize three states, which change depending on the user input. After the user submits our form, we execute the <code>signUp()</code> function. The function takes in <code>FormEvent</code>, so we can prevent the default behaviour. Then we fire the <code>supabase.auth.signUp()</code> function, which takes in an email and a password. You can also add optional data, in my case I send the user's name which will be added to the database, more on that later.</p>
<p>The fourth state (success) is then used to display a message asking the user to confirm their email.</p>
<blockquote>
<p>Find out more about signUp() <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/reference/javascript/v1/auth-signup"><strong>HERE</strong></a></p>
</blockquote>
<h3 id="heading-sign-in">Sign in</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> [password, setPassword] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);

<span class="hljs-keyword">const</span> supabase = useSupabaseClient();
<span class="hljs-keyword">const</span> router = useRouter();

<span class="hljs-keyword">const</span> signIn = <span class="hljs-keyword">async</span> (e: FormEvent) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({
      email,
      password,
    });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }

    <span class="hljs-keyword">if</span> (data) {
      <span class="hljs-built_in">console</span>.log(data);
      router.push(<span class="hljs-string">"/studio"</span>);
    }
};

<span class="hljs-keyword">return</span> (
    {<span class="hljs-comment">/* ... */</span>}
    &lt;form onSubmit={signIn}&gt;
        &lt;input
            id=<span class="hljs-string">"email"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setEmail(e.target.value)}
        /&gt;
        &lt;input
            id=<span class="hljs-string">"password"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span>
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setPassword(e.target.value)}
        /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> value=<span class="hljs-string">"Sign up"</span> /&gt;
    &lt;/form&gt;
    {<span class="hljs-comment">/* ... */</span>}
)
</code></pre>
<p>As you probably already noticed, it's almost the same as our sign-up component. The only difference is that we utilize the <code>supabase.auth.signInWithPassword()</code> function. It also redirects the user to another page, once signed in.</p>
<blockquote>
<p>Find out more about signInWithPassword() <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/guides/auth/auth-email"><strong>HERE</strong></a></p>
</blockquote>
<h3 id="heading-providers">Providers</h3>
<p>If you want to give your users more freedom on how they can sign in. You can use any providers that Supabase offers. I couldn't even fit all of them into this screenshot, that's how many there are.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670318084273/ps3iJeeni.png" alt="Providers" class="image--center mx-auto" /></p>
<p>As Gmail services are fairly widespread, that's the only provider I'm offering in my application. It's also easy to implement into the code, but needs some setup in advance using Google Cloud Platform and Supabase Dashboard. But don't be afraid, the process is greatly documented.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> supabase = useSupabaseClient();

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signInWithGoogle</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithOAuth({
      provider: <span class="hljs-string">"google"</span>,
    });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }
}

<span class="hljs-keyword">return</span> (
    {<span class="hljs-comment">/*...*/</span>}
    &lt;button onClick={signInWithGoogle}&gt;
        Google
    &lt;/button&gt;
    {<span class="hljs-comment">/*...*/</span>}
)
</code></pre>
<p>When the button is clicked we call signInWithGoogle() which fires <code>supabase.auth.signInWithOAuth</code> where we can specify the provider we want to use, in this case, it's Google.</p>
<blockquote>
<p>Find out more about providers <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/guides/auth#providers"><strong>HERE</strong></a></p>
</blockquote>
<h3 id="heading-sign-out">Sign out</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> supabase = useSupabaseClient();

<span class="hljs-keyword">const</span> signOut = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    } <span class="hljs-keyword">else</span> {
      router.push(<span class="hljs-string">"/studio"</span>);
    }
};

<span class="hljs-keyword">return</span> (
    &lt;button onClick={signOut}&gt;
        Sign out
    &lt;/button&gt;
)
</code></pre>
<p>If you thought providers are easy to implement, then <code>signOut()</code> is just kindergarten. It's crazy how easy Supabase makes it to set up authentication that I would spend weeks implementing using Express and Passport.</p>
<blockquote>
<p>Find out more about signOut() <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/reference/javascript/v1/auth-signout"><strong>HERE</strong></a></p>
</blockquote>
<h1 id="heading-database">Database</h1>
<p>As mentioned earlier, I set up authentication in advance for pricing plans. But we can already now utilize it to keep our users updated on how the development of Mockury is going on. For that, we need to keep track of users who want to receive our newsletter issues.</p>
<p>Even though, you can create tables using Supabase's GUI. It's even better to know some basic PostgreSQL, so you can set up tables faster and utilize RLS (Row Level Security).</p>
<p>My favourite way (and probably the fastest way) to create tables is to utilize quick-start templates offered by Supabase. <strong>Choose</strong> the template you want to use, <strong>modify</strong> your query and <strong>run</strong> your SQL snippet. It's that simple.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670318125751/neC0U4sDp.png" alt="Quick start templates" class="image--center mx-auto" /></p>
<p>As I'm not that good with SQL, I like to duplicate the template. If I do something wrong, I would just switch between them and compare the original with my modified snippet, find the problem and make changes.</p>
<p>For our user database, we will be using the <strong>User Management Starter</strong> template.</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> users (
  id <span class="hljs-type">uuid</span> <span class="hljs-keyword">references</span> auth.users <span class="hljs-keyword">not</span> <span class="hljs-keyword">null</span> <span class="hljs-keyword">primary key</span>,
  email <span class="hljs-type">text</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">null</span>,
  <span class="hljs-type">name</span> <span class="hljs-type">text</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">null</span>,
  newsletter <span class="hljs-type">boolean</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">true</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">null</span>
);

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> users
  <span class="hljs-keyword">enable</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">level</span> <span class="hljs-keyword">security</span>;

<span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> "Users can view their own info." <span class="hljs-keyword">on</span> users
  <span class="hljs-keyword">for</span> <span class="hljs-keyword">select</span> <span class="hljs-keyword">using</span> (auth.uid() = id);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> "Users can insert their own info." <span class="hljs-keyword">on</span> users
  <span class="hljs-keyword">for</span> <span class="hljs-keyword">insert</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">check</span> (auth.uid() = id);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">policy</span> "Users can update own info." <span class="hljs-keyword">on</span> users
  <span class="hljs-keyword">for</span> <span class="hljs-keyword">update</span> <span class="hljs-keyword">using</span> (auth.uid() = id);
</code></pre>
<p>Here's my table. We got the user's identifier, user's email, user's name and if they want to subscribe to the newsletter.</p>
<p>We also have RLS (row-level security) enabled, so each user can only get, set and change their data and stop any possible misbehaviour.</p>
<p>Now we have the table, how do we add the users to our database? Let me introduce you to triggers and functions.</p>
<h3 id="heading-triggers-and-functions">Triggers and functions</h3>
<p>Triggers listen for events such as insert, update or delete. Once one of your specified events is <em>triggered</em>, the trigger calls a function.</p>
<p>Functions are SQL snippets that contain assignments, declarations, loops, flows-of-control, etc.</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">function</span> <span class="hljs-built_in">public</span>.handle_new_user()
<span class="hljs-keyword">returns</span> <span class="hljs-type">trigger</span> <span class="hljs-keyword">as</span> $$<span class="pgsql">
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> <span class="hljs-built_in">public</span>.users (id, email, <span class="hljs-type">name</span>)
  <span class="hljs-keyword">values</span> (<span class="hljs-built_in">new</span>.id, <span class="hljs-built_in">new</span>.email, <span class="hljs-built_in">new</span>.raw_user_meta_data-&gt;&gt;<span class="hljs-string">'name'</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">new</span>;
<span class="hljs-keyword">end</span>;
$$</span> <span class="hljs-keyword">language</span> plpgsql <span class="hljs-keyword">security</span> <span class="hljs-keyword">definer</span>;
<span class="hljs-keyword">create</span> <span class="hljs-keyword">trigger</span> on_auth_user_created
  <span class="hljs-keyword">after</span> <span class="hljs-keyword">insert</span> <span class="hljs-keyword">on</span> auth.users
  <span class="hljs-keyword">for</span> <span class="hljs-keyword">each</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">execute</span> <span class="hljs-keyword">procedure</span> <span class="hljs-built_in">public</span>.handle_new_user();
</code></pre>
<p>First, we create a function that inserts the user data into our user database. Using <code>new.raw_user_meta_data</code> we can access the optional data that we provided during the sign-up, in my case I only wanted the name of the user. By now, you probably noticed that one column is missing: <code>newsletter</code>. This is because when creating the table, we used the <code>default true</code> statement which sets our <code>newsletter</code> value to true automatically as long as no other data is provided. We will allow users to modify it later.</p>
<p>In the end, we create a trigger which calls our newly created function once a new user has been added to the <code>auth.users</code> table, which is provided by default by Supabase.</p>
<h3 id="heading-manipulating-data">Manipulating data</h3>
<p>Not every user wants to receive the newsletter, and that's fine. We shouldn't force it onto them, so let's provide a checkbox button which will change the value of <code>newsletter</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [newsletter, setNewsletter] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">true</span>);

<span class="hljs-keyword">const</span> session = useSession();
<span class="hljs-keyword">const</span> supabase = useSupabaseClient();
<span class="hljs-keyword">const</span> router = useRouter();

useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> getData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">"users"</span>)
        .select()
        .single();

      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
      }
      <span class="hljs-keyword">if</span> (data) {
        setNewsletter(data.newsletter);
      }
    };

    getData().catch(<span class="hljs-built_in">console</span>.error);
}, [supabase]);

<span class="hljs-keyword">const</span> updateNewsletter = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"users"</span>)
      .update({ newsletter: !newsletter })
      .eq(<span class="hljs-string">"id"</span>, session?.user?.id);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }

    setNewsletter(!newsletter);
};

<span class="hljs-keyword">return</span> (
    {<span class="hljs-comment">/* ... */</span>}
    {newsletter ? (
        &lt;button onClick={updateNewsletter} className=<span class="hljs-string">"btn"</span>&gt;
            Deactivate
        &lt;/button&gt;
     ) : (
        &lt;button onClick={updateNewsletter} className=<span class="hljs-string">"btn highlight"</span>&gt;
            Activate
        &lt;/button&gt;
     )}
    {<span class="hljs-comment">/* ... */</span>}
)
</code></pre>
<p>When the settings page renders, we fetch the current value of <code>newsletter</code> using <code>supabase.from().select()</code> which decides which checkbox button we should show.</p>
<blockquote>
<p>Find out more about select() <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/reference/javascript/select"><strong>HERE</strong></a></p>
</blockquote>
<p>Once the button is pressed, we update our database using <code>supabase.from().update()</code> where we have to provide the user ID due to RLS. We also update the newsletter state on the client side to display the appropriate button.</p>
<blockquote>
<p>Find out more about update() <strong>👉</strong> <a target="_blank" href="https://supabase.com/docs/reference/javascript/update"><strong>HERE</strong></a></p>
</blockquote>
<p>Now I have a collection of users with their newsletter preferences. Using Supabase's table editor, I can filter users who want to receive my newest newsletter issue, export the data to CSV and send out the issue.</p>
<p>Check out <a target="_blank" href="http://mockury.com">Mockury</a> and sign up for the newsletter to see how it works yourself!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670318151546/YMfh2Qa73.png" alt="Mockury landing page" class="image--center mx-auto" /></p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the Content Storm which celebrates <a target="_blank" href="https://supabase.com/launch-week">SupaLaunchWeek 6</a>. If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com/"><strong>Hashnode</strong></a> and <a target="_blank" href="https://twitter.com/kirillinoz"><strong>Twitter</strong></a>!</p>
]]></content:encoded></item><item><title><![CDATA[Stealing Coin Cat with CSS]]></title><description><![CDATA[In this article, I'm going to break down my thoughts behind the “Stealing Coin Cat” which was my submission for the Hashnode CSS Challenge organized by Nazanin Ashrafi back in January. Thanks to #4articles4weeks challenge that reminded me about the e...]]></description><link>https://blog.kirillinoz.com/stealing-coin-cat-with-css</link><guid isPermaLink="true">https://blog.kirillinoz.com/stealing-coin-cat-with-css</guid><category><![CDATA[HTML5]]></category><category><![CDATA[CSS]]></category><category><![CDATA[CSS Animation]]></category><category><![CDATA[HashnodeChallenge]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 18 Oct 2022 06:30:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1672032294836/2592b208-58a1-4c1f-87db-836c8a2a6f92.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I'm going to break down my thoughts behind the “Stealing Coin Cat” which was my submission for the Hashnode CSS Challenge organized by <a class="user-mention" href="https://hashnode.com/@Nazanin">Nazanin Ashrafi</a> back in January. Thanks to <a target="_blank" href="https://townhall.hashnode.com/4-articles-in-4-weeks-hashnode-writeathon">#4articles4weeks</a> challenge that reminded me about the existence of this draft.</p>
<h3 id="heading-idea-and-inspiration">Idea and inspiration</h3>
<p>Let's kick off with the origin of the idea, my first thought was:</p>
<blockquote>
<p>“What are the greatest CSS art creations that I have seen and how would I make them different (even better if possible)?”</p>
</blockquote>
<p>One of these creations that popped into my head was “<a target="_blank" href="https://codepen.io/jh3y/pen/LYNZwGm">The Impossible Checkbox</a>” by <a class="user-mention" href="https://hashnode.com/@jh3y">Jhey Tompkins</a>. I found it fascinating, funny and kind of cute how the bear reached out for the checkbox to turn it off. This reminded me of cat banks.</p>
<p>Cat banks are little boxes which have a button on top of them. The user puts a coin on top of the button, presses it, and a little kitten pops out of the box to steal the coin. This makes storing coins in piggy banks more exciting and cuter.</p>
<p>Of course, I didn't get straight to work. This was my third CSS art project and I wanted it to be different from my last two. I wanted to learn something new from this experience. After thinking about it, I came up with two big points about the “Stealing Coin Cat” that were interesting and new to me: <strong>3D Perspective</strong> and <strong>Cohesive Animations</strong>.</p>
<p>From there I equipped myself with an image of such a cat bank and started working on the art itself.</p>
<h3 id="heading-box">Box</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1665815837804/Yj1TT_d8s.png" alt="1.png" class="image--center mx-auto" /></p>
<p>The box is fully made of trapezoids, besides button, cover shadow or hidden elements. I was thinking pretty long on what to put at the front. I thought about putting fruits, vegetables, logos or whatsoever. In the end, I decided to simply go with “Hashnode”. The most difficult part was probably layering, because of different elements that should be hidden behind each other when animated.</p>
<h3 id="heading-cat">Cat</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1665815861578/9q2PmFkoB.png" alt="2.png" class="image--center mx-auto" /></p>
<p>The cat is made of basic shapes, such as eclipses and triangles. Somehow I completely forgot to add whiskers to the cat, but it still looks decent because I wasn't going for anything hyperrealistic. At first, I didn't add any eye highlights, so the eyes of the kitten looked like void. While I was working on other elements, I constantly looked into those deep eyes and at some point I decided to come back and make it cuter by adding highlights.</p>
<h3 id="heading-paw-and-coin">Paw and coin</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1665815878352/7BGmyLQlj.png" alt="3.png" class="image--center mx-auto" /></p>
<p>The paw was a challenge on its own. It should be under the cardboard and then go over the cardboard, which would be a complete mess to animate. Except if you break it down into two parts, and that's what I did. The first part stretches up, the second part waits until it's finished, then it stretches down towards the coin. It was quite hard to time it precisely and make it look like the paw is stretching from inside to outside, but I think it turned out pretty well.</p>
<p>The coin (if you haven't noticed) represents the Hashnode logo. It was part of the competition to integrate the Hashnode logo into your art piece. At first, I wanted to put the logo at the front of the box, but at some point I realized that it looks like a coin, so I replaced the golden circle with a golden Hashnode logo.</p>
<h3 id="heading-animations">Animations</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1665823121330/SH78PpwQH.png" alt="4.png" /></p>
<p>I think that's the most keyframes I have ever used. The animations aren't that special, it's only moving, rotating and stretching. As previously said, the timing was the hardest thing to implement, but after multiple tries and fails, at some point it worked fine.</p>
<p>Here's the result, you can check out the final code in this CodePen:</p>
<iframe height="500" style="width:100%" src="https://codepen.io/inkuantum/embed/XWeoaXP?default-tab=html%2Cresult">
  See the Pen <a href="https://codepen.io/inkuantum/pen/XWeoaXP">
  Stealing Coin Cat</a> by Kirill Inoz (<a href="https://codepen.io/inkuantum">@inkuantum</a>)
  on <a href="https://codepen.io">CodePen</a>.
</iframe>

<h3 id="heading-results">Results</h3>
<p>The challenge had tons of interesting submissions and winners were picked randomly. I was lucky enough to win a mug and a t-shirt, which you can see in this picture.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1665823376414/dk3rQ8w6w.jpeg" alt="FOXLlJWX0AErDQT.jpeg" /></p>
<p>In the end, I learned more about <strong>3D Perspective</strong> and <strong>Cohesive Animations</strong> in CSS, which were my main goals going into this challenge.</p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the series “<a target="_blank" href="https://blog.ikirill.com/series/css">Things With CSS</a>”. If you want to be the first one to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com">Hashnode</a> and on <a target="_blank" href="https://twitter.com/kirillinoz">Twitter</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Here Is What You Could Build]]></title><description><![CDATA[Every developer loves to build. And why not? It's an efficient way to acquire new skills, gather new information and maybe even make some income. Even though that's the case, it's often hard to come up with ideas.
In this article, I want to share som...]]></description><link>https://blog.kirillinoz.com/heres-what-you-could-build-fe</link><guid isPermaLink="true">https://blog.kirillinoz.com/heres-what-you-could-build-fe</guid><category><![CDATA[Frontend Development]]></category><category><![CDATA[ideas]]></category><category><![CDATA[side project]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[#week4]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Sun, 11 Sep 2022 20:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284157192/933912df-4316-4d95-98b1-886d0c01f071.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every developer loves to build. And why not? It's an efficient way to acquire new skills, gather new information and maybe even make some income. Even though that's the case, it's often hard to come up with ideas.</p>
<p>In this article, I want to share some ideas on what you could build as your next side project and what technologies you could use. You don't have to build all of them or even any of them. I just want to share some of my recommendations and if I can even slightly inspire your next project, I'm going to consider it a win.</p>
<hr />
<h2 id="heading-clone-interesting-websites">Clone interesting websites</h2>
<p>Someone would say cloning is a very uncreative project idea, and I will agree in 9 out of 10 times. That one case when I would disagree is if you gave the cloned website your own twist.</p>
<p>But creativity isn't the thing you necessarily should chase when choosing a project idea. I'm going to mention a couple famous websites. They were built by groups of professionals at large companies. These people aren't rookies, and you could learn a lot from them by just mimicking their work and understanding why and how they used this or that technique.</p>
<h3 id="heading-netflix-example">Netflix (Example)</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662900127900/7vjBFZWSu.png" alt="Xnapper-2022-09-11-14.40.44.png" class="image--center mx-auto" /></p>
<p><em>Netflix's</em> landing page is very straightforward, you have:</p>
<ul>
<li><p>Header: logo, language and sign in button.</p>
</li>
<li><p>Hero section: showing the catalogue of shows, catchphrase, email input with a call-to-action button</p>
</li>
<li><p>Features: rows split into an image and description</p>
</li>
<li><p>FAQ: Expandable drop-downs with question and text</p>
</li>
<li><p>Footer: Menu, legal and commercial stuff</p>
</li>
</ul>
<p>It's pretty easy to replicate. I did it myself as well, taking some inspiration from other streaming services such as <em>Disney+</em> and <em>Apple TV+</em>. It was actually my first website made outside a course. So if you want to check it out, I'll leave it <a target="_blank" href="https://action-plus.netlify.app/">here</a>.</p>
<p>If you would like, you could go one step further and build the logged in page. This one could be even easier, as it consists of rows filled with images of shows and movies, with a big banner at the top.</p>
<h4 id="heading-similarly-structured-landing-pages">Similarly structured landing pages</h4>
<p><strong>Discord</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662900185901/k0HcQxxbb.png" alt="Xnapper-2022-09-11-14.42.47.png" class="image--center mx-auto" /></p>
<p>It's the same as the <em>Netflix's</em> landing page, it's just more colourful and has no FAQ section. </p>
<p><strong>Slack</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662900330369/5JtwNd7Df.png" alt="Xnapper-2022-09-11-14.45.18.png" class="image--center mx-auto" /></p>
<p>Very similar to <em>Netflix's</em> landing page, but adds some statistics and testimonials after features section.</p>
<hr />
<h2 id="heading-some-quick-ideas-for-web-apps">Some quick ideas for web apps</h2>
<p>If you are NOT into cloning websites, here are some ideas for web applications you could create. Let your creativity loose on how to design and build them.</p>
<h3 id="heading-basic">Basic</h3>
<ul>
<li>To-do List</li>
<li>Calculator</li>
<li>Word Counter</li>
</ul>
<h3 id="heading-advanced">Advanced</h3>
<ul>
<li>Task Management</li>
<li>Music Player</li>
<li>Dashboard</li>
</ul>
<hr />
<h2 id="heading-technologies">Technologies</h2>
<p>When it comes only to landing pages like in the first chapter, you could probably go with plain HTML, CSS, and maybe JavaScript if even needed. Otherwise, you can save some time on designs using CSS frameworks. </p>
<p>When it comes to web applications like in the second chapter, you could still build them with plain HTML, CSS, and JavaScript, but you will save time and effort using a JavaScript framework. Of course, only go with the last option if you're familiar with JavaScript.</p>
<p>Every day, there's some new JavaScript framework or CSS framework that is popping up, so you can go with any of them, but here are my suggestion (aka most popular ones).</p>
<h3 id="heading-css-frameworks">CSS frameworks</h3>
<ul>
<li>Bootstrap</li>
<li>Tailwind CSS</li>
<li>Materialize CSS</li>
<li>…</li>
</ul>
<h3 id="heading-javascript-frameworks">JavaScript frameworks</h3>
<ul>
<li>React</li>
<li>Angular</li>
<li>Vue</li>
<li>Svelte</li>
<li>…</li>
</ul>
<p>Choose any framework (if you need to) and good luck with your next side project! I hope I could give you some inspiration and ideas on what technologies you could use.</p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the #4articles4weeks challenge. If you want to be the first to see my next article, follow me on Hashnode and on Twitter!</p>
]]></content:encoded></item><item><title><![CDATA[How I Built Reclip and Learned TypeScript]]></title><description><![CDATA[Maybe “built” isn't the right word, as it's still an ongoing project. But I just released the first version of it, so I decided that it's a great opportunity to share my learnings. Especially after my tech stack has somewhat changed since my last pro...]]></description><link>https://blog.kirillinoz.com/how-i-built-reclip-and-learned-typescript</link><guid isPermaLink="true">https://blog.kirillinoz.com/how-i-built-reclip-and-learned-typescript</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[#week3]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Sun, 04 Sep 2022 18:29:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284127369/14b99bae-32c2-4aa9-a6d1-d9d4b9fd4ff5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Maybe “built” isn't the right word, as it's still an ongoing project. But I just released the first version of it, so I decided that it's a great opportunity to share my learnings. Especially after my tech stack has somewhat changed since my last project, so I could explore and learn a new technology called TypeScript.</p>
<h3 id="heading-typescript-javascript-on-steroids">TypeScript? JavaScript on steroids.</h3>
<p>TypeScript is a programming language. Its syntax builds on top of JavaScript, by introducing types which are typical for object-oriented programming languages. These types should allow developers to find mistakes before running your code.</p>
<p>JavaScript uses dynamic variables, which can change their type. TypeScript introduces static types, which won't allow variables to change their type during runtime.</p>
<p>JavaScript's in built types are also available in TypeScript, such as <code>number</code>, <code>string</code>, <code>boolean</code>, <code>symbol</code>, <code>null</code>, <code>undefined</code>, <code>object</code>. TypeScript also allows you to create your own types and not only that, it comes with a few more types such as <code>enum</code>, <code>never</code>, <code>tuple</code>, <code>array</code>, <code>union</code>, <code>any</code>, <code>void</code> and <code>never</code>.</p>
<p>All this helps the code to become more structured, as one variable will always correspond to one type. The code becomes more bulletproof, no wrong types can be passed into components or functions, which could also save your app from crashing when a user inputs wrong data.</p>
<blockquote>
<p>If you want to find out more about TypeScript, I recommend reading this great blog post by <a class="user-mention" href="https://hashnode.com/@codecryrepeat">Sergii Kirianov</a> → <a target="_blank" href="https://codecryrepeat.hashnode.dev/dont-be-afraid-of-typescript">Don't be afraid of TypeScript</a></p>
</blockquote>
<h3 id="heading-tech-stack-for-reclip">Tech Stack for Reclip</h3>
<h4 id="heading-typescript">TypeScript</h4>
<p>TypeScript was the newest technology in my tech stack that I haven't used before. I took a <a target="_blank" href="https://scrimba.com/learn/typescript">course</a> beforehand on Scrimba which introduced me to the basics of TypeScript and its syntax. This was enough to get me started with this project and get my hands dirty in the “wilderness” aka building a new project from scratch.</p>
<h4 id="heading-react">React</h4>
<p>React isn't my go-to frontend framework, I love using Vue and Nuxt more. Recently, I got a job where React is the frontend framework of choice, so I decided to use it in this project as well to get better at it. I haven't worked with it a lot before, so I thought this project would be a good opportunity for it.</p>
<h4 id="heading-tailwind-css">Tailwind CSS</h4>
<p>Something familiar that was in this project for me was Tailwind. It's my go-to CSS framework and I love it. I believe it's even better to use with React than Vue, so I had no intentions of choosing another CSS framework or using only raw CSS.</p>
<h4 id="heading-ffmpeg">FFmpeg</h4>
<p>FFmpeg enables video &amp; audio record, convert and stream right inside browsers. It's definitely the most important node package I have to work with and understand for this project. It's basically the brain of the editor. I already tried its vanilla version half a year ago. This time I used WebAssembly and JavaScript port to allow editing directly on user's device without any external servers in play.</p>
<h3 id="heading-what-is-reclip-about">What is Reclip about?</h3>
<p>Reclip is an editor that allows to crop videos from 16:9 ratio (used on YouTube, Twitch) to 9:16 ratio (used on TikTok, YouTube Shorts). This is the main functionality of the first version of the editor. It just crops your video wherever you want it to do so. This is also what most of the other editors already do.</p>
<p>The problem is that so much content just gets thrown away as you crop from horizontal to vertical. The mission of Reclip is to crop the content in a way that the most important stays constantly in the frame. This will be realized using the following features.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662311433473/eoLUvwcEZ.png" alt="Xnapper-2022-09-04-18.50.32.png" class="image--center mx-auto" /></p>
<blockquote>
<p>For more information or better readability, check out → <a target="_blank" href="https://reclip.vercel.app">Reclip</a></p>
</blockquote>
<h3 id="heading-file-uploader-typescript-example">File Uploader (TypeScript Example)</h3>
<p>The first thing that you see when opening the Reclip editor is the file uploader, so I wanted to showcase how I used TypeScript to build this component.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> FileUploaderProps = {
    inputVideo: File | <span class="hljs-literal">null</span> | <span class="hljs-literal">undefined</span>
    setInputVideo: <span class="hljs-function">(<span class="hljs-params">video: File | <span class="hljs-literal">null</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>
}
</code></pre>
<p>First, we define types for properties the component will take in. <code>inputVideo</code> is a state that equals <code>undefined</code> in the beginning, but after the user chooses a video can only be <code>null</code> or of type <code>File</code>. <code>setInputVideo</code> is just a setter for our state that takes in exactly the two values I mentioned before.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FileUploader</span>(<span class="hljs-params">{ inputVideo, setInputVideo }: FileUploaderProps</span>) </span>{
  <span class="hljs-keyword">const</span> hiddenFileInput = useRef&lt;HTMLInputElement&gt;(<span class="hljs-literal">null</span>)
  <span class="hljs-keyword">const</span> [fileName, setFileName] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">''</span>)

  <span class="hljs-comment">//...</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> FileUploader
</code></pre>
<p>After declaring the types for our props, our component will only take in these props if its values have the appropriate type.</p>
<p>We also add two hooks:</p>
<ol>
<li><p>Reference to the hidden file input. I wanted to style the input differently (and overcomplicate things), so we need it. As you can see, type is declared differently in this case compared to our previous snippet. It follows this formula for all types of hooks: <code>hook&lt;type&gt;(default value)</code>.</p>
</li>
<li><p>State of the file name. Again I wanted to display the name, but because we hide the default file input, it has to be saved separately. In this case, we're using another hook, but the syntax is the same as for our previous reference.</p>
</li>
</ol>
<pre><code class="lang-html">return (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            {inputVideo ? (
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center px-4 py-2 bg-gray-100 rounded-sm"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                        <span class="hljs-attr">className</span>=<span class="hljs-string">"button mr-5 rounded-full"</span>
                        <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleClick}</span>
                    &gt;</span>
                        Change
                    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-bold text-sm overflow-hidden"</span>&gt;</span>
                        {fileName}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            ) : (
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                        <span class="hljs-attr">className</span>=<span class="hljs-string">"w-1/2 h-1/2 mx-auto"</span>
                        <span class="hljs-attr">src</span>=<span class="hljs-string">"/img/upload-video.svg"</span>
                        <span class="hljs-attr">alt</span>=<span class="hljs-string">"Upload video"</span>
                    /&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                        <span class="hljs-attr">className</span>=<span class="hljs-string">"button highlight mt-20"</span>
                        <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleClick}</span>
                    &gt;</span>
                        Choose video
                    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-2 text-center text-xs text-gray-600"</span>&gt;</span>
                        Supported format <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-bold"</span>&gt;</span>MP4<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>{' '}
                        | Max file size <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-bold"</span>&gt;</span>50MB<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            )}

            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
                <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span>
                <span class="hljs-attr">className</span>=<span class="hljs-string">"hidden"</span>
                <span class="hljs-attr">ref</span>=<span class="hljs-string">{hiddenFileInput}</span>
                <span class="hljs-attr">onChange</span>=<span class="hljs-string">{handleChange}</span>
                <span class="hljs-attr">accept</span>=<span class="hljs-string">".mp4"</span>
            &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">input</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    )
</code></pre>
<p>Now we add some JSX and Tailwind CSS, of course no TypeScript, so no types here.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">() =&gt;</span> {
    hiddenFileInput?.current?.click()
}
</code></pre>
<p>This is fairly interesting, as earlier we declared <code>hiddenFileInput</code>'s type as <code>HTMLInputElement</code>. It's an interface that comes from the DOM and can be an HTML input element or <code>null</code>. Because we can't click on a <code>null</code> value, we add optional chaining <code>?.</code> to check that we're safe to click on it.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleChange = <span class="hljs-function">(<span class="hljs-params">event: React.ChangeEvent&lt;HTMLInputElement&gt;</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> fileUploaded = event.target.files?.[<span class="hljs-number">0</span>]
        <span class="hljs-keyword">if</span> (fileUploaded !== <span class="hljs-literal">undefined</span>) {
            <span class="hljs-keyword">if</span> (fileUploaded.size &lt;= <span class="hljs-number">52</span>_428_800) {
                setInputVideo(fileUploaded)
                setFileName(fileUploaded.name)
            } <span class="hljs-keyword">else</span> {
                alert(<span class="hljs-string">'File size is too big!'</span>)
            }
        }
    }
</code></pre>
<p>At last, we want to check when a file is selected. We also need to retrieve the data, so we have to pass an event handler as a parameter. There are multiple event handler types available by React, so you need to be careful to select the right one. Don't worry, you'll be notified by your IDE if you use the wrong one. In our case, it's <code>ChangeEvent&lt;HTMLInputElement&gt;</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662314485545/Gkwb4HWJG.png" alt="Xnapper-2022-09-04-20.00.51.png" class="image--center mx-auto" /></p>
<p>That's it, our file uploader is ready! I hope you could understand TypeScript better by means of this example.</p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the #4articles4weeks challenge. If you want to be the first to see my next article, follow me on Hashnode and on Twitter!</p>
]]></content:encoded></item><item><title><![CDATA[Helpful Communities You Should Know About]]></title><description><![CDATA[Developers are often considered problem-solvers in the first place. It's a skill that takes a lot of practice to perfect. So it isn't unusual for developers to get stuck.
Speaking of skills, some people think Googling is cheating, something we were t...]]></description><link>https://blog.kirillinoz.com/helpful-communities-you-should-know-about</link><guid isPermaLink="true">https://blog.kirillinoz.com/helpful-communities-you-should-know-about</guid><category><![CDATA[freeCodeCamp.org]]></category><category><![CDATA[LearnWeb3Dao]]></category><category><![CDATA[Twitter]]></category><category><![CDATA[#week2]]></category><category><![CDATA[4articles4weeks]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Sun, 28 Aug 2022 19:57:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284093790/3c26cc53-77e8-44a7-b8ca-d7f80c75ccf6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Developers are often considered problem-solvers in the first place. It's a skill that takes a lot of practice to perfect. So it isn't unusual for developers to get stuck.</p>
<p>Speaking of skills, some people think Googling is cheating, something we were taught in school a lot, when actually it should be seen as a skill. Google is an algorithm that takes in words and gives you the best possible results. So choosing the correct words is the key to getting the correct results and saving you time on solving a problem.</p>
<p>But what will you do if you get stuck and Google can't help you? Let's picture the scenario where you have digested your issue, you looked for a solution on Google / Stack Overflow. You took a walk, you slept on the issue, and nothing has helped you come up with a solution.</p>
<blockquote>
<p>Two heads are better than one...</p>
</blockquote>
<p>… but more is even better. That's where communities come into play. There's a probability that someone else had the same issue, but came up with a solution by themselves and never documented it online, but they would love to help you.</p>
<p>In this article, I want to mention 3 communities that I heard about or that I'm part of, and how they can help you get “unstuck” in your problem-solving journey.</p>
<hr />
<h2 id="heading-freecodecamp">freeCodeCamp</h2>
<p>If you're looking for an amazing non-profit community to learn to code, freeCodeCamp is the way to go. They help tons of people get into coding, offering courses and other learning materials. freeCodeCamp has a forum where you can ask questions if you get stuck, and it's an incredibly open and heart-warming community.</p>
<p>I especially recommend taking a deeper look into it if you're just beginning your programming journey.</p>
<blockquote>
<p>Learn more here -&gt; <a target="_blank" href="https://www.freecodecamp.org/">freeCodeCamp</a></p>
</blockquote>
<h2 id="heading-learnweb3">LearnWeb3</h2>
<p>I have been interested in Web3 for almost 10 months now, and a lot of what I've learned is due to LearnWeb3. Besides teaching you about web development and blockchain through their website filled with different tracks, they have a strong and active community on Discord. </p>
<p>Not that long ago, I have been working on an app and got stuck, so I turned to the help channel on Discord. It took just a few hours before I connected with <a class="user-mention" href="https://hashnode.com/@DanyTulumidis">Dany Tulumidis</a> who helped me out.</p>
<p>If you're looking to get into Web3, or you're stuck and need a helping hand in that space, LearnWeb3 is the way to go.</p>
<blockquote>
<p>Learn more here -&gt; <a target="_blank" href="https://learnweb3.io/">LearnWeb3</a></p>
</blockquote>
<h2 id="heading-tech-twitter">Tech Twitter</h2>
<p>Even though you won't get much help, posting a tweet describing your issue (without any audience). You can reach out directly to developers on Twitter, and in most cases they are happy to answer and help you out.</p>
<p>Your first message counts, as a simple “Hi!” would probably be just ignored. In your first message, state what your problem is or pose questions you require answers to. Of course, be polite and don't expect people you're reaching out to, to answer quickly.</p>
<p>If you have done everything correctly, developers would be glad to answer your question or guide you to some resources that might be able to help you out.</p>
<hr />
<p>Thanks for reading! ❤️ This article is part of the #4articles4weeks challenge. If you want to be the first to see my next article, follow me on Hashnode and on Twitter!</p>
<p>Tell me in the comments which communities are you in.</p>
]]></content:encoded></item><item><title><![CDATA[From Gaming to Debugging]]></title><description><![CDATA[Every developer has their own superhero origin story on how they got into development. I wanted to share mine and give you some takeaways, so you don't do the same mistakes I did. Enjoy! ❤️

Gamer Kid
Video games accompanied me since I can remember. ...]]></description><link>https://blog.kirillinoz.com/from-gaming-to-debugging</link><guid isPermaLink="true">https://blog.kirillinoz.com/from-gaming-to-debugging</guid><category><![CDATA[software development]]></category><category><![CDATA[gaming]]></category><category><![CDATA[Career]]></category><category><![CDATA[4articles4weeks]]></category><category><![CDATA[week1]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Wed, 17 Aug 2022 15:10:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284066821/659cd13b-95e6-4f7e-883a-bc8ed83ae014.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every developer has their own superhero origin story on how they got into development. I wanted to share mine and give you some takeaways, so you don't do the same mistakes I did. Enjoy! ❤️</p>
<hr />
<h2 id="heading-gamer-kid">Gamer Kid</h2>
<p>Video games accompanied me since I can remember. And when I say “since I can remember”, I mean it. “Finding Nemo”, the video game from 2003, is one of if not the farthest memory I can remember. My 4-year-old-self was happier than ever to get first to the family computer to play some mini-games with his favourite cartoon characters.</p>
<p>Just four years later, I got my Nintendo DS and “Pokémon: Heart Gold” which is still one of my most favourite games, probably because of nostalgia. Staying longer after school and swapping Pokémon from one friend to another? <strong>Sign me up!</strong></p>
<p>And when you're passionate about something, it won't take long before you want to find out what's happening behind the scenes…</p>
<h2 id="heading-school-informatics">School Informatics</h2>
<p>Besides learning how to turn on the computer and type in Word, let's not talk about this informatics class, I had another informatics class where we made games with different game engines / programming languages, one of them was Scratch.</p>
<blockquote>
<p>Scratch is a high-level block-based visual programming language and website aimed primarily at children as an educational tool for programming. – Wikipedia</p>
</blockquote>
<p>As someone who gamed a lot, I quickly realized that Scratch is way too limited on what you can create with it. We coded some basic programs in Visual Basic as well, but nothing from that hooked me to pursue programming as a hobby, not even talking about doing it professionally.</p>
<h2 id="heading-first-developer-gig">First Developer Gig</h2>
<p><em>“Wait… first programming gig? But you didn't even like programming? What has changed in two years?”</em></p>
<p>One word: <strong>Minecraft</strong>.</p>
<p>Minecraft is an extraordinary game. Whether you like combat, adventure, building or whatever activity, you can find it in this game. I found… programming.</p>
<p>There are many big public servers where anyone can play mini-games on. These servers and mini-games are maintained by fully fledged teams of builders, supporters, moderators, and <strong>developers</strong>. I was very intrigued when I discovered an opening for the position of a developer on one of these servers. One of the most important requirements was Java.</p>
<p>I made myself on track learning this programming language. I didn't get the job. Fair to say, I haven't even tried. Instead, I discovered a group of Minecraft YouTubers who took part in an event. This event needed some plugins, so I contacted the organizer and started supplying them with features they wanted for their event. I was paid miserably compared to the work hours and sleepless nights I've been putting in. But I got the feeling of acceptance and my first programming gig, my teenage-self was seeking.</p>
<h2 id="heading-web-journey">Web Journey</h2>
<p>For two years, I've been making games here and there with the Unity engine, but nothing big. Then the famous lockdown happened, which overwhelmed me with spare time. I missed having fun with my classmates, so I decided to build a Discord Bot to play “The Werewolves of Millers Hollow” aka “Mafia” with them. Discord Bots can be coded in Python or JavaScript, the last one sounded very similar to Java. Even though developers often mention how different Java and JavaScript are, I found myself fairly comfortable with this new language.</p>
<p>Afterwards, I found out about HTML, CSS, and even later about frontend frameworks. Then I joined Twitter and Hashnode, so basically without the lockdown, I would've not written this article.</p>
<h2 id="heading-first-developer-job">First Developer Job</h2>
<p>I got my first developer job not that long ago, June 2022. Even though I received some interview proposals on Twitter, I typically felt underqualified, or I was lacking time because of my studies. It was LinkedIn that got me my first and current job. I work for a legal information service provider as a working student (part-time) which doesn't clash with my studies, so it felt like a perfect fit. I mostly fix bugs in the company's webshop and implement small features using React. My probationary period ends this month, so let's see what the future holds.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<ol>
<li><p><strong>It's rarely love at first sight</strong> — it took me some time to fall in love with coding. If it feels hard or boring, try to use another learning method or learn another programming language.</p>
</li>
<li><p><strong>Know your worth</strong> — I worked sleepless nights for barely any cash. Don't put hours of work into something for someone without getting a proper payout.</p>
</li>
<li><p><strong>Apply, apply, apply</strong> — I felt underqualified more than I should have. You will never meet all the requirements for a job opening. And that's a good thing, you'll learn something new as you go.</p>
</li>
</ol>
<hr />
<p>Thanks for reading! ❤️ This article is part of the <em>#4articles4weeks</em> challenge. If you want to be the first to see my next article, follow me on <a target="_blank" href="https://blog.ikirill.com">Hashnode</a> and on <a target="_blank" href="https://twitter.com/kirillinoz">Twitter</a>!</p>
]]></content:encoded></item><item><title><![CDATA[My First Experience with Three.js]]></title><description><![CDATA[I was interested in Three.js for a long time. It was back in February when I saw Bruno Simon's 3D portfolio, where you drift through his projects and other information on a car. You should check it out afterwards if you haven't. It's amazing. At that...]]></description><link>https://blog.kirillinoz.com/my-first-experience-with-threejs</link><guid isPermaLink="true">https://blog.kirillinoz.com/my-first-experience-with-threejs</guid><category><![CDATA[ThreeJS]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[3d]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[HTML5]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 28 Sep 2021 14:07:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284037787/79931c1b-82df-4302-8bdf-d89e79f02763.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was interested in Three.js for a long time. It was back in February when I saw <a target="_blank" href="https://bruno-simon.com/">Bruno Simon's 3D portfolio</a>, where you drift through his projects and other information on a car. You should check it out afterwards if you haven't. <strong>It's amazing</strong>. At that time, I just got into learning Vue and I wanted to concentrate on getting more familiar with it than to start learning a new framework.</p>
<p>So a few weeks ago, I finally had time to get into Three.js. But on its own, it looked very complicated. So I checked out <strong>Trois.js</strong>. It implements Three.js into a more familiar environment of Vue.js. After some experiments, I got into brainstorming ideas. I've never done a form before, so I decided to go with this idea, but using Three.js make it more interesting for people to fill out.</p>
<p>With the goal set, I warmed up to the idea of having a Mystery Box which would have a form element on each side and when you fill out one side it would go to the other, until you've completed it and the box would open revealing something interesting. I had to scrap this idea after understanding that it would be pretty difficult to project a 2D element on a 3D element in the way that would look good. But I decided to stick to this topic and here's what I did.</p>
<p>After using my favourite research buddy <em>Google</em>, I found a Lucky Block model on the internet which I decided to implement beside my form. And let the camera rotate around the z-axis of the block, but to make it more interesting, I added some variables which would multiply its rotating speed by looking at how many form fields were filled out.</p>
<pre><code class="lang-plaintext">&lt;template&gt;
&lt;GltfModel src="../../assets/LuckyBlock.glb" :visible="LBvisible" /&gt;
&lt;/template&gt;

&lt;script&gt;
data() {
    return {
        t: 0.2
    }
},
props: {
    LBvisible: Boolean,
    speed: Number
},
mounted() {
    setInterval(() =&gt; {
        this.t += this.speed * 0.01
    }, 10)
}
&lt;/script&gt;
</code></pre>
<p>To not waste any time designing a form, as the main target of the project was getting more familiar with Three.js, I found an existing form design on Figma (<a target="_blank" href="https://www.figma.com/community/file/992788557716887114">The Perfect Input Field Kit</a>) which I then implemented into the website.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1632751241345/IlAPruIX4.png" alt="One.png" /></p>
<p>Afterwards, I assigned an event (<em>keydown</em> for typing inputs and <em>change</em> for selection inputs) to each input element in the form. Like that, I could check if the field was filled or not and speed up the rotation of the block accordingly.</p>
<p>Now the only thing was to add a reward that would appear out of the block. I decided to go with a cake model I found on the web. I added a simple animation which would:</p>
<ul>
<li><p>Shrink the block (aka zoom out the camera)</p>
</li>
<li><p>Replace the block with the cake</p>
</li>
<li><p>Enlarge the block (aka zoom in the camera)</p>
</li>
</ul>
<p>Here's how it works:</p>
<pre><code class="lang-javascript">onSubmit() {
    <span class="hljs-keyword">const</span> zoomOutInterval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-comment">// Enlarge the distance between the object and the camera</span>
            <span class="hljs-built_in">this</span>.radius += <span class="hljs-number">4</span>
    }, <span class="hljs-number">10</span>)

    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">clearInterval</span>(zoomOutInterval)
        <span class="hljs-comment">// Set the rotation speed of the camera back to normal</span>
        <span class="hljs-built_in">this</span>.speed = <span class="hljs-number">1</span>
        <span class="hljs-comment">// Hide the Lucky Block</span>
        <span class="hljs-built_in">this</span>.LBvisible = <span class="hljs-literal">false</span>
        zoomIn()
    }, <span class="hljs-number">1000</span>)

    <span class="hljs-keyword">const</span> zoomIn = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> zoomInInterval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
             <span class="hljs-comment">// Shrink the distance between the object and the camera</span>
            <span class="hljs-built_in">this</span>.radius -= <span class="hljs-number">4</span>
        }, <span class="hljs-number">9</span>)

        <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-built_in">clearInterval</span>(zoomInInterval)
            <span class="hljs-comment">// Correct the distance</span>
            <span class="hljs-built_in">this</span>.radius = <span class="hljs-number">13</span>
        }, <span class="hljs-number">1000</span>)
    }
},
</code></pre>
<p>Besides the block, the form disappears as well. Instead, you can see a little message which thanks the user for filling out the form, nothing extraordinary.</p>
<p>I still have some issues with the loading time for my models, which take approximately 2-3 minutes (differentiates upon the internet connection) to load. So besides the link to the website, I'll also leave a little clip which shows all the possible interactions with this website, so you don't have to wait an eternity. 😅</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/aYQQ_VDS7tI">https://youtu.be/aYQQ_VDS7tI</a></div>
<p> </p>
<blockquote>
<p>You can check out the website here 👉 <a target="_blank" href="luckyform.netlify.app">Lucky Form</a></p>
<p>You can check out the code here 👉 <a target="_blank" href="https://github.com/kirillinoz/lucky-form-threejs">Lucky Form (Code)</a></p>
</blockquote>
<p>Finally, I just want to add that I really like this technology and I see myself using it in the future, especially for projects where 3D models are necessary. Although this technology is still fairly new and there are not a lot of good tutorials on the internet, the <a target="_blank" href="https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene">documentation</a> is clear and helpful.</p>
<p>Thanks for reading! ♥</p>
<hr />
<p>If you like this article, consider following me on <a target="_blank" href="https://twitter.com/Inkuantum">Twitter</a>. I post tips around web development and progress on my projects. If you have any questions, my DMs on Twitter are always open.</p>
<p><a target="_blank" href="https://www.buymeacoffee.com/inkuantum"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626339263324/PrLP627fE.png" alt="buymecookie.png" /></a></p>
]]></content:encoded></item><item><title><![CDATA[My 100 Days of Code in a Nutshell]]></title><description><![CDATA[At the end of November 2020, I became more active on Twitter. I started to follow more content creators and just great people who gave me insides and tips about a fairly new domain for me called web development. This was the time when I also found th...]]></description><link>https://blog.kirillinoz.com/100daysofcode</link><guid isPermaLink="true">https://blog.kirillinoz.com/100daysofcode</guid><category><![CDATA[HTML5]]></category><category><![CDATA[CSS]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[100DaysOfCode]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Thu, 15 Jul 2021 13:11:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735284014246/7453e8eb-1c2d-4196-a273-c19493bda762.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the end of November 2020, I became more active on Twitter. I started to follow more content creators and just great people who gave me insides and tips about a fairly new domain for me called web development. This was the time when I also found the 100DaysOfCode challenge. Although I saw a lot of great projects people were working on as part of this challenge, I still didn't want to participate in it. The main excuse was lack of time, which was caused by school (so I thought at that time). But with a new lockdown announced at the start of December and a new course by <a target="_blank" href="https://twitter.com/florinpop1705">Florin Pop</a> and <a target="_blank" href="https://twitter.com/traversymedia">Brad Traversy</a> all the stars aligned for me to start with this challenge.</p>
<p>So this is “My 100 Days of Code in a Nutshell 🥜”.</p>
<h4 id="heading-from-day-1-to-day-50">From Day 1 to Day 50</h4>
<p>The challenge began on December 4th when I started with the course “50ProjectsIn50Days”. This course is made by the people I mentioned above and is oriented to practising the fundamentals of HTML, CSS, and JavaScript. I found this very useful as I was lacking experience, especially in the JavaScript sector. Another beauty of this course was its perfect match to the 100DaysOfCode challenge, as I wouldn't need to plan anything for the first half of my challenge.</p>
<p>So that's what I did. I followed this course and worked every day on a new project. Most of the projects took me about 20-30 minutes to complete. But I didn't stop there…</p>
<p>Another blessing of this course was the creative part. Because the projects are so small, you have enough possibilities and time to customize your projects. I took another 20-30 minutes for customizations. I tried to keep them simple but also useful in the context of the current project. Sometimes it was a design change, other times an additional functionality, I tried everything.</p>
<p>After finishing this course, I was confident enough to try out a framework. I chose Vue.</p>
<h4 id="heading-from-day-51-to-day-61">From Day 51 to Day 61</h4>
<p>To be honest, it wasn't my first encounter with Vue. But it was the first time that I was building a fully fledged web application with this framework.</p>
<p>I decided to build a weather application, as I wanted to try to incorporate some kind of Rest API into my app. To make it more interesting and different from other weather applications, I saw out there, I decided to use glassmorphism as the main design theme. I read a lot about it in the past and found some great websites which made it easier to create glass panels with CSS.</p>
<p>I didn't have any big problems building it and in just 10 days of code I finished it.</p>
<blockquote>
<p>If you want to find out more about glassmorphism and why you should use it, check out my blog post 👉 <a target="_blank" href="https://inkuantum.hashnode.dev/glassmorphism-whats-so-interesting-about-it">Glassmorphism - what's so interesting about it?</a></p>
</blockquote>
<h4 id="heading-from-day-62-to-day-68">From Day 62 to Day 68</h4>
<p>The next project I wanted to do was an e-commerce type of application. As product, I decided to go with fast food like sandwiches and additional drinks, deserts etc. The project consists of a landing page, menu, and sandwich builder.</p>
<p>To make the sandwich builder more authentic and realistic, I went on the Subway page and looked up ingredients they use in their sandwiches. When constructing your perfect sandwich, you're limited on how many ingredients you're able to choose. This is because a lot of companies do that to not run into any financial issues because the sandwich price stays consistent. The menu is split into two parts, the menu itself and a basket where you can see what products you have already selected.</p>
<p>This was pretty much it. In the end, I never published the project as I didn't like how the design turned out.</p>
<h4 id="heading-from-day-69-to-day-71">From Day 69 to Day 71</h4>
<p>After the blow with Empare (that's what the last project was called), I decided to uplift my UI and CSS skills. I read more about design and followed more accounts which posted about it on Twitter. For a long time, I've been following <a target="_blank" href="https://twitter.com/Prathkum">Pratham</a> and <a target="_blank" href="https://twitter.com/jh3yy">Jay</a> on Twitter, who post beautiful CSS art. That's when I decided to try it out myself.</p>
<p>There is not a lot to say about it except that it seemed more difficult than it actually was. Then I wrote a full breakdown of this project and published it on Hashnode and DevTo. It was my first blog post that really took off and received a lot of attention.</p>
<blockquote>
<p>If you want to find out more about this project, check out my blog post 👉 <a target="_blank" href="https://inkuantum.hashnode.dev/ukulele-with-css">Ukulele with CSS</a></p>
</blockquote>
<h4 id="heading-from-day-72-to-day-90">From Day 72 to Day 90</h4>
<p>Besides ukulele, for which I created a CSS art, I have another passion which is football. For a long time, I wanted to create <a target="_blank" href="http://www.higherlowergame.com/">The Higher Lower Game</a> clone, but with footballers and their goals. The problem then was that I didn't know how to get the information (such as footballer's names, goals, etc.) and how to set up the backend. At this point of time, I already knew how to get the first one done and that's what I've directly taken care of as well as the frontend. The backend was completely new to me, so I decided to take <a target="_blank" href="https://www.udemy.com/course/the-complete-nodejs-developer-course-2/">The Complete Node.js Developer Course</a> on the side while working on the other things for this project. I learned about Node.js and Express that allowed me to finish this project.</p>
<blockquote>
<p>If you want to find out how I got the information on footballers for this project, check out my blog post 👉 <a target="_blank" href="https://inkuantum.hashnode.dev/web-scraping-with-puppeteer">Web Scraping with Puppeteer</a></p>
</blockquote>
<h4 id="heading-from-day-91-to-day-100">From Day 91 to Day 100</h4>
<p>The last but not least is my current project - Circlist. It's a web app that allows you to gather people, you interact the most on Twitter with, in one or multiple lists. This isn't my first take on the Twitter API, but surely the biggest. The project nears its first beta-testing and likewise its final release. I already finished the application. The last thing to do is the landing page, which will take some time to design and then to build. So certainly not one I could complete in the last 10 days and which goes beyond this challenge.</p>
<p>So that was “My 100 Days of Code in a Nutshell 🥜”. Thanks for reading and have an amazing day! ♥</p>
<h4 id="heading-where-can-i-find-these-projects">Where can I find these projects?</h4>
<p>50Projects50Days | <a target="_blank" href="https://github.com/kirillinoz/50-projects-in-50-days">GitHub</a></p>
<p>Holo Weather | <a target="_blank" href="https://holo-weather.netlify.app/">Website</a> &amp; <a target="_blank" href="https://github.com/kirillinoz/holo-weather">GitHub</a></p>
<p>Empare | Never published</p>
<p>Ukulele | <a target="_blank" href="https://ukulele-css.netlify.app/">Website</a>, <a target="_blank" href="https://codepen.io/inkuantum/pen/XWpKNOz">CodePen</a> &amp; <a target="_blank" href="https://github.com/kirillinoz/ukulele-css">GitHub</a></p>
<p>FootGuess | <a target="_blank" href="https://soccerstatues.com/">Website</a></p>
<p>Circlist | Never published</p>
<hr />
<p>In case you want to try out 100DaysOfCode for yourself, you'll find out more information on the following website (https://www.100daysofcode.com/).</p>
<p>If you liked this article, consider following me on <a target="_blank" href="https://twitter.com/kirillinoz">Twitter</a>. I post tips around web development and progress on my projects. If you have any questions, my DMs on Twitter are always open.</p>
]]></content:encoded></item><item><title><![CDATA[Mario with CSS]]></title><description><![CDATA[Without doubt, one of the most iconic characters in video game history is Mario. First he appeared in Donkey Kong (1981) before receiving his own game just four years later. This game became a massive success, so I'm pretty sure that you've seen, hea...]]></description><link>https://blog.kirillinoz.com/mario-with-css</link><guid isPermaLink="true">https://blog.kirillinoz.com/mario-with-css</guid><category><![CDATA[HTML5]]></category><category><![CDATA[CSS]]></category><category><![CDATA[CSS Animation]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 29 Jun 2021 13:04:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735283987177/b427d874-b293-4831-ab21-dcf9c33464bd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Without doubt, one of the most iconic characters in video game history is Mario. First he appeared in Donkey Kong (1981) before receiving his own game just four years later. This game became a massive success, so I'm pretty sure that you've seen, heard or even played it. Although it came out a long time before I was born, I still played it a couple of times. And is there any better way to pay homage to a legendary game than creating a CSS art for it?</p>
<h4 id="heading-preparation">Preparation</h4>
<p>First, I came up with the script for the animation.</p>
<blockquote>
<p>Mario is standing on a platform. He's approached by Goomba. When Goomba touches him, Mario dies in his iconic way (move up and then drop down). Whole animation loops and starts from the beginning.</p>
</blockquote>
<p>As with my last CSS art, I had to find a template to orientate myself. Last time it was my ukulele. I don't have any Mario merchandise lying around, so I went on the internet. I found original sprites of Mario's animation, so I decided to use these as well as sprites for Goomba's animation.</p>
<p>Now it was clear, I had to create two images of Goomba, two images of Mario as well as an image of a platform, so five images in total.</p>
<h4 id="heading-pixel-art">Pixel Art</h4>
<p>The game which comes from 1985 isn't unexpected to be made using pixel art, so I went for this approach for my CSS art as well. The best way to create pixel art in CSS is using box shadows. Although it's the best choice, it's not the easiest one if you have to do it by hand. You'll need to fill out box shadows for every pixel and filling out 16x16 fields could be pretty intimidating and boring.</p>
<p>Luckily, there is a great website called <a target="_blank" href="https://www.pixelartcss.com/">Pixel Art to CSS</a> which takes all the dirty work away and only leaves you with the creative process.</p>
<p>After drawing the characters and frames for their animation, I exported them as key frames into the project. I tried to do the same with the ground, but its size in comparison to the characters was too big, so I went with key frames for the ground as well. But because it had only one frame, it didn't make any difference visually. The scale was now fixed, so I counted that as a win.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1624719486042/ay0egqHsE.png" alt="sprites.png" /></p>
<h4 id="heading-placement-and-animation">Placement and Animation</h4>
<p>Now the only thing left to do was placing our elements and animating them. For every element, I created a div to which I assigned the specific CSS class. I placed them on the screen like this using absolute position, as it was the easiest choice in this situation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1624720280294/mMyYxHiKN.png" alt="animation.png" /></p>
<p>Last but not least are walking animation for Goomba and death animation for Mario. Using key frames, I mapped two points between which Goomba is going to walk (start of the platform and end of the platform). While in between, I timed the point when Goomba slightly touches Mario and that's when Mario's death animation should begin. To create the animation as well as to time it properly, I broke down the animation into three parts.</p>
<ul>
<li><p>Part One: Mario flies up</p>
</li>
<li><p>Part Two: Mario falls down</p>
</li>
<li><p>Part Three: Mario goes back up</p>
</li>
</ul>
<p>That's pretty much it. From this point the animation loops back to beginning and as everyone knows a loop in an animation makes everything two times better.</p>
<p>You can find the whole animation and code on the dedicated <a target="_blank" href="https://codepen.io/inkuantum/pen/XWRrpMd">CodePen page</a> for this project.</p>
<p>Thanks for reading and have a great day! ♥</p>
<hr />
<p>If you liked this article, consider following me on <a target="_blank" href="https://twitter.com/Inkuantum">Twitter</a>. I post tips around web development and progress on my projects. If you have any questions, my DMs on Twitter are always open.</p>
<p><a target="_blank" href="https://www.buymeacoffee.com/inkuantum"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621926138327/Rpn-M4czP.png" alt="scrnli_25-05-2021_08-56-16.png" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Web Scraping with Puppeteer]]></title><description><![CDATA[Internet is a wide place full of information. Here you can find everything from cute kitten videos up to scientific researches. This information isn't only useful to us, but it could become vital for our websites and applications.
There are a few way...]]></description><link>https://blog.kirillinoz.com/web-scraping-with-puppeteer</link><guid isPermaLink="true">https://blog.kirillinoz.com/web-scraping-with-puppeteer</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[data]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Tue, 25 May 2021 13:01:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735283962357/6ada3113-5e5c-4899-9d8c-7c8f20b72ade.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Internet is a wide place full of information. Here you can find everything from cute kitten videos up to scientific researches. This information isn't only useful to us, but it could become vital for our websites and applications.</p>
<p>There are a few ways to access the necessary data, Rest APIs, public databases and web scraping. Puppeteer is an awesome tool for getting the last one done. In this post, I want to help you discover this tool for yourself and show you what it's capable of.</p>
<p>Let's get the first question out of the way.</p>
<h3 id="heading-what-is-puppeteer">What is Puppeteer?</h3>
<blockquote>
<p>Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol.</p>
</blockquote>
<p>That's what the official documentation says. Complicated isn't it? In simpler terms, we're dealing with an invisible browser which is controlled via code.</p>
<h3 id="heading-show-me-the-way">Show me the way!</h3>
<p>After installing the Puppeteer NPM package, you'll have to write some code and show Puppeteer where and which information you would like to get.</p>
<p>Note that most of Puppeteer's methods are asynchronous, so you'll have to use an <strong>async function</strong> or <strong>then method</strong>. For upcoming examples, I'll stick to the first one.</p>
<h3 id="heading-lets-get-some-data">Let's get some data!</h3>
<p>I think that the best method to learn something new is by practicing. So let's take an example for web scraping with Puppeteer and break it down. I'll try to explain every step as best as I can. But if you're stuck or you want to find out more about a specific method, I encourage you to check out the official <a target="_blank" href="https://pptr.dev/">documentation</a>.</p>
<p>For starters, we need to check if the website allows web scraping at all. For this, we will check if the website contains a <strong>robot.txt</strong> file. If not, we are good to go. Otherwise, you'll see which restrictions are placed by the owner of the website.</p>
<p>In this example we will be scraping some data from the official <a target="_blank" href="https://www.premierleague.com/stats/top/players/goals">English Premier League</a> website, to be more precise, a table of football players and their total amount of goals per season.</p>
<p>With the following code we will launch our browser with a predefined viewport. And navigate to the website while leaving some time for all components to load at the end of the code.</p>
<blockquote>
<p>Note: websites may differ according to the client's viewport that's why it's important to predefine it.</p>
</blockquote>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> viewport = {<span class="hljs-string">'defaultViewport'</span> : { <span class="hljs-string">'width'</span> : <span class="hljs-number">1430</span>, <span class="hljs-string">'height'</span> : <span class="hljs-number">979</span> }}
<span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch(viewport)
<span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage()
<span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'https://www.premierleague.com/stats/top/players/goals'</span>)
<span class="hljs-keyword">await</span> page.waitForTimeout(<span class="hljs-number">3000</span>)
</code></pre>
<p>Now let's scrape the table we're seeing on the website. For that, we will use <strong>$$eval</strong> to find all the table components using HTML elements as the selector and then getting the <strong>innerText</strong> in each of the selected elements.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//'tbody tr td' - selector</span>
<span class="hljs-keyword">let</span> tableEls = <span class="hljs-keyword">await</span> page.$$eval(<span class="hljs-string">'tbody tr td'</span>, <span class="hljs-function"><span class="hljs-params">tds</span> =&gt;</span> tds.map(<span class="hljs-function"><span class="hljs-params">td</span> =&gt;</span> {
    <span class="hljs-keyword">return</span> td.innerText
}))
</code></pre>
<p>Now we just have an array filled up with all the strings that we could find in the table. We have empty strings and unnecessary spacing. We should also split this array into smaller arrays containing the data for each individual player and then create an object out of each array to make it easier to read and access for the front-end.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//Modifications</span>
tableEls = tableEls.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el.trim())
tableEls = tableEls.filter(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el)
tableEls = chunkArray(tableEls, <span class="hljs-number">5</span>)
tableEls = tableEls.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el.slice(<span class="hljs-number">1</span>))
<span class="hljs-comment">//Final Arr To Save</span>
<span class="hljs-keyword">let</span> seasonData = []
<span class="hljs-comment">//Create an object</span>
tableEls.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> {
    <span class="hljs-keyword">let</span> obj = {
        <span class="hljs-attr">id</span>: nextID,
        <span class="hljs-attr">name</span>:  el[<span class="hljs-number">0</span>],
        <span class="hljs-attr">team</span>: el[<span class="hljs-number">1</span>],
        <span class="hljs-attr">country</span>: el[<span class="hljs-number">2</span>],
        <span class="hljs-attr">goals</span>: +el[<span class="hljs-number">3</span>]
    }
    nextID++
    seasonData.push(obj)
})

<span class="hljs-comment">// Close the browser at the end of your session</span>
<span class="hljs-keyword">await</span> browser.close()
</code></pre>
<h3 id="heading-lets-get-some-images">Let's get some images!</h3>
<p>Now that we've collected our data about each and every footballer in the table, we could also use some images of each football player. The website, we were currently on, doesn't provide us with any photos, so let's start a new session and go to the well-known website which finds billions of photos across the whole internet. I'm talking about <strong>Google Images</strong>.</p>
<p>First, we start a new session and open a new page. Then we use the array <strong>seasonData</strong> to get the name of each footballer. The name needs some adjustment because we will pass it into an URL and any space has to be replaced with <strong>+</strong>. Afterwards we will need to select the first image on the page. This can be done using <strong>XPath</strong> which gives the shortest unique path to every element on a website. Then we select our element and get the image URL. In the end, we should add it as a property to our player object.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">for</span>(<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; seasonData.length; i++) {
    <span class="hljs-keyword">let</span> obj = seasonData[i]
    <span class="hljs-keyword">const</span> search = obj.name.replace(<span class="hljs-regexp">/\//g</span>, <span class="hljs-string">'+'</span>)
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">`https://www.google.com/search?tbm=isch&amp;q=<span class="hljs-subst">${search}</span>`</span>)

    <span class="hljs-keyword">const</span> imageSelect = <span class="hljs-string">'//*[@id="islrg"]/div[1]/div[1]/a[1]/div[1]/img'</span>;
    <span class="hljs-keyword">await</span> page.waitForXPath(imageSelect)
    <span class="hljs-keyword">const</span> image = <span class="hljs-keyword">await</span> page.$x(imageSelect)
    <span class="hljs-keyword">let</span> imageSrc = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function"><span class="hljs-params">img</span> =&gt;</span> img.src, image[<span class="hljs-number">0</span>])

    obj.imageURL = imageSrc
}
</code></pre>
<p>Now using Node's file system we can save our <strong>seasonData</strong> array to a JSON file, pass the data to the front-end or create a Rest API.</p>
<h3 id="heading-more-possibilities">More possibilities</h3>
<p>This example is just the tip of the iceberg. You can do other things with the Puppeteer library such as interact with elements, take screenshots and more. If you want to find out more about it, check out the official <a target="_blank" href="https://pptr.dev/">documentation</a>.</p>
<p>I hope I could waken interest in you to learn more about this awesome JavaScript library.</p>
<p>Thank you for your time! ❤</p>
<hr />
<p>If you liked this article, consider following me on <a target="_blank" href="https://twitter.com/Inkuantum">Twitter</a>. I post tips around web development and progress on my projects. If you have any questions, my DMs on Twitter are always open.</p>
<p><a target="_blank" href="https://www.buymeacoffee.com/inkuantum"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1621926138327/Rpn-M4czP.png" alt="scrnli_25-05-2021_08-56-16.png" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Ukulele with CSS]]></title><description><![CDATA[If you follow me on Twitter, you would probably know that I'm passionately learning to play ukulele. I always loved to listen to music but I was never interested in learning to play an instrument or even thinking of creating my own music. But that ch...]]></description><link>https://blog.kirillinoz.com/ukulele-with-css</link><guid isPermaLink="true">https://blog.kirillinoz.com/ukulele-with-css</guid><category><![CDATA[HTML]]></category><category><![CDATA[CSS]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Thu, 08 Apr 2021 11:34:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735283931031/5e08cdba-b42f-4f14-a0a6-1c96956597ed.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you follow me on Twitter, you would probably know that I'm passionately learning to play ukulele. I always loved to listen to music but I was never interested in learning to play an instrument or even thinking of creating my own music. But that changed in the recent years and now I'm a proud owner of an ukulele.</p>
<p>Another thing that I really wanted to try out for a long time is CSS art. I saw a lot of beautiful art pieces on Twitter and they really inspired me to create my own. That's how I combined programming and music in this art piece.</p>
<h1 id="heading-html-amp-css">HTML &amp; CSS</h1>
<h3 id="heading-body">Body</h3>
<p>First I created the body of the ukulele using a div and pseudo-elements (after and before).</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/95l689vbtknv8xyx15j4.png" alt="Alt Text" /></p>
<h3 id="heading-bridge-amp-saddle">Bridge &amp; Saddle</h3>
<p>Before adding the neck, I've added a bridge at the bottom of the body. Using a pseudo-element I also added a saddle on top of the bridge where the strings are going to end.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1dtfog0pw17kn8lc8r7m.png" alt="Alt Text" /></p>
<h3 id="heading-neck">Neck</h3>
<p>Up next we have the neck of ukulele. It's a simple rectangular div.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2qsoks4sj6ud5o41v9ao.png" alt="Alt Text" /></p>
<h3 id="heading-headstock">Headstock</h3>
<p>After creating the neck it was time to put the headstock on top. In this case the pseudo elements are just purple circles which give ukulele's headstock an interesting shape.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aa62mq8r73776mp699ps.png" alt="Alt Text" /></p>
<h3 id="heading-tuning-pegs">Tuning pegs</h3>
<p>Now it was time to add the other end called turning pegs where the strings are going to be attached. I decided to split them into two rows with a pair in each row. Like that I could position them relative to each other.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7fkdb9ox67hdqfo6gvz.png" alt="Alt Text" /></p>
<h3 id="heading-turners">Turners</h3>
<p>With the same method as mentioned before, I added the turners themselves.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eoa66t0ksl4gzrr5c8wv.png" alt="Alt Text" /></p>
<h3 id="heading-strings">Strings</h3>
<p>Now let's add the most important part the strings. Again positioned relative to each other but absolute in the global container. These are positioned exactly between the saddle and the nut.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fd6mw7wz8tphsni8n4j.png" alt="Alt Text" /></p>
<h3 id="heading-nut">Nut</h3>
<p>Not a lot to say about the nut, just a div positioned absolute in the global container.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/di94doec40g4ve6obb6m.png" alt="Alt Text" /></p>
<h3 id="heading-rest-of-the-strings">Rest of the strings</h3>
<p>The part that took me the longest, I reckon. These are placed in a separate container. In this container they are placed absolute and under a specific angle, so they barely touch the pegs.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spwzxkfdau5vwmod0mc5.png" alt="Alt Text" /></p>
<h3 id="heading-frets">Frets</h3>
<p>A lot of divs positioned relative to each other and absolute in the global container.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0aacf9fn8eu7mkfzxw14.png" alt="Alt Text" /></p>
<h1 id="heading-animation-and-audio">Animation and Audio</h1>
<p>Now let's bring the whole thing to life with some animations and sounds. At first I wanted to make the strings realistic that means the animation would start at the point of interaction. But that would be too much effort for a project like this. So I went with a very simple animation in which the string would just move from side to side but get less momentum.</p>
<p>For the sounds I took my ukulele and my phone. I recorded each string separately and assigned each audio to each string.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6x6yida5s3g3urfi7yow.png" alt="Alt Text" /></p>
<p>If you want to try out the audio and animation, I made a separate <a target="_blank" href="http://ukulele-css.netlify.app/">website</a> for this CSS art (desktop only).</p>
<p>If you want to use this art (with appropriate reference 😉), feel free to check it out on <a target="_blank" href="https://codepen.io/inkuantum/pen/XWpKNOz">CodePen</a>.</p>
<p>Thank you for your time! ❤</p>
<h4 id="heading-if-you-liked-this-article-consider-following-me-on-twitterhttpstwittercominkuantum-i-post-tips-and-progress-on-my-projects-such-as-this-one-if-you-have-any-questions-my-dms-on-twitter-are-always-open">If you liked this article, consider following me on <a target="_blank" href="https://twitter.com/Inkuantum">Twitter</a>. I post tips and progress on my projects such as this one. If you have any questions, my DMs on Twitter are always open.</h4>
]]></content:encoded></item><item><title><![CDATA[Glassmorphism]]></title><description><![CDATA[Design isn't something that stays the same forever. Old trends go, new trends replace them. It's constantly evolving.
A relatively new trend on the web is Glassmorphism. You can imagine it as taking a glass panel and putting it in front of a backgrou...]]></description><link>https://blog.kirillinoz.com/glassmorphism</link><guid isPermaLink="true">https://blog.kirillinoz.com/glassmorphism</guid><category><![CDATA[webdev]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Sun, 28 Feb 2021 14:35:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735283899743/0c1d5899-8043-4aba-beed-333365c98eff.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Design isn't something that stays the same forever. Old trends go, new trends replace them. It's constantly evolving.</p>
<p>A relatively new trend on the web is <strong>Glassmorphism</strong>. You can imagine it as taking a glass panel and putting it in front of a background. Give the glass a little bit of colour and a blur effect which result in the background changing completely when you're looking through the glass piece. Now add the information that you want to transmit to your user on top and voilà, you've got yourself a pretty looking glassmorphism UI.</p>
<p>Is it all about pretty looking? Or is there more to it? Let's find out.</p>
<h3 id="heading-1-minimalist-design">1) Minimalist design 🐁</h3>
<p>Minimalist design took over a while ago. We don't stuff the websites full of information anymore, instead we build in more white space, add more margin and sprinkle in some padding. This way the user doesn't feel attacked or pressured with information which could be very overwhelming.</p>
<p>Glassmorphism gives you the possibility to do the same by spreading your information on a glass pane, leaving more space in between to see through the glass into an interesting background. </p>
<h3 id="heading-2-focus-users-attention">2) Focus user's attention 🔎</h3>
<p>By spreading your information on the glass pane, it would differentiate itself from your other content. So it can easily catch user's attention.</p>
<p>The information on the glass pane would stand out even more because of the blur the background receives. The text, images or icons look even more sharpen and clear on a blurred background that's why they would be more attractive for the user than on a plain background.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfkyft5moj38wclhyhd3.png" alt="Alt Text" /></p>
<h3 id="heading-3-wow-effect">3) Wow effect ✨</h3>
<p>Although we talked about some interesting points before which help you provide the information to the user in the best possible way, "pretty looking" could also bring its value to the table. Would a simple circle attract your attention? Probably not, but what about a circle that slowly fades away? Probably yes...</p>
<p>That's why geometric forms are very beloved by glassmorphism enthusiasts. They are very strict but when you put them behind a glass pane they fade away. You can often see a part of a triangle behind the glass pane and a part stretching out of it. This gives the whole thing an antithesis of strict and lenient which gives it this <strong>Wow effect</strong> ✨.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltgfzr9b0wgk1w48mbsy.png" alt="Alt Text" /></p>
<h3 id="heading-how-to-create-a-glassmorphism-design">How to create a glassmorphism design?</h3>
<p>I could share a code snippet right here of the CSS. But there are plenty of websites which help you generate a glassmorphism design which suits you the best. Here are some resources:</p>
<ul>
<li><a target="_blank" href="https://glassmorphism.com/">Glassmorphism CSS Generator</a></li>
<li><a target="_blank" href="https://mdbootstrap.com/docs/standard/tools/design/masks/">CSS Masks &amp; Glassmorphism Generator</a></li>
</ul>
<h6 id="heading-ls0tls0tls0tls0t">------------</h6>
<h6 id="heading-the-images-are-from-my-latest-project-holo-weatherhttpsholo-weathernetlifyapp">The images are from my latest project 👉 <a target="_blank" href="https://holo-weather.netlify.app/">Holo Weather</a></h6>
]]></content:encoded></item><item><title><![CDATA[Dynamic Twitter Banner]]></title><description><![CDATA[As you might know, Twitter has a large developer community which often makes use of a powerful tool Twitter is providing them with. I'm talking about Twitter API. Just by browsing through Twitter you can find a ton of bots and services which make use...]]></description><link>https://blog.kirillinoz.com/dynamic-twitter-banner</link><guid isPermaLink="true">https://blog.kirillinoz.com/dynamic-twitter-banner</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Twitter]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Kirill Inoz]]></dc:creator><pubDate>Sun, 31 Jan 2021 19:54:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735283841726/09d341b1-bf2b-4270-8490-e24dff5bf88e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[
<p>As you might know, Twitter has a large developer community which often makes use of a powerful tool Twitter is providing them with. I'm talking about Twitter API. Just by browsing through Twitter you can find a ton of bots and services which make use of this API. Another popular use-case that you can encounter are follower counts. They can be integrated into name, bio or banner of your profile. But they often look identical and boring - just some plain numbers.</p>
<p>I was very interested in this type of project but I wanted to do something different, something more interesting.</p>
<p>That's when I came up with the idea of a moving object which could represent my followers count. By adding some details and a story to it we get:</p>
<blockquote>
<p>"Gary the Snail who is hungry and wants to get to some food at the end of the road."</p>
</blockquote>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/i/9iroybv5pog6yahs6zet.jpg" alt="Alt Text" /></p>
<p><em>So... how did I do it? How can you make your own version and what tools do you need?</em></p>
<h3 id="heading-here-are-the-5-main-steps">Here are the 5 main steps:</h3>
<p>1) Get the tokens for your app on <a target="_blank" href="https://developer.twitter.com/">Twitter's developer page</a>.</p>
<p>2) Use a library/add-on/module for Twitter integration. I've used <a target="_blank" href="https://www.npmjs.com/package/twitter-lite">Twitter Lite</a> for NodeJS. But there are also other possibilities.</p>
<ul>
<li>Setup the authentication and check for the available endpoints (<a target="_blank" href="https://developer.twitter.com/en/docs/api-reference-index">docs</a>).</li>
</ul>
<p>3) Create your design for the banner using photo-editing software, I used Photoshop but you can use paintNET, GIMP, etc. Don't forget to subdivide your design into different layers and export them separately.</p>
<p><em>Example:</em></p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/i/8yb3h01lpw65ls5ch24p.png" alt="Different Parts" /></p>
<p>4) Now it's time to get the followers count, create a single image out of the three above and update the profile banner:</p>
<ul>
<li>Fetch the follower count using one of the two APIs: <a target="_blank" href="https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids">GET followers/ids</a> or <a target="_blank" href="https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/follow-search-get-users/api-reference/get-users-show">GET users/show</a></li>
<li>Use <a target="_blank" href="https://www.npmjs.com/package/merge-images">Merge Images</a> for NodeJS to merge the images in the right place. You'll probably need the scale function to map the moving object on top of the background. Here is a JavaScript example:
<img src="https://dev-to-uploads.s3.amazonaws.com/i/j5lb33k6xiyxr55vcxsu.png" alt="Code Snippet" /></li>
<li>To update the profile banner you should use <a target="_blank" href="https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner">POST account/update_profile_banner</a> by sending the new Banner as a base64 format.</li>
</ul>
<p>5) Using a cron job you can repeat all the steps above in a specific duration you like.</p>
<p>That's how I integrated a followers count in my profile banner. If you want to see it in action, you can check my <a target="_blank" href="https://twitter.com/Inkuantum">Twitter profile</a>.</p>
<p>If you're stuck or you want to use exactly the same version, I uploaded the source code to my <a target="_blank" href="https://github.com/Inkuantum">GitHub page</a>.</p>
<p>This project was inspired by <a target="_blank" href="https://dev.to/radnerus/twitter-api-is-followers-count-mda">Suren's post</a>.</p>
]]></content:encoded></item></channel></rss>