<?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[Loving The Boulder]]></title><description><![CDATA[Hello! My name is Peyi. I'm a diverse budding, skilled Frontend Software engineer with 4+ years in UX design . LovingTheBoulder features my journey towards tran]]></description><link>https://lovingtheboulder.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1698823842589/jIBwlKjI4.png</url><title>Loving The Boulder</title><link>https://lovingtheboulder.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 30 Aug 2026 21:07:16 GMT</lastBuildDate><atom:link href="https://lovingtheboulder.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Exploring Dark Mode, Reusable Vue UI Components 🌚🌞]]></title><description><![CDATA[Greetings once more! Imagine this: two birds, one stone. That's what I accomplished recently:

Crafting a sleek one-page website for a health insurance company, Kiaret.

Taking my first strides into the world of Vue.


Now, the first task was a bit l...]]></description><link>https://lovingtheboulder.hashnode.dev/exploring-dark-mode-reusable-vue-ui-components</link><guid isPermaLink="true">https://lovingtheboulder.hashnode.dev/exploring-dark-mode-reusable-vue-ui-components</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[dark mode]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[composition api]]></category><dc:creator><![CDATA[Oluwapeyibomi Oyelohunnu]]></dc:creator><pubDate>Sat, 04 Nov 2023 22:40:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699138364358/7e4ebd92-ec32-4478-959b-12df4416d1ef.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699138328746/1925804b-9294-467d-8054-5f7cc370a34b.jpeg" alt class="image--center mx-auto" /></p>
<p>Greetings once more! Imagine this: two birds, one stone. That's what I accomplished recently:</p>
<ol>
<li><p>Crafting a sleek one-page website for a health insurance company, Kiaret.</p>
</li>
<li><p>Taking my first strides into the world of Vue.</p>
</li>
</ol>
<p>Now, the first task was a bit like setting sail with a treasure map that had only two clues: the company’s name and the company’s objectives. As for the second mission, it was part of my grand attempt to shift from being a Senior Product Designer to a fledgling Front End Developer. Since I'd already danced with React in a previous project, I figured I should also groove with Vue. It was like trying to learn a new strategy while trying to speed-run a new game.</p>
<p>This blog is your backstage pass to the creation of that one-page wonder – a website complete with dark mode and a basket full of reusable UI components. Let's dive into this intriguing journey!</p>
<h3 id="heading-creating-a-simple-dark-ready-design-system">Creating a Simple Dark-Ready Design System</h3>
<p><img src="https://lh7-us.googleusercontent.com/pUKLcziH0kHjsSHImrCzHt8AIe8W_CLBmKQF67BHK35eVkdcydaCQirwi3-5Pq_DUSGyUaGdjZugVFoV0S8m8ik2v7IamgqndW5iqUeC_HPvws11peC2lqHoLenI43uXABH6gcLiBS3Uj0wY77F2N4E" alt /></p>
<p>Creating the design system was an exciting journey that helped me address critical questions about fonts, elements, and assets. You can explore the design system's wild ride in this <a target="_blank" href="https://www.figma.com/file/4RXo3KvFDVJz1fFaxNoVYP/Kiaret?type=design&amp;node-id=0%3A1&amp;mode=design&amp;t=HszWoiRGi1bSsaHQ-1">Figma file</a>.</p>
<p>I hopped on board the Golden Ratio train to create a font guide tailored for both web and mobile systems. This involved making decisions about font sizes, weights, and line heights to elevate typography and readability. Implementing these font guides gave me a newfound appreciation for the frontend devs I've collaborated with in the past 😅.</p>
<p><img src="https://lh7-us.googleusercontent.com/JzwK3syyCsf7xyidU3kEIeoCqEy9KN-CYbOzDqZjql5tQts9epIuS4YkF31VSuecR_5zOySreUKRUSxewCD-gkHQV6LiWRRWcIexpwCJAEInqpNJbjPNIcTbUIHhsUHdTqQJ92LCFENsg4Gx3F3okkA" alt /></p>
<p>I embarked on a colourful journey to craft a palette, enlisting the help of AI tools like Midjourney and Microsoft Designer (yes, I called upon our AI overlords 🤖). Together, we quickly conjured up the business logo (loosely inspired by the Lambda symbol and a touch of Half-Life) and some human imagery for the application. The colour palette was carefully chosen to maintain brand identity while ensuring contrast in both light and dark modes.</p>
<p><img src="https://lh7-us.googleusercontent.com/RIRB8VgiuttAgey3jgHtT8343IX-b-3VYLvTinbsObbuAVbS4hDibOS5Vwu2oY1_rLP2efs-v_V25c5TNTmBIFj8YufiK7g4G-PdkZ6j3pUng6HeXfOv0FLWQjiWH_CU57i-mnzJ_iMpFIzV6XgO1pk" alt /></p>
<p>I put a bow on the design system by crafting reusable UI components that would come in handy during implementation. My focus here was on creating versatile buttons and toggles that would seamlessly switch between dark and light modes.</p>
<p><img src="https://lh7-us.googleusercontent.com/g3EjmzuU9XA0PPyoE97vyDQkzhn_uzVhyMB6XjdyxlMCy4OLcQtWq0kvpb2XkzycRdV_SwGfxAQ85ZYM7m0pep1_A98QeaKwHrIFrh6Z-06clOZbr-oJUaOVzE65DlL4VAGDCBwZo9rZjn77L9ob1K0" alt /></p>
<h3 id="heading-my-view-on-vue-a-pleasant-surprise-a-react-devs-perspective">My <em>View</em>* on Vue: A Pleasant Surprise - A React Dev's Perspective</h3>
<p>*😉 Pun Intended</p>
<p>As I mentioned earlier, my decision to use Vue in this project was born out of seeing it in a lot of Front-End developer postings 🙈. So I went ahead and dived into the official docs and boy oh boy was I pleasantly surprised.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699137038172/8cdd5079-8cb7-40cd-aed9-b7f39f3191cc.png" alt class="image--center mx-auto" /></p>
<p>As the comic above illustrates, Vue is so refreshingly declarative. After learning Vanilla JS, I picked up React and started learning about lifecycles, hooks, and JSX. It was only after creating <a target="_blank" href="https://hashnode.com/post/cloffzuvg000b09lcb530h9hp">HxSkits</a> did I understand the power React’s imperative granularity presented.</p>
<p>Nonetheless, it was surprisingly refreshing to discover how Vue tackled reactivity with its ref() and reactive() methods, managed dependencies through the computed and watch methods, and, most notably, offered built-in directives and a user event handling syntax. I also couldn't help but be impressed by Vue's compositional API. Perhaps it's my aspirational desire to weave intricate code talking, but Vue's compositional API not only helped me maintain code organization but also strangely improved my clarity of thought. It made it easy to distinguish where the code ended and the templating began.</p>
<p>I was initially worried about the syntax differences when learning the framework, but everything seemed like a “more to-the-point” version of React. Safe to say for less complex projects, I’ll be keeping Vue in view (Yes I know, I'm using the dad joke again, come fight me 🥷🏾).</p>
<h3 id="heading-building-with-vue-implementing-the-design-system-components-and-dark-mode">Building With Vue: Implementing the design system, components, and Dark Mode</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/6x4CLjC8KofaU/giphy.gif">https://media.giphy.com/media/6x4CLjC8KofaU/giphy.gif</a></div>
<p> </p>
<p>Given the small size of this project, my first decision was to use Pinia to handle global state management over VueX. This was because Pinia’s independent stores were simple enough to store, modify, and share key states across the application without the need to lug a single state tree from module to module (Man, I can't wait for Vue 5’s switch to Pinia). Apart from Pinia, I also went with TailwindCSS for styling.</p>
<p>So, let's dive into the code! First up, I grappled with font styling. But before I spill the beans, let's take a moment to salute all the Frontend devs I've pestered in my past life as a UX/UI designer. You folks are the real MVPs 🥹.</p>
<p>Now, when it comes to font styling – line heights, font sizes, and all that jazz – it's all about making the text more readable. Ensuring a responsive font size that still followed the design was a little bit of a doozy, but rem spacing came in clutch at the end of the day. Setting the root font size as 16px, I was able to set dynamic font for smaller breakpoints changing the root font size.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;

<span class="hljs-comment">/***************************
    WEB FONT STYLES
***************************/</span>
<span class="hljs-keyword">@layer</span> base {
    <span class="hljs-selector-class">.body2</span> {
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.8125rem</span>;
        <span class="hljs-attribute">line-height</span>: <span class="hljs-number">1.375rem</span>;
    }

    <span class="hljs-selector-class">.body</span>{
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1rem</span>; <span class="hljs-comment">/** Equivalent to 1 x 16 = 16*/</span>
        <span class="hljs-attribute">line-height</span>:<span class="hljs-number">1.625rem</span>;
    }
}

<span class="hljs-comment">/***************************
    TRADITIONAL MOBILE FONT STYLES
***************************/</span>
<span class="hljs-keyword">@layer</span> base {
    <span class="hljs-keyword">@media</span> (<span class="hljs-attribute">max-width:</span> <span class="hljs-number">640px</span>) {
        <span class="hljs-selector-class">.body2</span> {
            <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.6875rem</span>;
            <span class="hljs-attribute">line-height</span>: <span class="hljs-number">1.3571428571rem</span>;
        }

        <span class="hljs-selector-class">.body</span> {
            <span class="hljs-attribute">font-size</span>:<span class="hljs-number">0.875rem</span>; <span class="hljs-comment">/** Equivalent to 0.875 x 16 = 14*/</span>
            <span class="hljs-attribute">line-height</span>: <span class="hljs-number">1.4375rem</span>;
        }
}
</code></pre>
<p>I moved on to creating my first component; the Navbar. Apart from being the first visual element at the top of the page, this element also allowed me to work on 3 things;</p>
<ol>
<li><p>The Dark Mode &amp; Menu Overlay Stores: The Dark Mode store is like the mission control centre for toggling dark mode on and off. It's equipped with a nifty method to set or toggle the dark mode. Then there's the Menu store, the brain behind opening and closing the menu. It has a method to toggle the menu's visibility.</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">//Taken from DarkModeStore.ts</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useDarkModeStore = defineStore(<span class="hljs-string">'darkModeStore'</span>, (): <span class="hljs-function"><span class="hljs-params">darkModeStoreInterface</span> =&gt;</span> {
     <span class="hljs-keyword">const</span> isDarkModeEnabled = ref(<span class="hljs-literal">false</span>)
     <span class="hljs-keyword">const</span> toggleDarkMode = <span class="hljs-function">(<span class="hljs-params">toggleOption?: <span class="hljs-built_in">boolean</span></span>) =&gt;</span> {
         <span class="hljs-keyword">if</span> (toggleOption === <span class="hljs-literal">true</span> || toggleOption === <span class="hljs-literal">false</span>) {
             isDarkModeEnabled.value = toggleOption
         } <span class="hljs-keyword">else</span> {
             isDarkModeEnabled.value = !isDarkModeEnabled.value
         }
     }
     <span class="hljs-keyword">return</span> { isDarkModeEnabled, toggleDarkMode }
 })

 <span class="hljs-comment">//Taken from MenuVisibilityStore.ts</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useMenuVisibilityStore = defineStore(<span class="hljs-string">'menuVisibilityStore'</span>,
     (): <span class="hljs-function"><span class="hljs-params">menuVisibilityInterface</span> =&gt;</span> {
         <span class="hljs-keyword">const</span> isMenuVisible = ref(<span class="hljs-literal">false</span>);

         <span class="hljs-keyword">const</span> toggleMenu = <span class="hljs-function">() =&gt;</span> {
             isMenuVisible.value = !isMenuVisible.value
         }

         <span class="hljs-keyword">return</span> { isMenuVisible, toggleMenu }
     })
</code></pre>
</li>
<li><p>The Toggle widget: This little gem is like a swanky checkbox, all dressed up. It's bound to the ‘isDarkModeEnabled’ property from the useDarkModeStore. Clicking it toggles dark mode with finesse. I even threw in some custom CSS to make the button dance when it's clicked. I reused this widget in the menu overlay, making it a breeze for mobile users to switch to dark mode.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> { isDarkModeEnabled } = storeToRefs(useDarkModeStore())
 <span class="hljs-keyword">const</span> { toggleDarkMode } = useDarkModeStore()

 <span class="hljs-keyword">const</span> assetsToBeUsed = computed(<span class="hljs-function">() =&gt;</span> {
     <span class="hljs-keyword">return</span> {
         logoSrc: isDarkModeEnabled.value ? whiteMoon : blueSun,
         title: <span class="hljs-string">"Toggle"</span>
     }
 })

 &lt;/script&gt;

 &lt;template&gt;
     &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"relative"</span>&gt;
         &lt;label <span class="hljs-keyword">class</span>=<span class="hljs-string">"relative"</span> <span class="hljs-meta">@click</span>=<span class="hljs-string">"() =&gt; { toggleDarkMode() }"</span> <span class="hljs-keyword">for</span>=<span class="hljs-string">"toggle"</span>&gt;
             &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"m-0 p-0 flex relative cursor-pointer"</span>&gt;
                 &lt;img :title=<span class="hljs-string">"assetsToBeUsed.title"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"w-5 mr-2"</span> :src=<span class="hljs-string">"assetsToBeUsed.logoSrc"</span> /&gt;
                 &lt;input v-model=<span class="hljs-string">"isDarkModeEnabled"</span> name=<span class="hljs-string">"toggle"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"sr-only"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"checkbox"</span> /&gt;
                 &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"dot transition absolute 
                 w-4 h-4
                 top-1 left-8 sm:w-6 sm:h-6 bg-blue rounded-full "</span>&gt;&lt;/div&gt;
                 &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"dot-bg w-10 h-6 
             sm:w-14 sm:h-8 
             bg-light-blue rounded-full border border-blue"</span>&gt;&lt;/div&gt;
             &lt;/div&gt;
         &lt;/label&gt;
     &lt;/div&gt;
 &lt;/template&gt;
</code></pre>
<pre><code class="lang-css"> <span class="hljs-comment">/****************
 TOGGLE STYLING
 ****************/</span>
 <span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">:checked</span> ~ <span class="hljs-selector-class">.dot</span> {
     <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">100%</span>);
     <span class="hljs-attribute">background-color</span>:black;
 }

 <span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">:checked</span> ~ <span class="hljs-selector-class">.dot-bg</span> {
     <span class="hljs-attribute">background-color</span>: white;
     <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid black;
 }
</code></pre>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699133760934/a9904793-db21-41e6-9242-596c282234cb.gif" alt class="image--center mx-auto" /></p>
</li>
<li><p>Menu Overlay: The menu button (essentially a styled hamburger icon) simply toggles the Menu overlay store when clicked.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699134738409/441f7b69-b3c0-47e0-b69f-a5805fa499e4.gif" alt class="image--center mx-auto" /></p>
<p>With the Navbar, dark mode store and its toggle methods being completed, I still needed to effect CSS changes based on when the ‘<em>isDarkModeEnabled</em>’ was either true or false. This is where the dark mode variant of TailwindCSS comes in! Using Vue’s Watch method, I could easily apply the dark to the body of the HTML document depending on whether the dark mode reference was truthy or falsy.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Taken from App.vue</span>

<span class="hljs-comment">//Toggle Tailwind's Dark Mode when Pinia's dark mode state is set to True</span>
watch(isDarkModeEnabled, <span class="hljs-function">() =&gt;</span> {
  toggleTailwindDarkClassToBody()
})
</code></pre>
<p>This is especially handy because it lets different sections (which I crafted as components) join the dark mode party. They can tweak background images, text colours, and colour schemes. Take the hero section, for instance, transforming from light mode to dark mode like a superstar 🕺🏾.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699135724701/e0903933-112f-4645-be59-46936716543b.gif" alt class="image--center mx-auto" /></p>
<pre><code class="lang-typescript">&lt;script setup lang=<span class="hljs-string">"ts"</span>&gt;

<span class="hljs-comment">//Vue imports</span>
<span class="hljs-keyword">import</span> { computed } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-comment">//Pinia store</span>
<span class="hljs-keyword">import</span> { useDarkModeStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../store/DarkModeStore'</span>;

<span class="hljs-comment">//Pinia methods</span>
<span class="hljs-keyword">import</span> { storeToRefs } <span class="hljs-keyword">from</span> <span class="hljs-string">'pinia'</span>

<span class="hljs-comment">//Components</span>
<span class="hljs-keyword">import</span> Buttons <span class="hljs-keyword">from</span> <span class="hljs-string">'../common/Buttons.vue'</span>;

<span class="hljs-comment">//Utility</span>
<span class="hljs-keyword">import</span> { scrollToSection } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../utilities/scrollIntoView'</span>;

<span class="hljs-comment">//Imags</span>
<span class="hljs-keyword">import</span> darkManHeroImage <span class="hljs-keyword">from</span> <span class="hljs-string">"../../assets/man-image-dark.png"</span>
<span class="hljs-keyword">import</span> whiteManHeroImage <span class="hljs-keyword">from</span> <span class="hljs-string">"../../assets/man-image-light.png"</span>

<span class="hljs-keyword">const</span> { isDarkModeEnabled } = storeToRefs(useDarkModeStore())

<span class="hljs-keyword">const</span> dynamicProperties = computed(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        {
            webBackgroundImage: isDarkModeEnabled.value ? <span class="hljs-string">"bg-web-hero-image-dark"</span> : <span class="hljs-string">"bg-web-hero-image-blue"</span>,
            mobileBackgroundImage: isDarkModeEnabled.value ? <span class="hljs-string">"bg-mobile-hero-image-dark"</span> : <span class="hljs-string">"bg-mobile-hero-image-blue"</span>,
            backgroundImageBorder: isDarkModeEnabled.value ? <span class="hljs-string">"border-gray"</span> : <span class="hljs-string">"border-navy"</span>,
            manHeroImage: isDarkModeEnabled.value ? darkManHeroImage : whiteManHeroImage,
        }
    )
})

&lt;/script&gt;

&lt;template&gt;
    &lt;section id=<span class="hljs-string">"hero-section"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"constrained-container
    flex
    py-10 lg:py-16
    h-full 2xl:max-w-7xl 2xl:mx-auto"</span>&gt;
        &lt;div id=<span class="hljs-string">"hero-section-internal-div"</span>
            :<span class="hljs-keyword">class</span>=<span class="hljs-string">"`dark w-full
                border-2 ${dynamicProperties.backgroundImageBorder}
                                                        m-0 p-0
                                                        px-6 sm:px-17
                                                        h-[calc(100vh-6.6875rem)] lg:h-[calc(100vh-8rem)]
                                                        flex
                                                        bg-mobile-hero-image-blue dark:bg-mobile-hero-image-dark xl:bg-web-hero-image-blue xl:dark:bg-web-hero-image-dark bg-no-repeat bg-cover`"</span>&gt;

            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex items-center"</span>&gt;
                &lt;div id=<span class="hljs-string">"hero-section-contents"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col-reverse sm:flex-col-reverse lg:flex-row lg:items-center"</span>&gt;
                    &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col"</span>&gt;
                        &lt;h1 id=<span class="hljs-string">"header-text"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"h3 sm:h1 text-white font-semibold sm:pt-0"</span>&gt;The Technology
                            Partner &lt;br id=<span class="hljs-string">"hero-section-line-break"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"hidden sm:block"</span>&gt; You Can Trust&lt;/h1&gt;
                        &lt;h4 id=<span class="hljs-string">"hero-supportingText"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"body sm:h4 text-white w-full sm:w-4/5 pt-4"</span>&gt;We help organizations transform their
                            businesses through bespoke technology solutions.&lt;/h4&gt;
                        &lt;div <span class="hljs-meta">@click</span>=<span class="hljs-string">"()=&gt;{scrollToSection('mission-section')}"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"pt-5"</span>&gt;
                            &lt;Buttons :buttonClassType=<span class="hljs-string">"`${isDarkModeEnabled ? 'base' : 'outline'}`"</span> buttonId=<span class="hljs-string">"heroCTA"</span> buttonText=<span class="hljs-string">"Get started"</span> iconType=<span class="hljs-string">"arrowDown"</span> /&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"pb-4 sm:pb-8 xl:p-0"</span>&gt;
                        &lt;img id=<span class="hljs-string">"hero-image"</span> :src=<span class="hljs-string">"`${dynamicProperties.manHeroImage}`"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"transition delay-300 ease-in-out 
                            w-48 sm:w-80 lg:w-96"</span> alt=<span class="hljs-string">""</span> srcset=<span class="hljs-string">""</span>&gt;
                    &lt;/div&gt;

                &lt;/div&gt;

            &lt;/div&gt;

        &lt;/div&gt;
    &lt;/section&gt;
&lt;/template&gt;
</code></pre>
<p>The last part of implementing dark mode was to use Javascript to automatically detect the user’s theme preference and set the '<em>isDarkModeEnabled'</em> flag to match.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Taken from checkLocalDarkModeSetting.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkLocalDarkModeSetting</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.matchMedia &amp;&amp; <span class="hljs-built_in">window</span>.matchMedia(<span class="hljs-string">'(prefers-color-scheme:dark)'</span>).media) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
}

<span class="hljs-comment">//Taken from App.vue</span>
<span class="hljs-comment">//Check Platform's local dark mode to and set Pinia dark mode to match</span>
<span class="hljs-keyword">if</span> (checkLocalDarkModeSetting()) {
  toggleDarkMode(<span class="hljs-literal">true</span>)
  toggleTailwindDarkClassToBody()
}
</code></pre>
<h3 id="heading-learn-from-my-struggles-importing-images-into-components-from-srcassets-rather-than-public">Learn from my struggles 🙃: Importing images into components from src/assets rather than /public</h3>
<p>While navigating the winding roads of this project, I had an 'aha' moment about asset storage – the distinction between housing them in the /public folder and sourcing them from the src/assets folder.</p>
<p>The key takeaway? Using assets from the /public directory in Vue components is like taking a shortcut that skips vital processes (think optimization and hashing) performed by Webpack. An example of this optimization is Webpack hashing an image in the src/assets folder and being able to cache this image after it is first displayed, preventing unnecessary network requests.</p>
<p>But wait, there's more 👻🔪. If you opt for URL paths to assets in the src folder without importing them as JavaScript objects, Webpack steps in with its own brand of magic. During the project build, Webpack sprinkles some hash fairy dust on the images in the src/assets folder and adds a unique signature to their filenames. The result? Image elements with broken paths. Don't fret, though – the solution is as easy as pie: import those images as objects, and you're good to go.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Taken from HealthInsuranceSection.vue</span>
<span class="hljs-comment">//Images</span>
<span class="hljs-keyword">import</span> familyImageBlack <span class="hljs-keyword">from</span> <span class="hljs-string">"../../assets/family-image-black.png"</span>
<span class="hljs-keyword">import</span> familyImageBlue <span class="hljs-keyword">from</span> <span class="hljs-string">"../../assets/family-image-white.png"</span>

<span class="hljs-keyword">const</span> pathToFamilyImage = computed(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> isDarkModeEnabled.value ? familyImageBlack : familyImageBlue
})
</code></pre>
<p>Now, before you kick the /public folder to the curb, let me be clear. It's not off-limits for images; they can still find a cozy spot there, as long as they're not used in JS modules. For example, I housed background images in the /public folder to create custom background classes in my tailwind.config.js file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//Taken from Tailwind.config.js</span>
<span class="hljs-keyword">const</span> defaultTheme = <span class="hljs-built_in">require</span>(<span class="hljs-string">'tailwindcss/defaultTheme'</span>)

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">darkMode</span>: <span class="hljs-string">"class"</span>,
  <span class="hljs-attr">content</span>: [
    <span class="hljs-string">"./index.html"</span>,
    <span class="hljs-string">"./src/**/*.{vue,js,ts,jsx,tsx}"</span>,
  ],
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {
      <span class="hljs-attr">colors</span>: {
      <span class="hljs-attr">backgroundImage</span>: {
        <span class="hljs-string">'mobile-hero-image-blue'</span>: <span class="hljs-string">"url('/mobile-hero-image-blue.png')"</span>,
        <span class="hljs-string">'mobile-hero-image-dark'</span>: <span class="hljs-string">"url('/mobile-hero-image-dark.png')"</span>,
        <span class="hljs-string">'web-hero-image-blue'</span>: <span class="hljs-string">"url('/web-hero-image-blue.png')"</span>,
        <span class="hljs-string">'web-hero-image-dark'</span>: <span class="hljs-string">"url('/web-hero-image-dark.png')"</span>,
        <span class="hljs-string">'contact-us-image-blue'</span>: <span class="hljs-string">"url('/contact-us-bg-blue.png')"</span>,
        <span class="hljs-string">'contact-us-image-dark'</span>: <span class="hljs-string">"url('/contact-us-bg-black.png')"</span>,
        <span class="hljs-string">'footer-bg-blue'</span>: <span class="hljs-string">"url('/footer-bg-blue.png')"</span>,
        <span class="hljs-string">'footer-bg-black'</span>: <span class="hljs-string">"url('/footer-bg-black.png')"</span>,
        <span class="hljs-string">'footer-bg-blue-mobile'</span>: <span class="hljs-string">"url('/footer-bg-blue-mobile.png')"</span>,
        <span class="hljs-string">'footer-bg-black-mobile'</span>: <span class="hljs-string">"url('/footer-bg-black-mobile.png')"</span>,
      }
    },
  },
  <span class="hljs-attr">variants</span>: {
    <span class="hljs-attr">extend</span>: {
      <span class="hljs-attr">backgroundImage</span>: [<span class="hljs-string">'responsive'</span>]
    }
  },
  <span class="hljs-attr">plugins</span>: [],
}
</code></pre>
<h3 id="heading-the-end-of-my-vue-tiful-journey-lessons-learned-from-kiaret">The End of My Vue-tiful *Journey: Lessons Learned from Kiaret</h3>
<p>*3rd time I'm using the same Vue-dad joke, fight me 🥷🏾</p>
<p>In this blog post, I shared my experience of creating a one-page website for Kiaret, a health insurance company, using Vue and TailwindCSS.</p>
<ul>
<li><p>Creating a simple dark-ready design system using AI tools and the golden ratio.</p>
</li>
<li><p>Learning Vue and its compositional API, made me appreciate its declarative and reactive nature</p>
</li>
<li><p>Building with Vue and implementing dark mode using Pinia and TailwindCSS.</p>
</li>
<li><p>Using Vue components to create modular and reusable UI elements.</p>
</li>
</ul>
<p>This project was a great opportunity for me to showcase my skills as a junior front-end engineer and to learn new technologies and techniques. I enjoyed the challenge and the creativity involved in this project, and I am looking forward to more web development opportunities in the future.</p>
<p>Thank you for reading my blog post. I hope you found it informative and entertaining. In my next post, I will share my progress on a big project that I am planning to do soon (Hint: It involves Whatsapp). This time, I will be doing UX research before I start coding, unlike my previous project where I ended up building for myself 🙃. Stay tuned for more updates and insights from my web development journey. See you soon! 😊</p>
]]></content:encoded></item><item><title><![CDATA[HxSkits: Crafting Instant Magic with React PWAs and Firebase Notifications 🪄🔔]]></title><description><![CDATA[Introduction 🎤: A World of Online Comedy
In the ever-entertaining realm of online streaming, where laughter reigns supreme, one personality stood out - Hyphonix. This online comedy virtuoso specialises in orchestrating hilarious pranks on Omegle, th...]]></description><link>https://lovingtheboulder.hashnode.dev/hxskits-crafting-instant-magic-with-react-pwas-and-firebase-notifications</link><guid isPermaLink="true">https://lovingtheboulder.hashnode.dev/hxskits-crafting-instant-magic-with-react-pwas-and-firebase-notifications</guid><category><![CDATA[progressive web apps]]></category><category><![CDATA[React]]></category><category><![CDATA[Express]]></category><category><![CDATA[firestore]]></category><category><![CDATA[FCM]]></category><dc:creator><![CDATA[Oluwapeyibomi Oyelohunnu]]></dc:creator><pubDate>Wed, 01 Nov 2023 07:34:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698586576468/5cbc4d6f-d2d0-4587-8d8a-e924c598a09f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction-a-world-of-online-comedy">Introduction 🎤: A World of Online Comedy</h3>
<p>In the ever-entertaining realm of online streaming, where laughter reigns supreme, one personality stood out - Hyphonix. This online comedy virtuoso specialises in orchestrating hilarious pranks on Omegle, the platform where video chats and unexpected connections take centre stage.</p>
<p>Being a dedicated subscriber, I closely followed Hyphonix's journey. He possessed the unique talent of whisking his audience away on a rollercoaster of fun. The premise was simple but genius: one of his viewers, let's call them 'A,' would initiate a conversation with an Omegle user, often referred to as 'the mark.' They would casually gather some seemingly mundane details - perhaps a name or a favourite song.</p>
<p>Here's where the magic began. Hyphonix, our puppet master, would seamlessly switch out viewer 'A' for viewer 'B,' who'd continue the charade as a new stranger. This relay would continue, with viewer 'C' taking their turn in the act. Then, the grand reveal: 'C' would drop the collected information, leaving the Omegle user in delightful bewilderment.</p>
<p>This masterpiece of chaos and comedy was aptly named the 'Community Name Skit.' Get a sneak peek here when the skit is perfectly executed:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtube.com/shorts/dlltX0T-i4k">https://youtube.com/shorts/dlltX0T-i4k</a></div>
<p> </p>
<p>However, Hyphonix faced challenges in executing this skit flawlessly. Viewers often stumbled, revealing information too soon or failing to play the part of genuine strangers. Hyphonix's attempts to streamline the process encountered roadblocks:</p>
<ol>
<li><p>Trying to display information on the screen using OBS software was thwarted by Omegle's watchful eye, leading to IP bans.</p>
</li>
<li><p>Relaying instructions through Discord messages had its limitations, specifically onboarding users resistant to signing up for Discord and unfamiliar with the platform</p>
</li>
<li><p>Resorting to YouTube chat proved a cumbersome endeavour, juggling pinned instructions while coordinating the show.</p>
</li>
</ol>
<p>Based on this background and the previous stopgaps, I distilled the objectives of any viable solution into the following goals:</p>
<ol>
<li><p><strong>Streamline viewer participation in Omegle pranks, bypassing the onboarding process that could slow down recruits to Discord.</strong></p>
</li>
<li><p><strong>Empower viewers with straightforward, instant, foolproof instructions, ensuring they have all the ammunition necessary to leave unsuspecting Omegle users in awe</strong></p>
</li>
</ol>
<p>My understanding of the project's goals mainly came from watching those live streams. But, I'll spill the beans on a major mistake I made in the conclusion.</p>
<h3 id="heading-ux-design-crafting-a-hilarious-visual-tale">UX Design 🗺️: Crafting a Hilarious Visual Tale</h3>
<p>To get a grip on the technical nuts and bolts needed to make our dreams come true, I embarked on a wild journey of storyboarding. Check out this colourful storyboard that shows the expected experiences needed to achieve Goals #1 and #2.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698609381318/062d8446-01e6-47f1-9cce-479778f055b9.jpeg" alt="A storyboard showing the typical experience provided by using the Hx Skits App Including Hyphonix, 2 of his viewers and a stranger named Poopies." class="image--center mx-auto" /></p>
<p>This storyboard spilt the beans on two must-have features:</p>
<ol>
<li><p><strong>Real-Time Chatter</strong>: Our app had to facilitate lightning-fast conversations between an ultra-exclusive Admin hideout and the broader audience. The Admin hotspot, a realm for Hyphonix and his playful moderators, served as the hub for skit wizardry, with updates and laughter flying at the speed of light.</p>
</li>
<li><p><strong>Laid-Back Onboarding</strong>: We couldn't skip out on an onboarding process that's as smooth as butter on a hot pancake. No matter which tech wizardry I chose, the goal was crystal clear: I wanted users to dive into the name skit frenzy with ease, with no strings attached!</p>
</li>
</ol>
<h3 id="heading-tech-selection-building-hilarity-with-a-tech-stack"><strong>Tech Selection 👨🏾‍💻: Building Hilarity with a Tech Stack</strong></h3>
<p>To put theory into practice, I set out to craft a React Progressive Web Application for the front end. The driving force behind this choice was Goal #2: <strong>A Seamless Onboarding</strong> <em>Experience</em>. At the time (not anymore 🥲), I firmly believed that the smoothest onboarding process could be achieved by turning the website into an installable application. The Progressive Web Application (PWA) framework seemed like the perfect tool for this job, but, as I elaborate on you'll see PWAs still had more than their fair share of quirks.</p>
<p>Transitioning from the frontend to the backend, I needed a robust foundation to support Goal #1: <strong>Real-Time Chatter</strong>. To tackle this, I harnessed Express to create APIs and laid out a plan to employ Firestore, Firebase Functions, and Cloud Messaging. This would facilitate the flow of notifications from the Admin section to the general user base. It's a bit of a broken record, I know, but as you'll soon discover, the PWA's idiosyncrasies forced some nimble adjustments along the way.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://tenor.com/en-GB/view/im-not-sure-of-anything-anymore-eric-cartman-south-park-s7e11-casa-bonita-gif-22098371">https://tenor.com/en-GB/view/im-not-sure-of-anything-anymore-eric-cartman-south-park-s7e11-casa-bonita-gif-22098371</a></div>
<p> </p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🧠</div>
<div data-node-type="callout-text">Average experience trying to build a browser-independent PWA</div>
</div>

<h3 id="heading-ui-design-crafting-a-hilarious-visual-tale"><strong>UI Design 🖼️: Crafting a Hilarious Visual Tale</strong></h3>
<p>In the realm of UI design, the mission was crystal clear - to craft a straightforward visual roadmap showcasing the admin and client-side interfaces. This encompassed detailing the user-friendly onboarding journey, including the installation process as a PWA, and accounting for those tricky edge cases. If you're curious to take a peek, venture <a target="_blank" href="https://www.figma.com/file/ZDnfk0Z8BHpHOTtMOnS2t7/Personal-Projects?type=design&amp;node-id=10-7196&amp;mode=design&amp;t=XSI8HXWxNn6UauQC-4">here</a>.</p>
<p>Despite my meticulous documentation of the project's features and requirements, I couldn't resist the allure of scope creep. This meant I also created a system that enabled admins to notify general users when Hyphonix went live, revealing his current platform and activity, be it the iconic name skit or simply hanging out with viewers</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698610460463/2276b15d-b366-4fb0-b8e2-8245809e693c.png" alt="Sample UI Design displaying the General Home Page" class="image--center mx-auto" /></p>
<p>While my design system might not have been my magnum opus, it did entail the intricate design of elements like Modals, Buttons, Inputs, Navbars, and an array of other components. Should you desire a closer look, mosey on over <a target="_blank" href="https://www.figma.com/file/ZDnfk0Z8BHpHOTtMOnS2t7/Personal-Projects?type=design&amp;node-id=312-3158&amp;mode=design&amp;t=XSI8HXWxNn6UauQC-4">here</a>.</p>
<h3 id="heading-diving-into-the-nuts-and-bolts-the-front-end"><strong>Diving into the Nuts and Bolts 🔩:  The Front End</strong></h3>
<details><summary>Tech used</summary><div data-type="detailsContent">React, Tailwind, Vite, Framer-Motion, Firebase Client SDK, Service Workers, Axios, React Router, Typescript, Vercel</div></details>

<p>As I dive into the fascinating world of React PWAs and Express APIs, it's important to clarify that this isn't a step-by-step guide for replicating my project. I won't dissect every line of code, but don't worry, I'll shine a light on the most critical code blocks. If you're itching to explore the full codebase, swing by the GitHub repositories for the <a target="_blank" href="https://github.com/Sisyphusean/Hx-Skits">Front End</a> and <a target="_blank" href="https://github.com/Sisyphusean/Hyphx-skit-app-BE">Back End</a>.</p>
<p>Kicking off the React PWA, I decided to use the Vite library. Despite being aware of state management tools like Redux and Zustand, I also took a different path and built my global state container using a React context I named 'App Data Context.' I persisted the context by manually storing it in local storage. When state updates occurred, I played the comparison game between the App Data Context's state and the state in local storage, making necessary updates along the way. This whole process gave birth to a custom hook called 'useSetAppData,' which made state updates a breeze by reconciling differences between in-memory and local storage states.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Store of the entire app's data with it's defined shape</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AppDataContextProvider</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
    <span class="hljs-keyword">const</span> [appData, setAppData] = useState(emptyAppData)
    <span class="hljs-comment">//instantiate app's data into state memory if it exists and create it if it doesn't exist.</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> initiliazeAppData = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">let</span> appDataResult = <span class="hljs-keyword">await</span> getAppDataInLocalStorage()

            <span class="hljs-keyword">if</span> (!appDataResult) {
                <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"initializing app data"</span>)
                <span class="hljs-comment">//Data doesn't exist and is set to false</span>
                setAppDataInLocalStorage(appData)
            }

            <span class="hljs-keyword">if</span> (appDataResult) {
                <span class="hljs-comment">//Data exists</span>
                <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Setting app data from local storage"</span>)
                setAppData(appDataResult)
            }
        }

        initiliazeAppData()

    }
        , [])

    <span class="hljs-keyword">return</span> (
        &lt;AppDataContext.Provider value={{ appData, setAppData }}&gt;
            {children}
        &lt;/AppDataContext.Provider&gt;
    )

}

<span class="hljs-comment">//Defintion of the useSetAppData Hook</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useSetAppData = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> { setAppData } = useContext(AppDataContext)
    <span class="hljs-keyword">const</span> updateAppDataWithShallowComparison = useUpdateAppDataWithShallowComparisonIgnoringTime()

    <span class="hljs-comment">/**
     * This function sets the app's data in react's memory and in local storage
     * @param newData The new app data to set
     */</span>
    <span class="hljs-keyword">const</span> setAppDataCallback = useCallback(<span class="hljs-function">(<span class="hljs-params">newData: appDataInterface</span>) =&gt;</span> {
        setAppData(newData)
        updateAppDataWithShallowComparison(newData)
    }, [setAppData])

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

}
</code></pre>
<p>One of the fantastic perks of using <a target="_blank" href="https://vitejs.dev/">Vite</a> for my PWA project was its plugin-driven setup. These plugins swiftly orchestrated vital features such as configuring the PWA's manifest and crafting a strategy for configuring the service worker.</p>
<p>The first major hiccup on the PWA journey was realizing that not all web browsers played nicely with the "<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event#browser_compatibility">beforeinstallprompt</a>" event. I needed to control the event to explain to users why they should install the HxSkits web app. I also needed the 'beforeinstallprompt' to trigger the installation process using the event's "prompt" method. It became clear that even when some Chromium desktop browsers embraced the "beforeinstallprompt" event, their mobile counterparts weren't quite as cooperative – think Microsoft Edge on desktop versus the mobile versions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698671837087/2507a6e6-cf65-42d1-89c1-beae57edfcd8.png" alt class="image--center mx-auto" /></p>
<p>Now, when it came to Safari, things took an interesting twist. Users had to bookmark the site before being able to trigger this event. Nevertheless, I pressed on, aiming to support browsers that played nice across the board. This meant dancing to Safari's somewhat convoluted installation process. I also had to initially exclude browsers that didn't support PWA APIs, like Opera, Firefox (Sorry Mozilla 🥲), Samsung Internet Browser and so on.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// This function checks if a browser supports PWA by checking</span>
<span class="hljs-comment">// if it supports certain features</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> checkIfBrowserSupportsPwa = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">/**
    * This variable is used to store the result of the check for PWA support
    */</span>
    <span class="hljs-keyword">let</span> isPwaSupported: <span class="hljs-built_in">boolean</span> = <span class="hljs-literal">false</span>

    <span class="hljs-keyword">const</span> hasAdvancedUserExperience = <span class="hljs-string">'serviceWorker'</span> <span class="hljs-keyword">in</span> navigator;
    <span class="hljs-keyword">const</span> hasWebPushNotifications = <span class="hljs-string">'PushManager'</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">window</span>;
    <span class="hljs-keyword">const</span> hasHomeScreen = <span class="hljs-string">'BeforeInstallPromptEvent'</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">window</span>;
    <span class="hljs-keyword">const</span> hasGeolocation = <span class="hljs-string">'geolocation'</span> <span class="hljs-keyword">in</span> navigator;
    <span class="hljs-keyword">const</span> hasVideoImageCapturing = <span class="hljs-string">'ImageCapture'</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">window</span>;
    <span class="hljs-keyword">const</span> hasBluetooth = <span class="hljs-string">'bluetooth'</span> <span class="hljs-keyword">in</span> navigator;

    <span class="hljs-comment">// Add this line to exclude Samsung Internet Browser, Opera browser, </span>
    <span class="hljs-comment">// firefox and other browsers with dodgy pwa support</span>
    <span class="hljs-keyword">const</span> excludedBrowsers = [<span class="hljs-string">'SamsungBrowser'</span>, <span class="hljs-string">'OPR'</span>, <span class="hljs-string">'Firefox'</span>, <span class="hljs-string">"Opera"</span>];
    <span class="hljs-keyword">if</span> (excludedBrowsers.some(<span class="hljs-function">(<span class="hljs-params">browser</span>) =&gt;</span> navigator.userAgent.includes(browser))) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }

    <span class="hljs-keyword">if</span> (hasAdvancedUserExperience &amp;&amp; hasWebPushNotifications &amp;&amp; hasHomeScreen &amp;&amp; hasGeolocation &amp;&amp; hasVideoImageCapturing &amp;&amp; hasBluetooth) {
    isPwaSupported = <span class="hljs-literal">true</span>
    }

    <span class="hljs-keyword">return</span> isPwaSupported
   }
<span class="hljs-comment">// This function does a PWA support check and returns the</span>
<span class="hljs-comment">// browser's name</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUserBrowsersDetails</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> userAgent = navigator.userAgent;
    <span class="hljs-keyword">let</span> browserName: <span class="hljs-built_in">string</span> = <span class="hljs-string">"unknown"</span>;
    <span class="hljs-keyword">let</span> doesBrowserSupportPwa: <span class="hljs-built_in">boolean</span> = checkIfBrowserSupportsPwa();

    <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Firefox"</span>)) {
        browserName = <span class="hljs-string">"firefox"</span>;
    } <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"SamsungBrowser"</span>)) {
        browserName = <span class="hljs-string">"samsung internet"</span>;
    } <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Opera"</span>) || userAgent.includes(<span class="hljs-string">"OPR"</span>)) {
        browserName = <span class="hljs-string">"opera"</span>;
    } <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Trident"</span>)) {
        browserName = <span class="hljs-string">"internet explorer"</span>;
    }
    <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Chrome"</span>)) {

        <span class="hljs-keyword">if</span> (checkBrowserBrandName(<span class="hljs-string">"Brave"</span>)) {
            browserName = <span class="hljs-string">"brave"</span>
        }

        <span class="hljs-keyword">if</span> (checkBrowserBrandName(<span class="hljs-string">"Google Chrome"</span>)) {
            browserName = <span class="hljs-string">"chrome"</span>
        }

    }
    <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Edg"</span>)) {
        browserName = checkBrowserBrandName(<span class="hljs-string">"Microsoft Edge"</span>) ? <span class="hljs-string">"edge"</span> : <span class="hljs-string">"unknown"</span>;
    }
    <span class="hljs-keyword">if</span> (userAgent.includes(<span class="hljs-string">"Safari"</span>) &amp;&amp; userOs === <span class="hljs-string">"ios"</span> || userOs === <span class="hljs-string">"macos"</span>) {
        browserName = <span class="hljs-string">"safari"</span>;
    }

    <span class="hljs-keyword">return</span> { browserName, doesBrowserSupportPwa };
}
</code></pre>
<p>I moved on, dead set on creating the PWA's service worker to play nice with Vite. This was a big deal because it was the ticket to ensure that the app could receive background messages from Firebase Cloud Messenger. So, I dived into the Vite docs, and I even <a target="_blank" href="https://stackoverflow.com/a/76620266/12713720">left a little trail on Stack Overflow,</a> where I spilt the beans on how I built the service worker. All that effort didn't go to waste as the HxSkits app could now receive background messages. This was achieved by passing the FCM token, acquired after registering the app with Firebase, through indexDB.</p>
<p>Yet, my story wouldn't be complete without a major twist – an iOS Webkit bug that left me scratching my head. It turned out that <a target="_blank" href="https://github.com/firebase/firebase-js-sdk/issues/7309#issuecomment-1631529601">service worker event listeners refused to work when launching the PWA from the home screen</a>. This hurdle forced me to forge a new path, an alternative onboarding route independent of PWA installation.</p>
<p>This choice was driven by the recognition that most of our users would be iPhone and Safari enthusiasts (yes, those Apple devotees 🙄) and users using unsupported browsers. Excluding them from using the application wasn't an option. As a result, instead of pushing users to install the site as a PWA, the onboarding process allowed these users to simply receive notifications while it remained open.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> consumeSSE = useGetAndConsumeSSE()
<span class="hljs-keyword">const</span> unsupportedBrowserSSEs = appData.userData.isUserBrowserSupported
        ? <span class="hljs-literal">null</span> : <span class="hljs-keyword">new</span> EventSource(homeUrl + <span class="hljs-keyword">import</span>.meta.env.VITE_SSE_ADMIN_SUBSCRIPTION)
 <span class="hljs-comment">//UseEffect for subscribing the unsupported browser to SSE endpoints </span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (unsupportedBrowserSSEs) {
            handleSseOnMessage(unsupportedBrowserSSEs)
            <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
                unsupportedBrowserSSEs.close()
            }
        }
   }, [unsupportedBrowserSSEs])
</code></pre>
<p>This pivot led me down the road of subscribing unsupported client devices to a Server-Sent Event (SSE) endpoint and updating the app's internal context using a custom hook called 'consumeSSE.' This workaround allowed unsupported users to receive notifications as long as the app remained open.</p>
<p>Through all this chaos and commotion, it was the inconsistent PWA standard and those pesky OS-level restrictions that caused all the "wahala." But in the end, the Front End application was in a state where users from all corners of the digital world, regardless of their device or 'browser creed,' could finally come aboard.</p>
<h3 id="heading-diving-into-the-nuts-and-bolts-the-back-end"><strong>Diving into the Nuts and Bolts 🧶:  The Back End</strong></h3>
<ul>
<li>Tech used: Express.js, body-parser, Express-validator, Passport.js, Firestore, Firebase Cloud Messenger, Firebase functions, Server-Sent Events, Typescript, Heroku</li>
</ul>
<p>Unlike the front end, working on the back end was a more straightforward journey. I kicked things off by setting up an Express server and crafting endpoints to handle administrator logins. To make this happen, I employed <a target="_blank" href="https://www.passportjs.org/">Passport</a>) for authentication and leveraged <a target="_blank" href="https://firebase.google.com/docs/firestore">Cloud Firestore</a> as our database backend. In a nutshell, I designed a local strategy that utilized Firestore to handle the login process. Additionally, I chose to implement JSON Web Tokens (JWT) to manage administrator sessions and secure protected endpoints.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> verifyUser = <span class="hljs-function">() =&gt;</span> {
    passport.use(<span class="hljs-keyword">new</span> LocalStrategy(
        <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verify</span>(<span class="hljs-params">username: <span class="hljs-built_in">string</span>, enteredPassword: <span class="hljs-built_in">string</span>, cb: <span class="hljs-built_in">Function</span></span>) </span>{
            <span class="hljs-keyword">try</span> {
                <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Running verify function"</span>)
                <span class="hljs-keyword">const</span> modCollectionRef = firebaseDB.collection(process.env.DB_MOD_COLLECTION <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)
                <span class="hljs-keyword">let</span> query = <span class="hljs-keyword">await</span> modCollectionRef.where(<span class="hljs-string">'username'</span>, <span class="hljs-string">'=='</span>, username).limit(<span class="hljs-number">1</span>)

                query.get().then(<span class="hljs-keyword">async</span> (querySnapshot: querySnapshot) =&gt; {
                    <span class="hljs-keyword">if</span> (querySnapshot.empty) {
                        <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"User does not exist"</span> })
                    } <span class="hljs-keyword">else</span> {
                        querySnapshot.forEach(<span class="hljs-keyword">async</span> (doc) =&gt; {
                            <span class="hljs-keyword">if</span> (doc.exists) {
                                <span class="hljs-keyword">const</span> id = doc.id
                                <span class="hljs-keyword">const</span> { username, password } = doc.data()
                                <span class="hljs-keyword">const</span> user = { id, username, password }
                                <span class="hljs-keyword">let</span> doPasswordsMatch = <span class="hljs-keyword">await</span> comparePassword(enteredPassword, password)

                                <span class="hljs-keyword">if</span> (!doPasswordsMatch) {
                                    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Incorrect password"</span>)
                                    <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"Incorrect password"</span> })
                                }


                                <span class="hljs-keyword">if</span> (doPasswordsMatch) {
                                    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Passwords match"</span>, <span class="hljs-built_in">JSON</span>.stringify(user))
                                    <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, user)
                                }

                            } <span class="hljs-keyword">else</span> {
                                <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"User does not exist"</span> })
                            }
                        })
                    }
                })
            } <span class="hljs-keyword">catch</span> (e) {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to get DB"</span>)
                <span class="hljs-keyword">return</span> cb(e, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"Failed to get DB"</span> })
            }
        }))
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> verifyJWT = <span class="hljs-function">() =&gt;</span> {
    passport.use(<span class="hljs-keyword">new</span> JwtStrategy(jwtOptions, <span class="hljs-keyword">async</span> (payload: <span class="hljs-built_in">any</span>, cb: <span class="hljs-built_in">Function</span>) =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> modCollectionRef = firebaseDB.collection(process.env.DB_MOD_COLLECTION <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)
            <span class="hljs-keyword">let</span> query = <span class="hljs-keyword">await</span> modCollectionRef.where(<span class="hljs-string">'username'</span>, <span class="hljs-string">'=='</span>, payload.username).limit(<span class="hljs-number">1</span>)

            query.get().then(<span class="hljs-keyword">async</span> (querySnapshot: querySnapshot) =&gt; {
                <span class="hljs-keyword">if</span> (querySnapshot.empty) {
                    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Token Invalid"</span>)
                    <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"User Token invalid"</span> })
                } <span class="hljs-keyword">else</span> {
                    querySnapshot.forEach(<span class="hljs-keyword">async</span> (doc) =&gt; {
                        <span class="hljs-keyword">if</span> (doc.exists) {
                            <span class="hljs-keyword">const</span> { username } = doc.data()
                            <span class="hljs-keyword">const</span> user = { username }
                            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"User's token is valid"</span>)
                            <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">null</span>, user)
                        } <span class="hljs-keyword">else</span> {
                            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Token Invalid"</span>)
                            <span class="hljs-keyword">return</span> cb(<span class="hljs-literal">false</span>, { message: <span class="hljs-string">"User Token invalid"</span> })
                        }
                    })
                }
            })

        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to verify JWT"</span>)
            cb(e, <span class="hljs-literal">false</span>, { message: <span class="hljs-string">"Failed to verify JWT"</span> })
        }
    }))
}
</code></pre>
<p>The next hurdle was devising a notification system. It all started by registering devices from the front end that had permitted browser notifications using a /user/savetoken endpoint. Unsupported browsers simply established an SSE connection using a /user/sse/subscribe endpoint</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Save Token Endpoint</span>
validateFCMRouter.post(<span class="hljs-string">'/savetoken'</span>,
    saveFcmTokenSchema,
    <span class="hljs-keyword">async</span> (req: Request, res: Response) =&gt; {

        <span class="hljs-keyword">const</span> result = validationResult(req)


        <span class="hljs-keyword">if</span> (!result.isEmpty()) {
            sendResponse.badRequest(res, <span class="hljs-string">"Error"</span>, <span class="hljs-number">400</span>, { ...result.array() })
        }

        <span class="hljs-keyword">if</span> (result.isEmpty()) {
            <span class="hljs-keyword">const</span> { token, platform } = req.body
            <span class="hljs-keyword">const</span> tokenData = <span class="hljs-keyword">await</span> getTokenData(token)

            <span class="hljs-keyword">if</span> (tokenData.wasTokenReturned) {
                sendResponse.conflict(res, <span class="hljs-string">"Token already exists"</span>, <span class="hljs-number">409</span>, { ...tokenData.fcmData })
            }

            <span class="hljs-keyword">if</span> (!tokenData.wasTokenReturned) {
                <span class="hljs-keyword">const</span> newClientsFcmDoc = fcmDetailsCollection.doc()
                <span class="hljs-keyword">const</span> newClientsFcmDocId = newClientsFcmDoc.id
                <span class="hljs-keyword">let</span> hasUserSubscribedToTokens = <span class="hljs-keyword">await</span> subscribeTokenToTopics(token)

                <span class="hljs-keyword">if</span> (!hasUserSubscribedToTokens) {
                    sendResponse.internalError(res, <span class="hljs-string">"Failed to subscribe user to topics"</span>, <span class="hljs-number">500</span>)
                }

                <span class="hljs-keyword">if</span> (hasUserSubscribedToTokens) {
                    <span class="hljs-keyword">const</span> newClientsFcmDocData: fcmClientDocument = {
                        token: token,
                        platform: platform,
                        addedOn: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
                        messagelastreceivedon: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
                        subscribedTo: [<span class="hljs-string">"nameskit"</span>, <span class="hljs-string">"livestream"</span>, <span class="hljs-string">"omegle"</span>]
                    }

                    newClientsFcmDoc.set(newClientsFcmDocData).then(
                        <span class="hljs-function">() =&gt;</span> {
                            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"New client added to FCM collection"</span>)
                            sendResponse.success(res, <span class="hljs-string">"New client added to FCM collection"</span>, <span class="hljs-number">200</span>, { ...newClientsFcmDocData, id: newClientsFcmDocId })
                        }
                    ).catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
                        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error adding new client to FCM collection: "</span> + error)
                        sendResponse.internalError(res, <span class="hljs-string">"Error adding new client to FCM collection"</span>, <span class="hljs-number">500</span>)
                    })
                }


            }

        }

    })

<span class="hljs-comment">// SSE Endpoint</span>
adminRouter.get(<span class="hljs-string">'/sse/subscribe'</span>, <span class="hljs-function">(<span class="hljs-params">req: Request, res: Response</span>) =&gt;</span> {
    <span class="hljs-comment">//Set headers for the SSE</span>
    res.setHeader(<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'text/event-stream'</span>);
    res.setHeader(<span class="hljs-string">'Cache-Control'</span>, <span class="hljs-string">'no-cache'</span>);
    res.setHeader(<span class="hljs-string">'Connection'</span>, <span class="hljs-string">'keep-alive'</span>);

    <span class="hljs-comment">//Send something to establish connection</span>
    res.write(<span class="hljs-string">'data: {\"trigger\":\"init\"}\n\n'</span>);

    <span class="hljs-comment">//Add the response to the array of clients</span>
    adminSseClients.push(res)

    <span class="hljs-comment">//Remove the response from the array of clients when the client closes the connection</span>
    req.on(<span class="hljs-string">'close'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> indexOfDisconnectedClient = adminSseClients.indexOf(res)
        <span class="hljs-keyword">if</span> (indexOfDisconnectedClient != <span class="hljs-number">-1</span>) {
            adminSseClients.splice(indexOfDisconnectedClient, <span class="hljs-number">1</span>)
        }
    })
})
</code></pre>
<p>Supported devices would in turn receive tokens that were then linked to specific topics. These topics included a Live Stream topic (for dispatching notifications about Hyphonix's current streaming platform), an Omegle Topic (for alerting users when the tags Hyphonix was using changed), and a Name Skit Topic (for sending users instructions on actions to perform, such as mentioning the mark's name or engaging in playful antics).</p>
<p>For devices that couldn't support Firebase notifications, I had a backup plan using the custom sendSseEvent method.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Function for subscribing tokens to topics</span>
<span class="hljs-keyword">const</span> subscribeTokenToTopics = <span class="hljs-keyword">async</span> (token: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
            firebaseAdmin.messaging().subscribeToTopic(token, livestreamTopic),
            firebaseAdmin.messaging().subscribeToTopic(token, nameSkitTopic),
            firebaseAdmin.messaging().subscribeToTopic(token, omegleTopic)
        ]);
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to subscribe user to topics:'</span>, error);
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }
};
</code></pre>
<p>In the realm of FCM notifications, my approach was to have administrators update information in Firestore, triggering a Firebase Cloud Function upon the update. This function would then send messages to the respective topics.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Sample Firebase function for sending a notification when </span>
<span class="hljs-comment">//Administrators updated the livestream DB</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> sendLiveStreamNotification = firebaseFunctions.firestore.document(<span class="hljs-string">`<span class="hljs-subst">${livestreamCollectionString}</span>/<span class="hljs-subst">${liveStreamDocumentString}</span>`</span>)
    .onUpdate(
        <span class="hljs-keyword">async</span> (change) =&gt; {

            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">const</span> currentOmegleTags = <span class="hljs-keyword">await</span> getOmegleTags().then(
                    <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
                        <span class="hljs-keyword">if</span> (response &amp;&amp; <span class="hljs-keyword">typeof</span> response !== <span class="hljs-string">"boolean"</span>) {
                            <span class="hljs-keyword">return</span> response.currentOmegleTags.join(<span class="hljs-string">", "</span>)
                        } <span class="hljs-keyword">else</span> {
                            <span class="hljs-keyword">return</span> <span class="hljs-string">"[]"</span>
                        }
                    }).catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
                        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to get Omegle tags"</span>, error)
                        <span class="hljs-keyword">return</span> <span class="hljs-string">"[]"</span>
                    })
                <span class="hljs-keyword">const</span> newData = change.after.data()
                <span class="hljs-keyword">const</span> dataToBeIncluded = {
                    messageFromEvent: <span class="hljs-string">"liveStreamUpdate"</span>,
                    liveStreamData: <span class="hljs-built_in">JSON</span>.stringify({
                        streamingOn: newData.streamingOn,
                        activityType: newData.activityType,
                        streamingLink: newData.streamingLink
                    }),
                    currentOmegleTags
                }
                <span class="hljs-keyword">let</span> message: notificationMessage | <span class="hljs-built_in">boolean</span> = <span class="hljs-literal">false</span>;

                <span class="hljs-keyword">if</span> (newData.streamingOn !== <span class="hljs-string">"none"</span>) {

                    <span class="hljs-keyword">if</span> (newData.streamingOn === <span class="hljs-string">"youtube"</span>) {
                        message = generateYoutubeNotificationMessage(newData, dataToBeIncluded);
                    }

                    <span class="hljs-keyword">if</span> (newData.streamingOn === <span class="hljs-string">"twitch"</span>) {
                        message = generateTwitchNotificationMessage(newData, dataToBeIncluded);
                    }

                } <span class="hljs-keyword">else</span> {
                    message = {
                        topic: liveStreamTopicString,
                        notification: {
                            title: <span class="hljs-string">"Hx is no longer live!😔"</span>,
                            body: <span class="hljs-string">`Hx's stream has ended. Don't worry, make sure to check back tomorrow. He streams live everyday!`</span>
                        },
                        data: dataToBeIncluded
                    };
                }

                <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> message !== <span class="hljs-string">"boolean"</span>) {
                    <span class="hljs-keyword">await</span> initializedFirebaseAdmin.messaging().send(message);
                    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Successfully sent live notification"</span>, message);
                }
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to send Notification"</span>, error);
            }

        });
</code></pre>
<p>The final step involved creating user-facing endpoints, enabling client devices to update data when they loaded the app.</p>
<pre><code class="lang-typescript">userRouter.get(<span class="hljs-string">'/livestream/get'</span>, <span class="hljs-function">(<span class="hljs-params">req: Request, res: Response</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> livestreamCollection = firebaseDB.collection(process.env.DB_LIVESTREAM_COLLECTION <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)
    <span class="hljs-keyword">const</span> livestreamMasterDocument = livestreamCollection.doc(process.env.DB_LIVESTREAM_DOCUMENT_ID <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)

    livestreamMasterDocument.get().then(
        <span class="hljs-function">(<span class="hljs-params">snapshot</span>) =&gt;</span> {
            <span class="hljs-keyword">if</span> (snapshot.exists) {
                <span class="hljs-keyword">const</span> livestreamData = snapshot.data() <span class="hljs-keyword">as</span> liveStreamDocument
                sendResponse.success(res, <span class="hljs-string">"Success"</span>, <span class="hljs-number">200</span>, { ...livestreamData })
            } <span class="hljs-keyword">else</span> {
                sendResponse.notFound(res, <span class="hljs-string">"Livestream not found"</span>, <span class="hljs-number">404</span>)
            }
        }
    ).catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to get livestream"</span>, error)
    })
})
</code></pre>
<p>With all these pieces in place, I finally deployed the backend to Heroku and proceeded to test it with potential members of the Hyphonix community.</p>
<h3 id="heading-guerilla-testing-the-app-meeting-a-harsh-reality">Guerilla testing the App🔬:  Meeting a Harsh Reality</h3>
<p>Drawing from my previous experience as a Senior UX/UI designer, I firmly believe that user testing is the phase where assumptions meet their maker. Through Hyphonix's private Discord, I connected with members of his community and laid out the problem I aimed to tackle, presenting the app as the solution.</p>
<p>To ensure that users could grasp the solution seamlessly, I crafted an onboarding video that provided a comprehensive overview of the app's purpose.</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=9qkV6J45xK0">https://www.youtube.com/watch?v=9qkV6J45xK0</a></div>
<p> </p>
<p>Prominent feedback highlighted that users weren't enthusiastic about installing an app that bombarded them with notifications each time Hyphonix went live. An example of this feedback is displayed below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698763033451/308b9c40-54ab-42c5-83f9-8532b0f63192.png" alt class="image--center mx-auto" /></p>
<p>Due to my excessive confidence in my perspective as a Hyphonix community member, I overlooked the fact that other users might not share the same enthusiasm for receiving continuous notifications about Hyphonix's activities. It became evident that a pivot was necessary to provide users with more control over notifications, but my initial choice of the PWA platform posed challenges for such a pivot. Additionally, it's worth noting that at this stage, I had started to lose steam and interest in the project.  </p>
<p>As a result of this, I made the harsh decision to sunset the application.</p>
<h3 id="heading-lessons-learned-the-bumpy-ride-of-hx-skits-on-simplicity-testing-and-project-objectives">Lessons Learned 🧠: The Bumpy Ride of Hx Skits - On Simplicity, Testing, and Project Objectives</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://tenor.com/en-GB/view/congratulations-south-park-christian-rock-s7e9-compliments-gif-22099684">https://tenor.com/en-GB/view/congratulations-south-park-christian-rock-s7e9-compliments-gif-22099684</a></div>
<p> </p>
<p>Congratulations 🎉🎉🥳🥳! You've reached the end of this extensive blog post. As we conclude, I'd like to share some of the valuable lessons I've gained during this journey:</p>
<ol>
<li><p>Prioritize UX Testing in Requirements Gathering and Systems Architecture</p>
<p> Despite my background as a UX designer, I fell into the trap of not validating assumptions and understanding users' needs and goals before diving into code. This led me to create a solution that essentially led to nowhere.</p>
</li>
<li><p>Write Code with Maintenance in Mind, Not Just for Immediate Functionality</p>
<p> JavaScript's flexibility can sometimes result in code that works but is a tangled mess. This project has taught me the importance of focusing on writing elegant, scalable, and well-structured code. Not only is it easier to read later, but it's also simpler to scale and maintain.</p>
</li>
<li><p>Embrace Testing</p>
<p> Regardless of the programming language or framework, writing tests is a practice that encourages the creation of better code. Testing not only helps uncover bugs but also aids in decoupling complex code and identifying regression issues. Given the nature of some of my React solutions, particularly the DIY persistent global state, it was essential to adopt a "test early and test often" approach to identify potential breaking issues. Regrettably, I didn't prioritize this, driven by my passion for building.</p>
</li>
</ol>
<p>These lessons have been instrumental in my journey with Hx Skits, and I hope they resonate with your own experiences and projects. Thank you for joining me on this adventure, and I look forward to sharing more insights and experiences in the future.</p>
]]></content:encoded></item></channel></rss>