<?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[Jeff Blogmeyer]]></title><description><![CDATA[Jeff Blogmeyer]]></description><link>https://blog.jeffpohlmeyer.com</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 20:44:10 GMT</lastBuildDate><atom:link href="https://blog.jeffpohlmeyer.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Vue3 + Vite Global Component Registration]]></title><description><![CDATA[I'm working on a project in my new job and as part of the project I'm trying to register some components globally. As a company we're going to build out a component library and we want to be able to use some components without having to manually impo...]]></description><link>https://blog.jeffpohlmeyer.com/vue3-vite-global-component-registration</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/vue3-vite-global-component-registration</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[vite]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Tue, 12 Jul 2022 15:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657058458250/WV-imiXSE.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm working on a project in my new job and as part of the project I'm trying to register some components globally. As a company we're going to build out a component library and we want to be able to use some components without having to manually import them in each component. In Vue2 this was an <a target="_blank" href="https://v2.vuejs.org/v2/guide/components-registration.html#Automatic-Global-Registration-of-Base-Components">easy thing to do</a> but in Vue3 it's a bit more complex.</p>
<h1 id="heading-initial-attempt">Initial Attempt</h1>
<p>I found another <a target="_blank" href="https://dev.to/jirehnimes/how-to-register-global-components-in-vue-3-dynamically-3gl">blog post</a> that describes how to do this in Vue3 but the issue I ran into was an error indicating that "require is not defined". I don't know if this is because of JS modules, or the fact that I'm using TypeScript, or that we're using <a target="_blank" href="https://vitejs.dev/">Vite</a> for the project, but it doesn't work.</p>
<h1 id="heading-svelte-experience-to-the-rescue">Svelte Experience to the Rescue</h1>
<p>As I've noted in a <a target="_blank" href="https://jeffpohlmeyer.com/building-a-blog-with-sveltekit-tailwindcss-and-mdsvex">previous blog post</a> we can import "local" files into a Vite module using a <a target="_blank" href="https://vitejs.dev/guide/features.html#glob-import">glob import</a>. So what I'll end up doing is grabbing all files that match a given format, convert the file names to the format I want, and globally register all components.</p>
<h2 id="heading-grab-components">Grab Components</h2>
<p>Let's assume that the file structure of the project includes a <code>main.ts</code> file at the root of the project, as well as a <code>/src/lib/components</code> folder, which is where I'll store all of the components to be globally registered. This I also stole from Svelte, although we can't just use <code>import * from '$lib/components/...</code> like we can in Svelte. Regardless, this felt like a natural place to store these components.</p>
<p>We'll be able to simply grab all components using <code>const components = import.meta.globEager('/src/lib/components/&lt;Name Pattern&gt;.vue')</code></p>
<h2 id="heading-create-a-plugin">Create a plugin</h2>
<p>Let's also create a plugin to grab and register all of these components, and set it up at <code>/src/plugins/global-components.ts</code>. It is here we'll set up the script to grab and register all components as</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { App } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> register = (app: App&lt;Element&gt;): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
  <span class="hljs-comment">// Grab all components in `/src/lib/components/` that start with "Base"</span>
  <span class="hljs-keyword">const</span> components = <span class="hljs-keyword">import</span>.meta.globEager(<span class="hljs-string">'../lib/components/Base*.vue'</span>)
  <span class="hljs-built_in">Object</span>.entries(components).forEach(<span class="hljs-function">(<span class="hljs-params">[path, component]</span>) =&gt;</span> {
    <span class="hljs-comment">// Just get the file name itself, remove the .vue extension, and remove the "Base" at the front of the file name</span>
    <span class="hljs-keyword">const</span> pathSplit = path.split(<span class="hljs-string">'/'</span>)
    <span class="hljs-keyword">const</span> fileName = pathSplit[pathSplit.length - <span class="hljs-number">1</span>].split(<span class="hljs-string">'.vue'</span>)[<span class="hljs-number">0</span>].split(<span class="hljs-string">'Base'</span>)[<span class="hljs-number">1</span>]

    <span class="hljs-comment">// Convert to kebab-case and register with a "jvp-" prefix</span>
    <span class="hljs-keyword">const</span> kebab = fileName.replace(<span class="hljs-regexp">/([a-z0–9])([A-Z])/g</span>, <span class="hljs-string">'$1-$2'</span>).toLowerCase()
    app.component(<span class="hljs-string">`jvp-<span class="hljs-subst">${kebab}</span>`</span>, component.default.render())
  })
}
</code></pre>
<p>The comments <em>should</em> be fairly self-explanatory, but just in case they're not this is what is happening</p>
<ul>
<li>Fetch all components that live in <code>/src/lib/components</code> that begin with <code>Base</code> in the file name</li>
<li>For each of these components<ul>
<li>Grab the file name by splitting the path on the <code>/</code> character, and getting the last entry in the resulting array.</li>
<li>Remove the <code>.vue</code> extension as well as the <code>Base</code> prefix</li>
<li>Convert the PascalCase file name to kebab-case using your favorite method (my Googling led me to https://medium.com/@mattkenefick/snippets-in-javascript-converting-pascalcase-to-kebab-case-426c80672abc)</li>
<li>Register the component with the prefix you want (I'm using "jvp" for these)</li>
</ul>
</li>
</ul>
<p>There are likely better ways to handle the file name search and string manipulation but this was a simple way for me to do it since I'm prescribing the naming convention myself.</p>
<h2 id="heading-use-plugin-in-maints">Use plugin in <code>main.ts</code></h2>
<p>Now that this is set up we need to use it in our <code>main.ts</code> file. This is a fairly simple process. First we need to import our newly created <code>register</code> function and execute it with the <code>app</code> instance that we create in <code>main.ts</code>. It should look something like this</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.vue'</span>
<span class="hljs-keyword">import</span> { createApp } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">import</span> { register } <span class="hljs-keyword">from</span> <span class="hljs-string">'./plugins/global-components'</span>

<span class="hljs-keyword">const</span> app = createApp(App)
register(app)
</code></pre>
<p>and we should now have access to any and all components created in <code>/src/lib/components/Base&lt;NameRemainder&gt;.vue</code> throughout our app as <code>&lt;jvp-name-remainder /&gt;</code> without having to import it directly. Again, there may be a simpler/easier way to handle this functionality, but this is the first way I tried that actually worked so I'm just going to leave this here.</p>
]]></content:encoded></item><item><title><![CDATA[Full SvelteKit Component JavaScript and Markup]]></title><description><![CDATA[In the last blog post I built the functionality to handle form submission for the file upload itself, but we still didn't have a way to actually handle the files. In this post I'll set up the very basic markup as well as the accompanying script funct...]]></description><link>https://blog.jeffpohlmeyer.com/full-sveltekit-component-javascript-and-markup</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/full-sveltekit-component-javascript-and-markup</guid><category><![CDATA[Svelte]]></category><category><![CDATA[Sveltekit]]></category><category><![CDATA[Google Drive]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Mon, 11 Jul 2022 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/bYiw48KLbmw/upload/v1655927570522/AJ-dOjg3V.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/handling-form-submission-and-sending-files-to-sveltekit-endpoints">last blog post</a> I built the functionality to handle form submission for the file upload itself, but we still didn't have a way to actually handle the files. In this post I'll set up the very basic markup as well as the accompanying <code>script</code> functionality required.</p>
<h1 id="heading-dependencies">Dependencies</h1>
<p>The only extra dependency needed for this section, as mentioned in the last post, is <a target="_blank" href="https://www.npmjs.com/package/svelte-file-dropzone">Svelte File Dropzone</a>, which can be installed by simply typing <code>npm i -D svelte-file-dropzone</code>.</p>
<h1 id="heading-script-setup">Script Setup</h1>
<p>I'm going to use basic functionality for Dropzone, as indicated in the <a target="_blank" href="https://github.com/thecodejack/svelte-file-dropzone#usage">documentation</a> with a couple of extra things:</p>
<ul>
<li>I will be adding a filter to not add files that include names already in the list of accepted files</li>
<li>I will be wrapping the Dropzone in a <code>&lt;form&gt;</code> tag because that just feels more natural to me.</li>
<li>I will be adding functionality to remove one or all of the accepted files</li>
</ul>
<p>The <code>script</code> from the last blog post just had the <code>files</code> object and the <code>handleSubmit</code> method. Now I'll add a few more things.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/routes/index.svelte</span>

&lt;script&gt;
  <span class="hljs-keyword">let</span> files = {
    <span class="hljs-attr">accepted</span>: [],
    <span class="hljs-attr">rejected</span>: []
  };

  <span class="hljs-keyword">const</span> accept = [<span class="hljs-string">'application/pdf'</span>];
  <span class="hljs-keyword">const</span> maxSize = <span class="hljs-number">2500000</span>;
  <span class="hljs-keyword">let</span> loading = <span class="hljs-literal">false</span>;

  <span class="hljs-keyword">const</span> handleFilesSelect = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { acceptedFiles, fileRejections } = e.detail;
    files.accepted = [
      ...files.accepted
        .filter(<span class="hljs-function">(<span class="hljs-params">file</span>) =&gt;</span> !acceptedFiles.map(<span class="hljs-function">(<span class="hljs-params">f</span>) =&gt;</span> f.name).includes(file.name)),
      ...acceptedFiles
    ];
  files.rejected = [...files.rejected, ...fileRejections];
  };

  <span class="hljs-keyword">const</span> handleRemoveFile = <span class="hljs-function">(<span class="hljs-params">e, index</span>) =&gt;</span> {
    files.accepted.splice(index, <span class="hljs-number">1</span>);
    files.accepted = [...files.accepted];
  };

  <span class="hljs-keyword">const</span> handleRemoveAll = <span class="hljs-function">() =&gt;</span> {
    files.accepted = [];
  };
&lt;/script&gt;
</code></pre>
<h1 id="heading-markup">Markup</h1>
<p>The markup for this is going to be wholly unspectacular in that I will not be including any  styling whatsoever, but functionally this will work.</p>
<pre><code class="lang-svelte">&lt;main&gt;
  &lt;form on:submit|preventDefault={handleSubmit}&gt;
    &lt;Dropzone
      {accept}
      {maxSize}
      on:drop={handleFilesSelect}
    /&gt;
    {#if files.accepted.length &gt; 0}
      &lt;div&gt;
        &lt;h3&gt;Files to be Uploaded&lt;/h3&gt;
        &lt;ul&gt;
          {#each files.accepted as item, index}
            &lt;li&gt;
              {item.name}
              &lt;button type="button" on:click={(e) =&gt; handleRemoveFile(e, index)}&gt;
                x
              &lt;/button&gt;
            &lt;/li&gt;
          {/each}
        &lt;/ul&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;button type="submit" disabled={loading}&gt;
          {#if loading}
            ...Loading
          {:else}
            &lt;span&gt;Submit&lt;/span&gt;
          {/if}
        &lt;/button&gt;
        &lt;button type="button" disabled={loading} on:click={handleRemoveAll}&gt;
          Remove All
        &lt;/button&gt;
      &lt;/div&gt;
    {/if}
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>
<h3 id="heading-style">Style</h3>
<p>As an aside, I also added a little bit of styling to make it centered</p>
<pre><code class="lang-css">&lt;<span class="hljs-selector-tag">style</span>&gt;
    <span class="hljs-selector-tag">main</span> {
        <span class="hljs-attribute">display</span>: grid;
        <span class="hljs-attribute">place-content</span>: center;
        <span class="hljs-attribute">height</span>: <span class="hljs-number">100vh</span>;
    }
&lt;/<span class="hljs-selector-tag">style</span>&gt;
</code></pre>
<p>The resulting page doesn't look great, but if you click on the box and choose the files you want to upload it should look like this
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655926145649/v_JZb_aIO.png" alt="Screen Shot 2022-06-22 at 3.27.10 PM.png" />
Then if you click submit you'll see, once the process is completed, that the files show up in the assigned folder
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655926203710/kwf7sbbdv.png" alt="Screen Shot 2022-06-22 at 3.29.39 PM.png" /></p>
<h1 id="heading-summary">Summary</h1>
<p>That's it. The functionality is nothing terribly complicated when you look at everything in the aggregate, but all of the moving pieces can be a bit complex. Like I mentioned, I spent quite a bit of time trying to figure all of this out for the first time, but now that I've gotten it working I wanted to share it with you all.
See you next time.</p>
]]></content:encoded></item><item><title><![CDATA[Handling Form Submission and Sending Files to SvelteKit Endpoints]]></title><description><![CDATA[In the last blog post I touched on setting up a Google Drive folder to serve as the location of our uploads as well as creating the necessary endpoints for uploading and renaming the files.
In this post I'll set up functions to send any necessary fil...]]></description><link>https://blog.jeffpohlmeyer.com/handling-form-submission-and-sending-files-to-sveltekit-endpoints</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/handling-form-submission-and-sending-files-to-sveltekit-endpoints</guid><category><![CDATA[Svelte]]></category><category><![CDATA[Sveltekit]]></category><category><![CDATA[Google Drive]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Wed, 06 Jul 2022 15:00:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/ldDmTgf89gU/upload/v1655927501412/FZ-IbYczr.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/google-drive-folder-setup-and-sveltekit-uploadrename-endpoints">last blog post</a> I touched on setting up a Google Drive folder to serve as the location of our uploads as well as creating the necessary endpoints for uploading and renaming the files.</p>
<p>In this post I'll set up functions to send any necessary files to these endpoints to upload. Then in the last post I'll add the markup to handle file acquisition.</p>
<h1 id="heading-sveltekit-component-script">SvelteKit Component Script</h1>
<p>The functionality for this script is fairly complex, so most of this post will handle this logic.</p>
<h2 id="heading-variable-instantiation">Variable Instantiation</h2>
<p>The only thing we'll really need at this point in time is an object to hold accepted and rejected files. I'll be using <a target="_blank" href="https://www.npmjs.com/package/svelte-file-dropzone">Svelte File Dropzone</a> for file acquisition and the setup used in the docs is as follows</p>
<pre><code><span class="hljs-operator">&lt;</span>script<span class="hljs-operator">&gt;</span>
  let files <span class="hljs-operator">=</span> {
    accepted: [],
    rejected: []
  }
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>script<span class="hljs-operator">&gt;</span>
</code></pre><p>That's it. From the perspective of basic functionality this is all we'll need. When I add in Dropzone-specific stuff I'll add more variable instantiation, but I'll leave that for the next post.</p>
<h2 id="heading-handling-form-submission">Handling Form Submission</h2>
<p>While I was working on this for my client, this was the feature on which I spent the most time. I'm not terribly familiar with handling readable streams, transmitting them to an endpoint as the correct type, etc. On top of that, I was trying to make the submit button disabled and add a loading spinner and then once all of the files were uploaded I would then add a notification and stop the loading spinner. This would have been less of an issue with a single file, but I wanted to allow for multiple file submissions at the same time. I found a <a target="_blank" href="https://stackoverflow.com/a/67484772/1016708">great Stack Overflow answer</a> about how to handle this, and it effectively wraps the <code>reader.onload</code> functionality in a Promise and you can just await a <code>Promise.all</code> for all of the necessary files.</p>
<h3 id="heading-file-reader">File Reader</h3>
<p>This will be broken up into two smaller sections:</p>
<ul>
<li>The file reader</li>
<li>The rest of <code>handleSubmit</code></li>
</ul>
<p>Following along a bit of the aforementioned Stack Overflow answer, I'm going to share the code in its entirety and discuss after</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> filePromises = files.accepted.map(<span class="hljs-function">(<span class="hljs-params">file</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> reader = <span class="hljs-keyword">new</span> FileReader();
        reader.onloadend = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/upload-files'</span>, {
                    <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
                    <span class="hljs-attr">body</span>: reader.result,
                    <span class="hljs-attr">headers</span>: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/pdf'</span> }
                });
                <span class="hljs-keyword">const</span> { id } = <span class="hljs-keyword">await</span> response.json();
                <span class="hljs-keyword">const</span> body = <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">name</span>: file.name, <span class="hljs-attr">fileId</span>: id });
                <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/rename-file'</span>, {
                    <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
                    body
                });
                resolve();
            } <span class="hljs-keyword">catch</span> (err) {
                reject(err);
            }
        };
        reader.onerror = <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> reject(error);
        reader.readAsArrayBuffer(file);
    });
});
</code></pre>
<p>The general flow of this method is as follows (for each file in the <code>files.accepted</code> array)</p>
<ul>
<li>Create a new <code>FileReader</code></li>
<li>When the file is fully loaded, try sending the <code>reader.result</code> element to the previously-created endpoint with the correct <code>Content-Type</code> header</li>
<li>Pull the <code>id</code> value from the endpoint that we'll use to rename the file</li>
<li>Create and stringify the submission body to rename the file</li>
<li>Send the data to rename the file using that previously-created endpoint</li>
</ul>
<h3 id="heading-handle-submission">Handle Submission</h3>
<p>I'm going to add more functionality than is necessary to the <code>handleSubmit</code> method and will explain why after</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> () =&gt; {
    loading = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">const</span> filePromises = files.accepted.map(<span class="hljs-function">(<span class="hljs-params">file</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> reader = <span class="hljs-keyword">new</span> FileReader();
            reader.onloadend = <span class="hljs-keyword">async</span> () =&gt; {
                <span class="hljs-keyword">try</span> {
                    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/upload-files'</span>, {
                        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
                        <span class="hljs-attr">body</span>: reader.result,
                        <span class="hljs-attr">headers</span>: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/pdf'</span> }
                    });
                    <span class="hljs-keyword">const</span> { id } = <span class="hljs-keyword">await</span> response.json();
                    <span class="hljs-keyword">const</span> body = <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">name</span>: file.name, <span class="hljs-attr">fileId</span>: id });
                    <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/rename-file'</span>, {
                        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
                        body
                    });
                    resolve();
                } <span class="hljs-keyword">catch</span> (err) {
                    reject(err);
                }
            };
            reader.onerror = <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> reject(error);
            reader.readAsArrayBuffer(file);
        });
    });
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(filePromises);
        files.accepted = [];

        <span class="hljs-comment">/* Extra functionality for fun */</span>
        notification = <span class="hljs-literal">true</span>;
        <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
            notification = <span class="hljs-literal">false</span>;
        }, <span class="hljs-number">10000</span>);
        fetch(<span class="hljs-string">'/api/send-uploaded-message'</span>, {
            <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">messageType</span>: <span class="hljs-string">'success'</span> })
        });
        <span class="hljs-comment">/***************************/</span>
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err);
    } <span class="hljs-keyword">finally</span> {
        loading = <span class="hljs-literal">false</span>;
    }
};
</code></pre>
<p>I've added in a bit of extra functionality for demonstration purposes.</p>
<ul>
<li>If we want to have a loading spinner that is displayed based on a variable called something like <code>loading</code> then we can instantiate it when the function is triggered and then stop it in the <code>finally</code> block of the <code>try/catch/finally</code> group</li>
<li>If we want to include a notification on the page then we can do that after the <code>Promise.all</code> evaluation<ul>
<li>I set up a <code>setTimeout</code> to close the notification automatically after 10 seconds</li>
</ul>
</li>
<li>We can also have a separate endpoint to notify whomever it's worth notifying upon successful completion of the file upload, which I've added as well</li>
</ul>
<h1 id="heading-summary">Summary</h1>
<p>These last two posts have been a bit shorter, but I wanted to keep them to the point and not clutter with a lot of information. In the next (last) blog post I'll add functionality to handle for file management within the component itself.</p>
]]></content:encoded></item><item><title><![CDATA[Google Drive Folder Setup and SvelteKit Upload/Rename Endpoints]]></title><description><![CDATA[In the last blog post I set up a service account to authenticate for Google Drive's API, encrypted the resulting .json file, and set up functionality to decrypt said file to use for the purpose of actual authentication.
In this post I will show you h...]]></description><link>https://blog.jeffpohlmeyer.com/google-drive-folder-setup-and-sveltekit-uploadrename-endpoints</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/google-drive-folder-setup-and-sveltekit-uploadrename-endpoints</guid><category><![CDATA[Sveltekit]]></category><category><![CDATA[Svelte]]></category><category><![CDATA[Google Drive]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Wed, 29 Jun 2022 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/Z19vToWBDIc/upload/v1655927425441/WkuHJGd0M.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/setting-up-a-service-account-for-uploading-to-google-drive-with-sveltekit">last blog post</a> I set up a service account to authenticate for Google Drive's API, encrypted the resulting <code>.json</code> file, and set up functionality to decrypt said file to use for the purpose of actual authentication.</p>
<p>In this post I will show you how to set up the folder in Google Drive, as well as how to share it correctly, and then add SvelteKit endpoints for handling file uploads and saving them in the necessary folder.</p>
<h1 id="heading-google-drive-folder-setup">Google Drive Folder Setup</h1>
<p>The first thing you need to do is go to https://drive.google.com/drive/my-drive and create a folder where the files will be stored, which I will set name 'Hashnode File Upload'. You'll then want to open that folder and grab the ID of the folder, which should show up after the <code>/folders/</code> location in the URL.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655912528329/cUILsh7f6.png" alt="Screen Shot 2022-06-22 at 11.41.29 AM.png" />
Copy this value and store it in the previously noted <code>.env</code> file as VITE_GOOGLE_DRIVE_FOLDER_ID. After the encryption information and this, our <code>.env</code> file should look like</p>
<pre><code class="lang-env">VITE_SERVICE_ENCRYPTION_IV=t8bcKDaNPRSGH3no
VITE_SERVICE_ENCRYPTION_KEY=hiHa4OLm9xLchadg
VITE_GOOGLE_DRIVE_FOLDER_ID=1StDQlW29iO72tlq6APa8DXT2hegx742l
</code></pre>
<h2 id="heading-sharing-capabilities">Sharing Capabilities</h2>
<p>In order to allow for the created service account to upload to this folder, you also need to add the email address set up in the service account <code>.json</code> file as an editor to the folder. To remind you, the email address in use for this project would be <code>file-upload@sample-project-for-hashnode.iam.gserviceaccount.com</code>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655924210708/M6ZyGgPdu.png" alt="Screen Shot 2022-06-21 at 5.30.12 PM.png" />
Go back to the parent folder of the one for which you grabbed the ID and right-click on the folder, and select "Share"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655924370648/n1Qoz2Qau.png" alt="Screen Shot 2022-06-22 at 2.59.18 PM.png" />
Then you simply need to add this email address as an editor, and click "Share"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655924578157/_iKCHm57E.png" alt="Screen Shot 2022-06-22 at 3.02.43 PM.png" /></p>
<h1 id="heading-sveltekit-file-upload-endpoint">SvelteKit File Upload Endpoint</h1>
<p>Now that this is done, I will create a file within the <code>src/routes</code> folder to handle file uploads.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/routes/upload-files.js</span>

<span class="hljs-keyword">import</span> { service } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/service'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">{ request }</span>) </span>{
    <span class="hljs-keyword">const</span> body = request.body;
    <span class="hljs-keyword">const</span> drive = service();
    <span class="hljs-keyword">const</span> folderId = <span class="hljs-keyword">import</span>.meta.env.VITE_GOOGLE_DRIVE_FOLDER_ID;
    <span class="hljs-keyword">const</span> file = <span class="hljs-keyword">await</span> drive.files.create({
        <span class="hljs-attr">resource</span>: { <span class="hljs-attr">parents</span>: [folderId] },
        <span class="hljs-attr">media</span>: { <span class="hljs-attr">mimeType</span>: <span class="hljs-string">'application/pdf'</span>, body },
        <span class="hljs-attr">fields</span>: <span class="hljs-string">'id'</span>
    });
    <span class="hljs-keyword">const</span> { id } = file.data;

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>, <span class="hljs-attr">body</span>: { id } };
}
</code></pre>
<p>Let's look a bit into what's happening with this function.</p>
<ul>
<li>Since I want to upload a file instead of handle JSON I'm using <code>request.body</code> instead of the more typical <code>await request.json()</code></li>
<li>The <code>drive</code> and <code>folderId</code> lines are fairly self-explanatory, with the <code>service</code> function coming from what was built in the last post</li>
<li>The methodology of uploading is done via the <code>await drive.files.create</code> method. The argument for this function was pulled from <a target="_blank" href="https://www.labnol.org/google-api-service-account-220404#5b-write-file-uploader">this post</a> with some slight modifications.<ul>
<li>I don't need the name because I'm going to be creating a separate function to rename uploaded files</li>
<li>Since I'm not using the file system, I didn't use the <code>fs</code> package or anything like that. The <code>request.body</code> itself is a readable stream of just the file contents so I just use that as the body.</li>
<li>I'm assuming that this is for a PDF but this can be used for other file types (for example <code>image/*</code>).</li>
</ul>
</li>
<li>The <code>id</code> that is returned from <code>file.data</code> and returned from the function itself is for the purpose of eventually renaming the file.<ul>
<li>The reason I'm handling the name in this way is because I don't know how to access the file name from the readable stream itself</li>
</ul>
</li>
</ul>
<h1 id="heading-sveltekit-rename-file-endpoint">SvelteKit Rename File Endpoint</h1>
<p>If you go look at the uploaded file you'll see that the name simply reads "undefined". Like I said, I don't know how to grab the file name from the readable stream, but what I've done instead is return the <code>id</code> from the uploaded file, which we will use in a newly-created <code>/src/routes/rename-file.js</code> endpoint.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/routes/rename-file.js</span>

<span class="hljs-keyword">import</span> service <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/service'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">{ request }</span>) </span>{
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> request.json();
    <span class="hljs-keyword">const</span> drive = service();
    <span class="hljs-keyword">const</span> { fileId, ...body } = data;
    <span class="hljs-keyword">await</span> drive.files.update({ fileId, <span class="hljs-attr">resource</span>: body });
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">204</span> };
}
</code></pre>
<p>This is a fairly simple function as we can see. I'm simply extracting the <code>fileId</code> from the request data, and storing the remaining information as a constant called <code>body</code> using the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">JavaScript spread operator</a>.</p>
<h1 id="heading-summary">Summary</h1>
<p>That's it for this short blog post. In the next one I'll work on the in-component functionality where we send any/all files to these routes for upload and renaming.</p>
]]></content:encoded></item><item><title><![CDATA[Setting up a service account for uploading to Google Drive with SvelteKit]]></title><description><![CDATA[Note: all credentials have already been destroyed so there is no need to let me know to not share credential information online.
I just finished working on a project for a client/friend in which she wanted to have a page where she could have forms av...]]></description><link>https://blog.jeffpohlmeyer.com/setting-up-a-service-account-for-uploading-to-google-drive-with-sveltekit</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/setting-up-a-service-account-for-uploading-to-google-drive-with-sveltekit</guid><category><![CDATA[Svelte]]></category><category><![CDATA[Sveltekit]]></category><category><![CDATA[Google Drive]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Fri, 24 Jun 2022 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/RMIsZlv8qv4/upload/v1655927331918/8QBOjnheT.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> all credentials have already been destroyed so there is no need to let me know to not share credential information online.</p>
<p>I just finished working on a project for a client/friend in which she wanted to have a page where she could have forms available for her own clients to download which they would then upload, also on her site. The site itself is a static site deployed to Netlify and built using SvelteKit, so right away I was going to be building this functionality in a server route.</p>
<p>The next decision that had to be made was the file storage. S3 is an easy place to handle static content, but my friend isn't a technical person and setting up Amazon IAM for a non-technical person, as well as assigning access within IAM, is no walk in the park. We had already been sharing files and such using Google Drive so I figured this would be the best place to handle this functionality going forward.</p>
<h1 id="heading-google-drive-api">Google Drive API</h1>
<h2 id="heading-enabling-the-api">Enabling the API</h2>
<p>The first thing we need to do is enable the Google Drive API for this project. To do this, first <a target="_blank" href="https://console.cloud.google.com/projectcreate">create a project</a> and call it whatever you like.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846582514/Z5tbBG_zM.png" alt="Screen Shot 2022-06-21 at 5.22.32 PM.png" />
Then you'll go to the left menu and hover over "APIs &amp; Services" and click on "Enabled APIs &amp; Services"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655923792562/ht-dIubJR.png" alt="Screen Shot 2022-06-22 at 2.49.17 PM.png" />
After clicking on this, you'll click on "ENABLE APIS AND SERVICES"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655923822794/hQAc_ZhNZ.png" alt="Screen Shot 2022-06-22 at 2.50.08 PM.png" />
and search for the Google Drive API. Once you find it and click on it, you then click the button that says "ENABLE".
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655923883927/IuyGN_QLc.png" alt="Screen Shot 2022-06-22 at 2.51.11 PM.png" />
Once this API is enabled you'll need to set up credentials to access the API.</p>
<h2 id="heading-setting-up-a-service-account">Setting up a service account</h2>
<p>If you go into Google's developer console, you can find a <a target="_blank" href="https://developers.google.com/drive/api/quickstart/nodejs">quickstart for Node.js for the Drive API</a>. The authentication needed for this quickstart is a <a target="_blank" href="https://cloud.google.com/iam/docs/service-accounts">service account</a>.
To set up a service account, open the left-hand navigation drawer, hover over "IAM &amp; Admin" and you will see an option for <a target="_blank" href="https://console.cloud.google.com/iam-admin/serviceaccounts?project=sample-project-for-hashnode">service accounts</a>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655924005747/-dXAlESJA.png" alt="Screen Shot 2022-06-22 at 2.52.47 PM.png" />
You will then want to click on "CREATE SERVICE ACCOUNT" at the top of the page
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846680990/eZuphynha.png" alt="Screen Shot 2022-06-21 at 5.24.23 PM.png" />
and name it whatever you want (I'm choosing to call it File Upload). Then click "DONE"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846746389/d9nKwzFtz.png" alt="Screen Shot 2022-06-21 at 5.25.33 PM.png" />
Then we'll be directed to the list of service accounts for the current project, where we will click on the field in the "Email" column
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846820271/VsaC0YNp3.png" alt="Screen Shot 2022-06-21 at 5.26.53 PM.png" />
Once here, we will click on "KEYS" in the sub-menu at the top of the page
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846867268/3v1Zc6tBV.png" alt="Screen Shot 2022-06-21 at 5.27.37 PM.png" />
Then click on the "ADD KEY" dropdown menu and click on "Create New Key"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846903112/W7jHJlcFX.png" alt="Screen Shot 2022-06-21 at 5.28.14 PM.png" />
Make sure the JSON Key type is selected and click on "CREATE"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655846927088/O2HhvNXcU.png" alt="Screen Shot 2022-06-21 at 5.28.41 PM.png" />
If we then open the resulting <code>.json</code> file that is downloaded it will look something like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655847021316/BNAezmFDE.png" alt="Screen Shot 2022-06-21 at 5.30.12 PM.png" /></p>
<h2 id="heading-storing-the-json-file">Storing the <code>.json</code> file</h2>
<p>If we were using this service account for a generic Node.js backend project we could just use the file as is. Since I want to deploy this project as a static site in Netlify, though, I'm unable to upload an entire <code>.json</code> file.
I tried stringifying the file to just save it as an environment variable that I would then parse and use, but AWS Lambda has a 4KB limit on environment variables and this stringified JSON file goes over that limit.</p>
<h3 id="heading-encrypting-the-file">Encrypting the file</h3>
<p>There is a very helpful article on <a target="_blank" href="https://vercel.com/support/articles/how-do-i-workaround-vercel-s-4-kb-environment-variables-limit">Vercel's support site</a> that describes encrypting the file and uploading the <code>SERVICE_ENCRYPTION_IV</code> and <code>SERVICE_ENCRYPTION_KEY</code> values as environment variables. Since the data itself is encrypted you can save it in your code and commit it to GitHub and then instead store the decryption information as environment variables.
So, the first thing we need to do is go to a <a target="_blank" href="https://www.devglan.com/online-tools/aes-encryption-decryption">site for AES encryption</a> and upload our recently saved <code>.json</code> file. Then we select CBC mode, and enter two 16-character strings, generated from <a target="_blank" href="https://www.random.org/strings/?num=2&amp;len=16&amp;digits=on&amp;upperalpha=on&amp;loweralpha=on&amp;unique=on&amp;format=html&amp;rnd=new">random.org</a>. We'll save the first one as the <code>IV</code> value and the second as the <code>KEY</code> value. Make sure to save them in a safe place as we'll use them later (this is likely a .env file in your project's root directory).
Then click on "Encrypt" and you should get the following
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655847616426/f_NuZ2DWI.png" alt="Screen Shot 2022-06-21 at 5.39.57 PM.png" /></p>
<h2 id="heading-storing-the-relevant-information-in-our-project">Storing the relevant information in our project</h2>
<p>We then want to create a <code>service-account.enc.js</code> file in the <code>/src/lib</code> folder, and in that folder we'll simply write</p>
<pre><code><span class="hljs-attribute">export</span> const encrypted = 'Okf<span class="hljs-number">7</span>DzlxsDj<span class="hljs-number">0</span>KoE<span class="hljs-number">2</span>HfCC<span class="hljs-number">5</span>AVgfZeLarc<span class="hljs-number">1</span>hqX<span class="hljs-number">2</span>lLtY<span class="hljs-number">68</span>ctVere/ArTUAyuVJ<span class="hljs-number">8</span>jVOG<span class="hljs-number">77</span>OGWTbFQnuEmsC<span class="hljs-number">1</span>qrPX<span class="hljs-number">5</span>agwZLaiTYar<span class="hljs-number">24</span>oHwDxTM<span class="hljs-number">3</span>SFitoBGP+<span class="hljs-number">26</span>EgpMYCBulz<span class="hljs-number">7</span>lH<span class="hljs-number">59</span>vy<span class="hljs-number">18</span>+<span class="hljs-number">0</span>epWce<span class="hljs-number">13</span>vwaFiXB<span class="hljs-number">8</span>e<span class="hljs-number">3</span>Jukl<span class="hljs-number">5</span>dxvsLxu/TUNAU<span class="hljs-number">0</span>vmNScvYe<span class="hljs-number">7</span>mBKKuEWn<span class="hljs-number">6</span>hhW+NAkHojsB+pMkwsZa<span class="hljs-number">43</span>E<span class="hljs-number">3</span>lIXOrF<span class="hljs-number">9</span>nk/QmSGBEVuPOwroQhUw<span class="hljs-number">47</span>vx<span class="hljs-number">6</span>b<span class="hljs-number">2</span>oXZ<span class="hljs-number">9</span>zdcZVBSEQUCM<span class="hljs-number">9</span>/ZIHD/oqfAG<span class="hljs-number">1</span>u<span class="hljs-number">8</span>A<span class="hljs-number">494</span>H<span class="hljs-number">4</span>+Chh<span class="hljs-number">4</span>fxpud+<span class="hljs-number">752</span>Sm<span class="hljs-number">3</span>Z+Tp<span class="hljs-number">7</span>yWF<span class="hljs-number">4</span>/nFlxfPohbrLeLcRav/r<span class="hljs-number">7</span>r<span class="hljs-number">8</span>xRLNkA<span class="hljs-number">3</span>YGEQQFh<span class="hljs-number">1</span>tArmNa<span class="hljs-number">0</span>GjiFGcH<span class="hljs-number">7</span>ymboINFIVJaBXVc<span class="hljs-number">00</span>XPJuaQ<span class="hljs-number">1</span>p<span class="hljs-number">6</span>MmHbHXJIxgaGvKEmxMsFef<span class="hljs-number">35</span>Xi+rRlZICZmp<span class="hljs-number">0</span>/ss/i/<span class="hljs-number">3</span>yV<span class="hljs-number">0</span>wLatobN<span class="hljs-number">0</span>UzULjIZj<span class="hljs-number">9</span>gfIlRM+<span class="hljs-number">3</span>aNOW<span class="hljs-number">5</span>uqYcVUBOcyn<span class="hljs-number">49</span>cw<span class="hljs-number">2</span>H<span class="hljs-number">648</span>sEkyTs<span class="hljs-number">8</span>Z<span class="hljs-number">4</span>vLfJiN<span class="hljs-number">3</span>orjvIPTrxdSfO<span class="hljs-number">8</span>FagFcV<span class="hljs-number">2</span>P/pTZicfsVzCbN<span class="hljs-number">3</span>a<span class="hljs-number">8</span>xOXRSsxOktnWCKZHXuXzog<span class="hljs-number">8</span>I<span class="hljs-number">6</span>RLrkznzVDSxjPm<span class="hljs-number">54</span>uXrGPWTUPw+X<span class="hljs-number">6</span>EG<span class="hljs-number">0</span>RvSeV<span class="hljs-number">1</span>w<span class="hljs-number">3</span>A<span class="hljs-number">4</span>F/FVBedJT/sm<span class="hljs-number">6</span>mcfF<span class="hljs-number">1</span>yj<span class="hljs-number">8</span>HP+lT<span class="hljs-number">9</span>qvujF<span class="hljs-number">7</span>lpkqYaLnmoBo<span class="hljs-number">7</span>FkbdFZq<span class="hljs-number">6</span>Ra+F<span class="hljs-number">79</span>VcdWOx<span class="hljs-number">4</span>aC<span class="hljs-number">5</span>a+<span class="hljs-number">0</span>ibu<span class="hljs-number">2</span>q<span class="hljs-number">2</span>pnQ<span class="hljs-number">9</span>Fiw<span class="hljs-number">58</span>cV<span class="hljs-number">87</span>tRefxrCNaW+/JrOlpZ+mmCgyL/<span class="hljs-number">1</span>zJpFabzQ<span class="hljs-number">0</span>sCIzx<span class="hljs-number">1</span>a<span class="hljs-number">5</span>Q<span class="hljs-number">7</span>FHocNttX<span class="hljs-number">8</span>pSwjSgD<span class="hljs-number">3</span>cM<span class="hljs-number">8</span>ZgKCPyO<span class="hljs-number">6</span>UBCmFzfgJ+AY<span class="hljs-number">2</span>ksqYDKxdoZh<span class="hljs-number">84</span>JHFyKmzmbb<span class="hljs-number">20</span>qeQF<span class="hljs-number">2</span>M<span class="hljs-number">6</span>Ux+lumxJYyQlC<span class="hljs-number">6</span>MfFE<span class="hljs-number">95</span>g<span class="hljs-number">5</span>HHXtdOGpYrB<span class="hljs-number">83</span>LrcK<span class="hljs-number">9</span>OShjcRkCMoIqOHlG<span class="hljs-number">8</span>CbyhBAVQgsHzB<span class="hljs-number">09</span>HzTkMgO<span class="hljs-number">0</span>x/sfnvryNb<span class="hljs-number">7</span>r<span class="hljs-number">0</span>j<span class="hljs-number">7</span>hgDFFiWAeIr/<span class="hljs-number">0</span>qXIOjOJ<span class="hljs-number">3</span>npF/E<span class="hljs-number">3</span>OTOZ<span class="hljs-number">8</span>Q<span class="hljs-number">8</span>dTuDQ<span class="hljs-number">75</span>Wd<span class="hljs-number">3</span>fClvkq<span class="hljs-number">1</span>aYBnsXSjZRWGl<span class="hljs-number">4</span>q<span class="hljs-number">38</span>+MFjU<span class="hljs-number">4</span>+yb+C<span class="hljs-number">4</span>Tfk<span class="hljs-number">37</span>Yd+x/<span class="hljs-number">4</span>b<span class="hljs-number">18</span>ey<span class="hljs-number">5</span>g<span class="hljs-number">9</span>EkFAiyXQtsZiDVjRIvcvJSiNLtce<span class="hljs-number">18</span>Asb<span class="hljs-number">874</span>z<span class="hljs-number">8</span>pVe<span class="hljs-number">0</span>Nui<span class="hljs-number">4</span>fFwtRtYJzJoaGnx<span class="hljs-number">6</span>BtIeNu<span class="hljs-number">0</span>A<span class="hljs-number">6</span>U<span class="hljs-number">9</span>NJRuqXIbK<span class="hljs-number">0</span>wAH<span class="hljs-number">4</span>BHwY/sS+rUjxgfr<span class="hljs-number">8</span>ag<span class="hljs-number">32</span>O<span class="hljs-number">4</span>cPIGHlChl<span class="hljs-number">2</span>INH/WDHUtYx<span class="hljs-number">9</span>O<span class="hljs-number">4</span>L<span class="hljs-number">32</span>q<span class="hljs-number">1</span>ijLxb<span class="hljs-number">5</span>Kz<span class="hljs-number">73</span>vfJj<span class="hljs-number">9</span>bgWVa<span class="hljs-number">8</span>fIyDT<span class="hljs-number">2</span>U<span class="hljs-number">05</span>/BLfTn<span class="hljs-number">3</span>fHbDpJR<span class="hljs-number">90</span>c+ImpvcPUaD<span class="hljs-number">1</span>Df/jJCuBrAR<span class="hljs-number">3</span>EPN<span class="hljs-number">8</span>r<span class="hljs-number">71</span>VgSlQhKsGIgm<span class="hljs-number">44</span>TDV<span class="hljs-number">1</span>lWyOXPotXoM<span class="hljs-number">1</span>BQHeQfOtbyYehufYmaPCEB/L<span class="hljs-number">4</span>wcDtiOo<span class="hljs-number">4</span>M<span class="hljs-number">4</span>qyCYZeOvlsEv<span class="hljs-number">63</span>DauNdQahl<span class="hljs-number">6</span>hPzuw+V<span class="hljs-number">7</span>CSRIwVP<span class="hljs-number">4</span>YnaPpoYiGZAw<span class="hljs-number">3</span>VrcQO/g<span class="hljs-number">0</span>XLaPT<span class="hljs-number">2</span>RfT<span class="hljs-number">2</span>zq<span class="hljs-number">5</span>Yp<span class="hljs-number">8</span>sJdgc<span class="hljs-number">82</span>imIZTynsnzf<span class="hljs-number">1</span>ZaJfW<span class="hljs-number">7</span>bToR<span class="hljs-number">466</span>RY<span class="hljs-number">9</span>SSqHpm<span class="hljs-number">92</span>MyqAxbVejv<span class="hljs-number">3</span>SAuYG<span class="hljs-number">64</span>Y/<span class="hljs-number">5</span>yIPh<span class="hljs-number">5</span>Dv<span class="hljs-number">6</span>ce<span class="hljs-number">93</span>kPRI<span class="hljs-number">443</span>z<span class="hljs-number">4</span>Pv/EIaTpwF<span class="hljs-number">1</span>+kzS<span class="hljs-number">3</span>iIF+ODc<span class="hljs-number">2</span>PtqJu<span class="hljs-number">61</span>fWnB<span class="hljs-number">4</span>ydXm<span class="hljs-number">7</span>iWoMH<span class="hljs-number">1</span>kkXpYHaTyqn<span class="hljs-number">5</span>pIatowkcQEBvkmNoWKdxlN/mfynAcy<span class="hljs-number">117</span>USXujKyXd<span class="hljs-number">58</span>sgRSG<span class="hljs-number">3</span>vPPXTPPLm<span class="hljs-number">0</span>M<span class="hljs-number">4</span>H<span class="hljs-number">6</span>Aqe<span class="hljs-number">9</span>MSR<span class="hljs-number">6</span>iuf<span class="hljs-number">0</span>YCNjiNI<span class="hljs-number">4</span>HWrnlbhNi<span class="hljs-number">22</span>Rnv<span class="hljs-number">6</span>zO<span class="hljs-number">8</span>cSg/B<span class="hljs-number">7</span>DS/V<span class="hljs-number">1</span>xL<span class="hljs-number">4</span>B<span class="hljs-number">3</span>eykH<span class="hljs-number">5</span>SU<span class="hljs-number">67</span>nfm<span class="hljs-number">3</span>aidjh/KrR<span class="hljs-number">5</span>VGIU<span class="hljs-number">0</span>/zSyJ/J<span class="hljs-number">9</span>DLi<span class="hljs-number">5</span>UpOAqKm<span class="hljs-number">3</span>hEAQTFz<span class="hljs-number">8</span>rfLV<span class="hljs-number">2</span>bTjWU<span class="hljs-number">9</span>XpcDghl<span class="hljs-number">0</span>UG<span class="hljs-number">9</span>rAqYMCFNCj<span class="hljs-number">2</span>w<span class="hljs-number">8</span>QZPzZx<span class="hljs-number">4</span>C<span class="hljs-number">42</span>cR+mCevG<span class="hljs-number">8</span>w+k<span class="hljs-number">7</span>EcXnm<span class="hljs-number">6</span>F<span class="hljs-number">93</span>yczsOWFClMolGDW+CjbJfQpHkxmmdVxap<span class="hljs-number">4</span>PziCZ<span class="hljs-number">6</span>p<span class="hljs-number">2</span>JU<span class="hljs-number">2</span>HDhnoazxrvlshir<span class="hljs-number">2</span>OWozfzA<span class="hljs-number">94</span>jQ<span class="hljs-number">5</span>MKhU<span class="hljs-number">6</span>O<span class="hljs-number">5</span>ozxELS<span class="hljs-number">4</span>n<span class="hljs-number">0</span>t<span class="hljs-number">9</span>EnjNoS+<span class="hljs-number">5</span>FI<span class="hljs-number">6</span>hgi<span class="hljs-number">8</span>sKNJN<span class="hljs-number">1</span>RXjgpcDeiVvp+sLWRT<span class="hljs-number">8</span>r/S<span class="hljs-number">5</span>tAACYgI<span class="hljs-number">23</span>tNR<span class="hljs-number">1</span>W<span class="hljs-number">7</span>e/oodJxKlTqBADybpTz<span class="hljs-number">3</span>NqonLp<span class="hljs-number">0</span>WEHZTPeUEcelcvqBsUlCCs<span class="hljs-number">46</span>sU/hWFCR<span class="hljs-number">0</span>IXLT<span class="hljs-number">6</span>rCZjAEeunqNO+<span class="hljs-number">4</span>Q<span class="hljs-number">78</span>D<span class="hljs-number">4</span>WQIzvcGqOvJWDiPqjIhwaj<span class="hljs-number">6</span>VI<span class="hljs-number">3</span>GoxXLN<span class="hljs-number">3</span>Dq<span class="hljs-number">0</span>fz<span class="hljs-number">395</span>fdl<span class="hljs-number">6</span>DB/gPEtuP<span class="hljs-number">8</span>P<span class="hljs-number">4</span>az<span class="hljs-number">7</span>py<span class="hljs-number">3</span>ZYEdEBunT<span class="hljs-number">4</span>QSEkByT<span class="hljs-number">8</span>kwaYSJbPOLoKzFMBiBNfMYofxyiAg<span class="hljs-number">6</span>rf<span class="hljs-number">0</span>+Mg<span class="hljs-number">9</span>BZVvXxAJ<span class="hljs-number">4</span>b<span class="hljs-number">9</span>HT<span class="hljs-number">5</span>+OvwiN/MuiC<span class="hljs-number">5</span>rSpOJkmvMPEbmhSztenMbZOtNH<span class="hljs-number">9</span>EXLzsCZq<span class="hljs-number">2</span>gTe<span class="hljs-number">21</span>H+yPXRZztD<span class="hljs-number">8</span>N<span class="hljs-number">80</span>yFDyNWQzq+FLh<span class="hljs-number">8</span>K<span class="hljs-number">7</span>h+JNg<span class="hljs-number">0</span>E<span class="hljs-number">32</span>bs<span class="hljs-number">6</span>lp<span class="hljs-number">7</span>DcYKmjshCa<span class="hljs-number">181</span>lfpk<span class="hljs-number">6</span>n<span class="hljs-number">9</span>EC<span class="hljs-number">9</span>JPusofkco<span class="hljs-number">1</span>FNCha<span class="hljs-number">7</span>YaKrFQLh<span class="hljs-number">3</span>zSXikKgKLhQYJx<span class="hljs-number">7</span>CKt<span class="hljs-number">12</span>wCXe/<span class="hljs-number">9</span>r<span class="hljs-number">4</span>ERkuz<span class="hljs-number">9</span>mRFFkaz<span class="hljs-number">7</span>EqReFETEKv<span class="hljs-number">5</span>iCO<span class="hljs-number">4</span>RAotUCVSsADKc<span class="hljs-number">9</span>ikVuy<span class="hljs-number">0</span>h<span class="hljs-number">2</span>Jyllt<span class="hljs-number">735</span>tLHFZTZh<span class="hljs-number">7</span>DCP+NDX<span class="hljs-number">7</span>/lbzlKkXrCjaBYkrtb<span class="hljs-number">2</span>Wb<span class="hljs-number">6</span>KvMaAWTUJpLqs<span class="hljs-number">1</span>DbOKiKLBFsIzJK<span class="hljs-number">32</span>/+<span class="hljs-number">8</span>OrRckqoJL/t<span class="hljs-number">7</span>c<span class="hljs-number">95</span>U<span class="hljs-number">93</span>ct<span class="hljs-number">2</span>VA/lQiRU<span class="hljs-number">9</span>dvIB/luU<span class="hljs-number">7</span>C<span class="hljs-number">4</span>r/Kuywh<span class="hljs-number">5</span>nI<span class="hljs-number">85</span>gqPovvvEzTm<span class="hljs-number">2</span>AUChmlzKRrsLznh<span class="hljs-number">388</span>c<span class="hljs-number">59</span>DjBGX<span class="hljs-number">8</span>F<span class="hljs-number">7</span>jDEBWJbAPuoCstIRM<span class="hljs-number">3</span>z<span class="hljs-number">4</span>jOwNFnHCNNktaSNEM<span class="hljs-number">9</span>HHmV<span class="hljs-number">47</span>k<span class="hljs-number">8</span>p<span class="hljs-number">6</span>tBsU<span class="hljs-number">4</span>rcZG<span class="hljs-number">34</span>UsLfyJQQH<span class="hljs-number">7</span>Iz<span class="hljs-number">7</span>GEkeVRuRfVDcIt<span class="hljs-number">6</span>hu+AItSfVoVnsVPF<span class="hljs-number">2</span>+<span class="hljs-number">2</span>QpmtFzf<span class="hljs-number">3</span>qZzNxA<span class="hljs-number">5</span>sngtxuqOeUoNZ<span class="hljs-number">5</span>WihUocrmbrnQZ<span class="hljs-number">6</span>F<span class="hljs-number">9</span>eUEaWeCn/qU<span class="hljs-number">17</span>tal<span class="hljs-number">3</span>vvj<span class="hljs-number">2</span>LLOMcwwUAZJM<span class="hljs-number">1</span>Exbg<span class="hljs-number">94</span>WHdGZEeWGKtCTq<span class="hljs-number">8</span>PjxnAw<span class="hljs-number">8</span>fNMEO<span class="hljs-number">3</span>b<span class="hljs-number">3</span>IbrU<span class="hljs-number">8</span>WCcfj<span class="hljs-number">4</span>BJm/<span class="hljs-number">0</span>dy<span class="hljs-number">6</span>Uz<span class="hljs-number">7</span>V<span class="hljs-number">495</span>Lr<span class="hljs-number">2</span>B<span class="hljs-number">4</span>R<span class="hljs-number">6</span>tHIHAwJkBgAlci<span class="hljs-number">0</span>FCdLg<span class="hljs-number">5</span>/WizXw<span class="hljs-number">1</span>HLZmeoaaVzucwp<span class="hljs-number">6</span>SoJbSIqfEw<span class="hljs-number">7</span>I<span class="hljs-number">2</span>h<span class="hljs-number">8</span>LNGiWJ/z<span class="hljs-number">1</span>wgC<span class="hljs-number">8</span>wIiVK<span class="hljs-number">3</span>AWqwTi<span class="hljs-number">4</span>d<span class="hljs-number">6</span>DM<span class="hljs-number">7</span>qcCzeKYNhkJOag<span class="hljs-number">7</span>qFlM<span class="hljs-number">8</span>i<span class="hljs-number">0</span>tZTMnyge<span class="hljs-number">5</span>EjkG<span class="hljs-number">7</span>b<span class="hljs-number">0</span>rs<span class="hljs-number">9</span>EuWZKiBkaQJIBvNu<span class="hljs-number">5</span>L/IAMfXBvQJROO+kFZro<span class="hljs-number">6</span>+<span class="hljs-number">5</span>bg<span class="hljs-number">5</span>iOu<span class="hljs-number">9</span>FscX+NwYaEFesOjOuMeDoBvOfGEOnOfY<span class="hljs-number">0</span>yPOOal<span class="hljs-number">5</span>DjyRiiL+dLsJdkgwOhXdMhh<span class="hljs-number">53</span>d<span class="hljs-number">7</span>tzzKqcYvCVfPPhkVQCnf<span class="hljs-number">2</span>RfL<span class="hljs-number">7</span>QKNCR<span class="hljs-number">99</span>/lsEMWZxizZBEWvXDuFBbyOfLhdY<span class="hljs-number">4</span>fZr<span class="hljs-number">1</span>QXzpThVLcmnvNxWU<span class="hljs-number">2</span>IviU<span class="hljs-number">9</span>uyKI<span class="hljs-number">7</span>GRxX<span class="hljs-number">69</span>m<span class="hljs-number">0</span>jw<span class="hljs-number">0</span>Yrbotx<span class="hljs-number">2</span>CwOmqT<span class="hljs-number">89</span>gwINMf<span class="hljs-number">7</span>VHDxMoYtSAxBnw=='
</code></pre><p>Then, we'll also create a file at <code>/src/lib/decrypt.js</code> that looks like the following</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/lib/decrypt.js</span>

<span class="hljs-keyword">import</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">'crypto'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> decrypt = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> algorithm = <span class="hljs-string">'aes-128-cbc'</span>;
    <span class="hljs-keyword">const</span> decipher = crypto.createDecipheriv(
        algorithm,
        <span class="hljs-keyword">import</span>.meta.env.VITE_SERVICE_ENCRYPTION_KEY,
        <span class="hljs-keyword">import</span>.meta.env.VITE_SERVICE_ENCRYPTION_IV
    );
    <span class="hljs-keyword">let</span> decrypted = decipher.update(data, <span class="hljs-string">'base64'</span>, <span class="hljs-string">'utf8'</span>);
    decrypted += decipher.final(<span class="hljs-string">'utf8'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(decrypted);
};
</code></pre>
<p>where the environment variables noted before have been saved with the <code>VITE_</code> prefix, as that is necessary to import the environment variables here.
Finally, we'll need to create a <code>/src/lib/service.js</code> file where we import the encrypted data and the decrypt method and we return an instantiated Google Drive instance.</p>
<pre><code>// /src/lib/service.js

<span class="hljs-keyword">import</span> { google } <span class="hljs-keyword">from</span> <span class="hljs-string">'googleapis'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">encrypted</span> } <span class="hljs-keyword">from</span> "$lib/service-account.enc";
<span class="hljs-keyword">import</span> { decrypt } <span class="hljs-keyword">from</span> "$lib/decrypt";

const getDriveService = () =&gt; {
    const SCOPES = [<span class="hljs-string">'https://www.googleapis.com/auth/drive.file'</span>];
    const credentials = decrypt(<span class="hljs-keyword">encrypted</span>)

    const auth = <span class="hljs-built_in">new</span> google.auth.GoogleAuth({
        credentials,
        scopes: SCOPES
    });
    const driveService = google.drive({ <span class="hljs-keyword">version</span>: <span class="hljs-string">'v3'</span>, auth });
    <span class="hljs-keyword">return</span> driveService;
};

export <span class="hljs-keyword">default</span> getDriveService;
</code></pre><p><em>note:</em> This assumes that you have already installed the <code>googleapis</code> package using <code>npm i -D googleapis</code>.</p>
<h2 id="heading-summary">Summary</h2>
<p>Now that we have all of this set up, we will be able to import the <code>service</code> module into any file where we want to use the Google Drive API and instantiate it in a manner like so</p>
<pre><code><span class="hljs-keyword">import</span> service <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/service'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">{ request }</span>) </span>{
  <span class="hljs-keyword">const</span> drive = service();
  ...
</code></pre><p>In the next blog post in this series I'll set up a server endpoint to grab a file and upload it to a specific sub-folder in Google Drive itself.</p>
]]></content:encoded></item><item><title><![CDATA[Building a blog with Nuxt Content v2 and TailwindCSS]]></title><description><![CDATA[Notes
This post is also available on my main page at https://www.jvp.design/blog/building-a-blog-with-nuxt-content-tailwind
For those who want to follow along in code, you can get it from https://github.com/jvp-design/nuxt-blog-example
A video accomp...]]></description><link>https://blog.jeffpohlmeyer.com/building-a-blog-with-nuxt-content-v2-and-tailwindcss</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/building-a-blog-with-nuxt-content-v2-and-tailwindcss</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Mon, 13 Jun 2022 14:30:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928194741/uzI_0jQrt.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-notes">Notes</h1>
<p>This post is also available on my main page at https://www.jvp.design/blog/building-a-blog-with-nuxt-content-tailwind</p>
<p>For those who want to follow along in code, you can get it from https://github.com/jvp-design/nuxt-blog-example</p>
<p>A video accompanying this post will be released on Thursday, 16-Jun. The link will be posted here when it's live.</p>
<h1 id="heading-intro">Intro</h1>
<p>In the <a target="_blank" href="https://www.jvp.design/blog/building-a-blog-with-sveltekit-tailwind-mdsvex">last blog post</a> I described how to set up a blog using SvelteKit with MDsveX and Tailwind.
It worked well enough that it warranted being featured in <a target="_blank" href="https://www.hashnode.com">Hashnode's</a> feed as well a share on <a target="_blank" href="https://www.linkedin.com/posts/hashnode_building-a-blog-with-sveltekit-tailwindcss-activity-6939403492956467200-QrcS?utm_source=linkedin_share&amp;utm_medium=member_desktop_web">LinkedIn</a>
but, if I'm being honest, it felt a little hacky.
Sure, it worked, but there was too much "custom" functionality I needed to add to get it working.
Also, I tweeted a couple of weeks ago about how excited I was to try playing with Nuxt Content v2, but it wasn't ready at the time that I was ready to write the last blog post and record the last video.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/jvpdesignllc/status/1529243836232392711">https://twitter.com/jvpdesignllc/status/1529243836232392711</a></div>
<p>Well well well, my handful of friends who may actually look at this, I've played around with Nuxt Content v2 to write this post.
Also, I want to give a quick shoutout to <a target="_blank" href="https://brixx.behonbaker.com/">Behon Baker</a> for the <a target="_blank" href="https://www.youtube.com/watch?v=hDJGGzyaYx8">video</a> he released a couple of weeks ago about this.
This may feel similar to what he did, but I'm trying to create as much content as I can of my own so here we go.</p>
<h1 id="heading-set-up-the-project">Set up the project</h1>
<p>Per the <a target="_blank" href="https://content.nuxtjs.org/get-started">documentation</a>, we can set up a new Content v2 project by running</p>
<pre><code class="lang-bash">npx nuxi init nuxt-blog-example -t content
<span class="hljs-built_in">cd</span> nuxt-blog-example
npm i
</code></pre>
<p>Running this sets up a very minimal scaffold for a project
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928042089/2Z1_ePJP2.png" alt="2-file-structure.png" />
and when I run <code>npm run dev</code> and navigate to http://localhost:3000 I'm met with the following window
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928051649/-K7gvCwKZ.png" alt="3-first-page.png" />
For what it's worth, when you go to http://localhost:3000 you <em>should</em> be shown what is on the index.md page but for some reason that didn't seem to work for me.
No matter, let's continue.</p>
<h2 id="heading-install-tailwind">Install Tailwind</h2>
<p>This is also a fairly simple process, laid out succinctly in the documentation for the <a target="_blank" href="https://tailwindcss.nuxtjs.org/">Nuxt/Tailwind</a> module.
To get started, for those who don't want to navigate elsewhere, you simply install the module and initialize Tailwind</p>
<pre><code class="lang-bash">npm i -D @nuxtjs/tailwindcss @tailwindcss/typography
npx tailwindcss init
</code></pre>
<p>then you just need to add the module to the <code>modules</code> section of your <code>nuxt.config.ts</code> file.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// nuxt.config.ts</span>
<span class="hljs-keyword">import</span> { defineNuxtConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'nuxt'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  modules: [<span class="hljs-string">'@nuxt/content'</span>, <span class="hljs-string">'@nuxtjs/tailwindcss'</span>]
})
</code></pre>
<p>Then, we create a new file, called <code>tailwind.css</code> under a newly created <code>assets/css</code> folder at the root of our project.
It is in this file where you'll add your tailwind directives</p>
<pre><code class="lang-css"><span class="hljs-comment">/* assets/css/tailwind.css */</span>

<span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>If you want to name the file something else that's fine you'll just need to configure the module to point to the location where you've stored this file.
You can read all about that in the documentation for the module.</p>
<p>In order to save time, I'm going to just re-use the <code>tailwind.config.js</code> file from the previous post, copy the content from the base <code>app.css</code> file from that post, and copy over the fonts.
Instead of saving the fonts in <code>/static/fonts/...</code>, though, they'll instead be saved in <code>/assets/fonts/...</code> and we'll need to update the base css file to account for this.</p>
<h3 id="heading-tailwindconfigjs-file"><code>tailwind.config.js</code> file</h3>
<p>For those who don't want to go elsewhere, this is the content that will go in the <code>tailwind.config.js</code> file</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> defaultTheme = <span class="hljs-built_in">require</span>(<span class="hljs-string">'tailwindcss/defaultTheme'</span>)

<span class="hljs-comment">/** @type {import('tailwindcss').Config} */</span>
<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = {
    content: [<span class="hljs-string">'./src/**/*.{html,js,vue,ts}'</span>],
    theme: {
        extend: {
            fontFamily: {
                sans: [<span class="hljs-string">'uncut-sans'</span>, ...defaultTheme.fontFamily.sans],
                serif: [<span class="hljs-string">'sprat'</span>, ...defaultTheme.fontFamily.serif]
            }
        }
    },
    plugins: [<span class="hljs-built_in">require</span>(<span class="hljs-string">'@tailwindcss/typography'</span>)]
};
</code></pre>
<p>Note that I have changed the extensions in the <code>content</code> attribute of <code>module.exports</code> to include a <code>.vue</code> extension instead of a <code>.svelte</code> one.</p>
<h3 id="heading-assetscsstailwindcss-file"><code>assets/css/tailwind.css</code> file</h3>
<p>As previously mentioned, we're going to copy over the same content from the last blog post's <code>/src/styles/app.css</code> into <code>/assets/css/tailwind.css</code> file, while also changing the location of where the font files are stored</p>
<pre><code class="lang-css"><span class="hljs-comment">/* /assets/css/tailwind.css */</span>
<span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;

<span class="hljs-keyword">@layer</span> base {
    <span class="hljs-selector-tag">h1</span>,
    <span class="hljs-selector-tag">h2</span>,
    <span class="hljs-selector-tag">h3</span>,
    <span class="hljs-selector-tag">h4</span>,
    <span class="hljs-selector-tag">h5</span>,
    <span class="hljs-selector-tag">h6</span> {
        @apply font-serif;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/sprat/Sprat-Regular.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/sprat/Sprat-RegularMedium.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/sprat/Sprat-RegularBold.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }

    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
        <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/uncut-sans/Uncut-Sans-Regular.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
        <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/uncut-sans/Uncut-Sans-Medium.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
        <span class="hljs-built_in">url</span>(<span class="hljs-string">'/assets/fonts/uncut-sans/Uncut-Sans-Semibold.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
}
</code></pre>
<h1 id="heading-site-content">Site content</h1>
<p>Now that the app is up and running it's time to start creating content.
The first thing we'll do is open <code>/app.vue</code> and set the height of the app to be the height of the screen, similarly to what we did in the previous post.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="h-screen"&gt;
    &lt;NuxtPage /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>We can add styling to this in a similar manner to how I set up the <code>__layout.svelte</code> file in the last post.
Nuxt <em>does</em> support multiple layouts, but they recommend that if you're only going to use one layout then just do all of the necessary styling in the <code>app.vue</code> file.</p>
<h2 id="heading-landing-page">Landing page</h2>
<p>Next we need to set up the main landing page in a similar manner to the previous post.
Within the <code>/pages</code> directory (where currently the only file that exists is <code>[...slug].vue</code>) I'll now create an <code>index.vue</code> file and add the following content.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="grid h-screen place-content-center gap-3"&gt;
    &lt;h1 class="text-5xl text-blue-500"&gt;Welcome to Nuxt Content&lt;/h1&gt;
    &lt;p class="text-xl text-red-600"&gt;
      Visit &lt;a href="https://content.nuxtjs.org/"&gt;content.nuxtjs.org&lt;/a&gt;
      to read the documentation
    &lt;/p&gt;
    &lt;nuxt-link
      href="/blog"
      class="mx-auto rounded-xl bg-amber-700 px-20 py-4 text-white"
    &gt;
      Go to Blog
    &lt;/nuxt-link&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928073888/nOvzMvkwW.png" alt="4-fonts-loaded.png" /></p>
<h2 id="heading-blog-content">Blog content</h2>
<p>Now that we have a landing page up and running we have to create something that will be loaded when we click on the button on the landing page.
There are plenty of ways that we can host blog content, but the idea behind this site is this would be a "normal" site with a blog section so we're going to create a <code>blog</code> folder within the already existing <code>content</code> folder.
This is where we'll store all of our articles.
We get nesting by simply doing this; if you move the <code>about.md</code> file into this new folder and go to http://localhost/3000/blog/about then you should see the same thing you saw before (you may need to restart the server).
We <em>also</em> want a page, though, that will display all of our blog posts, which was the <code>/src/routes/blog/index.svelte</code> component in the last post.
To do this, we simply create a <code>/pages/blog/index.vue</code> file and, for now, add some dummy content</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;h1 class="grid h-full place-content-center text-5xl"&gt;Blog Home&lt;/h1&gt;
&lt;/template&gt;
</code></pre>
<p>The next thing we need to do is somehow fetch the articles in the <code>/content/blog</code> folder to display here.</p>
<h3 id="heading-nuxt-content-built-in-helpers">Nuxt Content built-in helpers</h3>
<p>Nuxt Content ships with some very helpful built-in components, about which you can read here https://content.nuxtjs.org/api/components/content-doc.
Before describing the two methods that we can use to fetch content, there is a built-in helper called <code>&lt;ContentNavigation&gt;</code> that will return a list of objects with "name" and "path" attributes strictly for navigation.
This does not provide access to things like the author, date, or description, so I'll leave exploration of that one to the reader.</p>
<h4 id="heading-contentlist">ContentList</h4>
<p>The very first helper that we can use for a "list" view is <code>&lt;ContentList&gt;</code>.
You can view the documentation for this helper at https://content.nuxtjs.org/api/components/content-list but I'll update the <code>/pages/blog/index.vue</code> component below.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;main class="bg-white px-4 pt-16 pb-20 sm:px-6 lg:px-8 lg:pt-24 lg:pb-28"&gt;
    &lt;div class="mx-auto max-w-lg lg:max-w-7xl"&gt;
      &lt;div class="border-b border-b-gray-200 pb-6"&gt;
        &lt;h2
            class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl"
        &gt;
          Recent Posts
        &lt;/h2&gt;
      &lt;/div&gt;
      &lt;div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12"&gt;
        &lt;ContentList path="/blog" v-slot="{ list }"&gt;
          &lt;div
              v-for="article in list"
              :key="article._path"
              class="flex flex-col justify-between rounded-lg border border-gray-200 p-4"
          &gt;
            &lt;nuxt-link :href="article._path"&gt;
              &lt;p class="text-xl text-gray-900"&gt;{{ article.title }}&lt;/p&gt;
              &lt;p class="mt-3 text-gray-500"&gt;{{ article.description }}&lt;/p&gt;
            &lt;/nuxt-link&gt;
            &lt;div class="mt-6"&gt;
              &lt;a
                  :href="`?author=${article.author}`"
                  class="text-sm font-medium text-gray-900"
              &gt;
                {{ article.author }}
              &lt;/a&gt;
              &lt;div class="text-sm text-gray-500"&gt;
                &lt;time datetime="2020-03-16"&gt;{{ article.date }}&lt;/time&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/ContentList&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p>You'll notice in the above component some very similar functionality.
First, the styling is identical to what existed in the <a target="_blank" href="https://github.com/jvp-design/sveltekit-tailwind-mdsvex-blog/blob/main/src/routes/blog/index.svelte">index.svelte</a> file.
Then, the main differences are in the way content is rendered in Vue vs in Svelte.</p>
<ul>
<li>Where we previously wrapped iterated content in an <code>{#each}{/each}</code> block in Svelte we use <code>v-for</code> in Vue</li>
<li>The colon <code>:</code> in Vue is a shorthand for the <code>v-bind</code> directive. In Vue, <code>:href="article._path"</code> would be equivalent to <code>href={article._path}</code> in Svelte</li>
<li>In Vue we use a <code>{{ }}</code> syntax to render a dynamic variable instead of the <code>{}</code> in Svelte</li>
</ul>
<p>The other main difference is including the built-in <code>&lt;ContentList&gt;</code> component, which will fetch all content at the <code>/content</code> directory unless we tell it otherwise, which I did by including the <code>path="/blog"</code> attribute, telling it to fetch content from <code>/content/blog</code> for this rendered content list.
In looking at the resulting page at http://localhost:3000/blog we see
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928171009/MWXuL8NTi.png" alt="5-main-content-page.png" />
There are a couple of things we need to "fix" to display things as we intended, but we automatically get the data without having to tell content where/how to parse the <code>.md</code> files.
All we've had to do is just point to the location and Nuxt Content knows where to find everything.</p>
<h5 id="heading-small-update-to-posts">Small update to posts</h5>
<p>In the SvelteKit example I had to manually choose what I wanted my excerpt to be.
In Nuxt Content all you need to do is add a little code snippet, <code>&lt;!--more--&gt;</code> after the code you want to be used as the description.
Everything before this tag will be included in the description (as well as in the main article) and that's all you need to do.
With that in mind, this is what the posts will now look like
<strong>/content/blog/foo-bar.md</strong></p>
<pre><code class="lang-markdown">---
title: Foo Bar
author: Jeff
<span class="hljs-section">date: 2022-04-15
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in.
<span class="xml"><span class="hljs-comment">&lt;!--more--&gt;</span></span>
Quisque tincidunt egestas libero rhoncus blandit. Etiam elit leo, facilisis id magna sit amet, vestibulum finibus nulla. Vestibulum porttitor nisl id ligula accumsan, et dapibus justo cursus. Phasellus congue mauris vitae dictum auctor. Sed vitae mollis quam. Morbi venenatis metus ligula, sit amet consectetur eros pharetra vel.

Maecenas efficitur mauris eu ex viverra, ut consequat metus ultrices. Sed imperdiet leo odio, in aliquam orci sagittis ut. Vivamus eget sem et nibh faucibus luctus vel a enim. Sed orci tortor, semper ut vulputate at, hendrerit dapibus dolor. Pellentesque tincidunt tempor efficitur. Etiam efficitur pellentesque nisi, sit amet feugiat nisi. Maecenas nisl odio, viverra vitae rhoncus eu, placerat vitae ante. Quisque suscipit nibh lacus, sit amet facilisis tellus fermentum in. Integer nec lacinia risus, ut lobortis ex.

Integer nec ultricies nisi. Curabitur odio mauris, scelerisque at luctus a, bibendum eget velit. Vivamus id tellus lectus. Nulla in purus sit amet mi tincidunt commodo. Ut auctor ante a mauris dignissim laoreet. Nullam magna arcu, tincidunt nec risus et, mattis fringilla augue. Suspendisse imperdiet, purus vel pharetra bibendum, enim purus convallis quam, ut faucibus nibh libero in enim. Curabitur feugiat leo ac accumsan tempor. Ut non convallis mauris, sed rutrum libero.

Maecenas vehicula maximus justo, pellentesque consequat sem dignissim a. Proin quis lectus molestie, pellentesque massa in, egestas orci. Vestibulum facilisis enim at magna scelerisque, quis suscipit quam ultrices. Proin a rutrum tortor. Proin vel scelerisque nunc. Nullam condimentum sit amet magna eu rutrum. Quisque magna enim, aliquet ut blandit et, viverra eu leo. Sed molestie sem et quam consequat mattis. Donec elit velit, cursus at ipsum nec, ullamcorper tincidunt neque.

Nunc convallis odio justo, non interdum dolor ultricies interdum. Curabitur accumsan sem a iaculis placerat. Donec eu facilisis sem, vel bibendum risus. Aliquam non tincidunt est, a auctor magna. Ut erat libero, commodo non malesuada quis, porttitor sit amet libero. Curabitur pulvinar ornare leo id efficitur. Donec sollicitudin arcu venenatis odio elementum, at venenatis erat efficitur. In porta mi et sollicitudin faucibus. Vivamus vel metus interdum, facilisis nisl at, ullamcorper mauris. Sed ac nisl at dolor varius aliquam. In facilisis pretium interdum. Sed tempus purus at pulvinar scelerisque. Etiam eu purus eleifend, commodo turpis eget, aliquet turpis. Mauris fermentum magna dictum lorem bibendum tempor.
</code></pre>
<p><strong>/content/blog/foo-baz.md</strong></p>
<pre><code class="lang-markdown">---
title: Foo Baz
author: Alice
<span class="hljs-section">date: 2022-07-15
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in.
<span class="xml"><span class="hljs-comment">&lt;!--more--&gt;</span></span>
Quisque tincidunt egestas libero rhoncus blandit. Etiam elit leo, facilisis id magna sit amet, vestibulum finibus nulla. Vestibulum porttitor nisl id ligula accumsan, et dapibus justo cursus. Phasellus congue mauris vitae dictum auctor. Sed vitae mollis quam. Morbi venenatis metus ligula, sit amet consectetur eros pharetra vel.

Maecenas efficitur mauris eu ex viverra, ut consequat metus ultrices. Sed imperdiet leo odio, in aliquam orci sagittis ut. Vivamus eget sem et nibh faucibus luctus vel a enim. Sed orci tortor, semper ut vulputate at, hendrerit dapibus dolor. Pellentesque tincidunt tempor efficitur. Etiam efficitur pellentesque nisi, sit amet feugiat nisi. Maecenas nisl odio, viverra vitae rhoncus eu, placerat vitae ante. Quisque suscipit nibh lacus, sit amet facilisis tellus fermentum in. Integer nec lacinia risus, ut lobortis ex.

Integer nec ultricies nisi. Curabitur odio mauris, scelerisque at luctus a, bibendum eget velit. Vivamus id tellus lectus. Nulla in purus sit amet mi tincidunt commodo. Ut auctor ante a mauris dignissim laoreet. Nullam magna arcu, tincidunt nec risus et, mattis fringilla augue. Suspendisse imperdiet, purus vel pharetra bibendum, enim purus convallis quam, ut faucibus nibh libero in enim. Curabitur feugiat leo ac accumsan tempor. Ut non convallis mauris, sed rutrum libero.

Maecenas vehicula maximus justo, pellentesque consequat sem dignissim a. Proin quis lectus molestie, pellentesque massa in, egestas orci. Vestibulum facilisis enim at magna scelerisque, quis suscipit quam ultrices. Proin a rutrum tortor. Proin vel scelerisque nunc. Nullam condimentum sit amet magna eu rutrum. Quisque magna enim, aliquet ut blandit et, viverra eu leo. Sed molestie sem et quam consequat mattis. Donec elit velit, cursus at ipsum nec, ullamcorper tincidunt neque.

Nunc convallis odio justo, non interdum dolor ultricies interdum. Curabitur accumsan sem a iaculis placerat. Donec eu facilisis sem, vel bibendum risus. Aliquam non tincidunt est, a auctor magna. Ut erat libero, commodo non malesuada quis, porttitor sit amet libero. Curabitur pulvinar ornare leo id efficitur. Donec sollicitudin arcu venenatis odio elementum, at venenatis erat efficitur. In porta mi et sollicitudin faucibus. Vivamus vel metus interdum, facilisis nisl at, ullamcorper mauris. Sed ac nisl at dolor varius aliquam. In facilisis pretium interdum. Sed tempus purus at pulvinar scelerisque. Etiam eu purus eleifend, commodo turpis eget, aliquet turpis. Mauris fermentum magna dictum lorem bibendum tempor.
</code></pre>
<p><strong>/content/blog/hello-world.md</strong></p>
<pre><code class="lang-markdown">---
title: Hello World
author: Jeff
<span class="hljs-section">date: 2022-05-27
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eget massa sit amet arcu varius lacinia nec quis lacus.
<span class="xml"><span class="hljs-comment">&lt;!--more--&gt;</span></span> 
Proin auctor lectus a volutpat porta. Nullam eget ipsum convallis, elementum orci sodales, blandit velit. In imperdiet, ligula sed ultricies pharetra, metus mi consequat dui, vitae luctus dolor ligula eu nunc. Fusce consequat mauris ac egestas iaculis. Quisque pharetra et ante maximus convallis. Nulla sollicitudin velit molestie mauris dignissim, at hendrerit diam fringilla. Donec mollis eget ex non iaculis. In a vehicula nisl. Donec dapibus orci in enim posuere, non rhoncus risus ultrices. Pellentesque elementum metus ipsum, ut scelerisque mauris ultrices vel.

Aliquam ullamcorper est vehicula, suscipit nulla pellentesque, convallis odio. Praesent eget elit eget magna fringilla pharetra tempor quis magna. Proin et est vestibulum neque rhoncus mattis non vel lacus. Proin vulputate risus vel dignissim vestibulum. Quisque id sollicitudin neque, sed sagittis urna. Vestibulum vehicula metus sed eros venenatis, sit amet facilisis nunc porta. Nam pharetra luctus sapien, ut venenatis nibh tincidunt mollis. Phasellus efficitur, felis vitae mattis cursus, sapien diam vulputate dui, sit amet pulvinar ante ipsum non urna.

Fusce est nulla, efficitur vitae turpis eget, pretium rutrum turpis. Fusce at lectus eros. Phasellus convallis condimentum dolor ac rutrum. Integer commodo augue et dui efficitur tincidunt. Nam scelerisque egestas quam, vitae ultrices turpis tincidunt rhoncus. Duis rutrum placerat erat. Ut ac tincidunt elit. In laoreet dictum mauris nec posuere. Curabitur tempus, dolor malesuada ultrices feugiat, ipsum eros faucibus tellus, eu ultricies nunc est sed dolor. Suspendisse nisi eros, vehicula vitae iaculis sit amet, aliquet sit amet leo. Sed euismod urna at eros posuere laoreet. Curabitur in sodales lorem. Nulla rutrum aliquam felis ac tempor.

Ut pretium vitae elit ac facilisis. Aliquam nisi tortor, feugiat at lacus sed, condimentum egestas urna. Vestibulum hendrerit augue non urna volutpat, et fermentum tortor pellentesque. Aenean eget pharetra leo. Vestibulum ut laoreet dui. Phasellus nec nunc imperdiet, mollis urna eget, interdum lacus. Nulla ac neque pulvinar ex vestibulum venenatis at sed mi. Aliquam faucibus risus eget dolor porttitor interdum. Phasellus rutrum augue ex, vel tempus velit sollicitudin vitae. Pellentesque libero sapien, ullamcorper nec elementum nec, pharetra sed nisl. Nullam egestas arcu et ex vulputate, pretium vestibulum odio convallis. Nam auctor risus nec fermentum ultricies.

Donec porttitor quis ipsum ut imperdiet. Fusce ac pretium felis, sit amet pharetra orci. Donec vitae quam ac tellus pellentesque fringilla. Curabitur placerat quam a leo imperdiet tincidunt. Nunc porta pulvinar orci sit amet varius. Suspendisse dapibus ipsum nec magna ultricies gravida. Maecenas varius justo ac sem rhoncus lobortis. Integer eget cursus diam. Vestibulum sollicitudin enim at metus scelerisque blandit. In sit amet pulvinar nunc. Sed sit amet rutrum ex, efficitur imperdiet nunc.
</code></pre>
<h4 id="heading-contentrenderer">ContentRenderer</h4>
<p>Now that we have the list of posts, we need to have something that will display the content when we click on the link.
This is equivalent to the <code>/src/routes/blog/_layout.svelte</code> component in the last project.
The place we'll start working with styling, etc, for this is in the <code>pages/[...slug].vue</code> component, which currently looks just like</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;main&gt;
    &lt;ContentDoc /&gt;
  &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p>Just as an example, if we go to http://localhost:3000/blog/foo-bar we'll see
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928100699/vYyei8usx.png" alt="6-foo-bar-detail-page.png" />
Copying the styling over from https://github.com/jvp-design/sveltekit-tailwind-mdsvex-blog/blob/main/src/routes/blog/_layout.svelte we get</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;main
    class="relative mx-auto max-w-prose overflow-hidden bg-white py-16 px-4 sm:px-6 lg:px-8"
  &gt;
    &lt;nuxt-link
      class="block cursor-pointer"
      href="/blog"
    &gt;
      &lt;svg
        xmlns="http://www.w3.org/2000/svg"
        class="inline h-6 w-6"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        stroke-width="2"
      &gt;
        &lt;path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M11 17l-5-5m0 0l5-5m-5 5h12"
        /&gt;
      &lt;/svg&gt;
      Back
    &lt;/nuxt-link&gt;
    &lt;ContentDoc v-slot="{ doc }"&gt;
      &lt;h2 class="my-4 text-4xl font-semibold"&gt;{{ doc.title }}&lt;/h2&gt;
      &lt;p class="my-4 text-gray-500"&gt;
        by, {{ doc.author }}, {{ convertDate(doc.date) }}
      &lt;/p&gt;
      &lt;div
        class="prose prose-lg first-letter:text-3xl first-letter:text-blue-600"
      &gt;
        &lt;ContentRenderer :value="doc" /&gt;
      &lt;/div&gt;
    &lt;/ContentDoc&gt;
  &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p>Again, we'll notice a few subtle differences</p>
<ul>
<li>The <code>on:click</code> directive in Svelte is replaced by <code>@click</code> in Vue</li>
<li>Rendering the variables uses double-curly braces instead of single, like we've seen before</li>
<li>We can get access to all of the individual document's attributes by wrapping the content in the built-in <code>&lt;ContentDoc&gt;</code> component, the documentation for which you can see <a target="_blank" href="https://content.nuxtjs.org/api/components/content-doc">here</a>.</li>
</ul>
<p>One thing I'll note, I still haven't been able to get the multiple slot functionality (i.e. the <code>#not-found</code> and <code>#empty</code> stuff) working.
Any time I try including it I get the following error:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928116920/Em6de6kpI.png" alt="7-slot-error-page.png" />
In order to get this all to work, though, we'll need to add in one more bit of functionality, namely the <code>convertDate</code> method that is referenced in the template.
Since this is not the only place we'll be using it, I'll create a <code>utils</code> folder at the root of the project and just create an <code>index.ts</code> file here to include this functionality.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// /utils/index.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> convertDate = (published: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> months = {
        <span class="hljs-number">1</span>: <span class="hljs-string">"Jan"</span>,
        <span class="hljs-number">2</span>: <span class="hljs-string">"Feb"</span>,
        <span class="hljs-number">3</span>: <span class="hljs-string">"Mar"</span>,
        <span class="hljs-number">4</span>: <span class="hljs-string">"Apr"</span>,
        <span class="hljs-number">5</span>: <span class="hljs-string">"May"</span>,
        <span class="hljs-number">6</span>: <span class="hljs-string">"Jun"</span>,
        <span class="hljs-number">7</span>: <span class="hljs-string">"Jul"</span>,
        <span class="hljs-number">8</span>: <span class="hljs-string">"Aug"</span>,
        <span class="hljs-number">9</span>: <span class="hljs-string">"Sep"</span>,
        <span class="hljs-number">10</span>: <span class="hljs-string">"Oct"</span>,
        <span class="hljs-number">11</span>: <span class="hljs-string">"Nov"</span>,
        <span class="hljs-number">12</span>: <span class="hljs-string">"Dec"</span>,
    };
    <span class="hljs-keyword">const</span> date = published.substring(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
    <span class="hljs-keyword">const</span> [year, month, day] = date.split(<span class="hljs-string">"-"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${day}</span>-<span class="hljs-subst">${months[<span class="hljs-built_in">parseInt</span>(month)]}</span>-<span class="hljs-subst">${year}</span>`</span>;
};
</code></pre>
<p>and we'll just import this method both into <code>/pages/[...slug].vue</code> as well as <code>/pages/blog/index.vue</code> as we'll use it there to convert the date format to a more readable one for the list view, as well.
Since, as of this point, both of those components are stateless (they don't have a <code>script</code> tag yet) we can just add the following snippets to the top of each of the components as displayed below
<strong>/pages/[...slug].vue</strong></p>
<pre><code class="lang-vue">&lt;script setup&gt;
  import { convertDate } from '../utils'
&lt;/script&gt;
</code></pre>
<p><strong>/pages/blog/index.vue</strong></p>
<pre><code class="lang-vue">&lt;script setup&gt;
  import { convertDate } from '../../utils'
&lt;/script&gt;
</code></pre>
<p>After adding this to <code>index.vue</code> we'll also want to replace the <code>&lt;time datetime="2020-03-16"&gt;{{ article.date }}&lt;/time&gt;</code> with <code>&lt;time datetime="2020-03-16"&gt;{{ convertDate(article.date) }}&lt;/time&gt;</code></p>
<h2 id="heading-extras">Extras</h2>
<p>At this point we're <em>essentially</em> done with the original "stuff" from the last post.
The only things we need to add to match functionality are</p>
<ul>
<li>Sorting descending by date</li>
<li>The ability to search by author</li>
<li>A custom NewWindowUrl component</li>
</ul>
<h3 id="heading-sorting">Sorting</h3>
<p>We can see in (https://content.nuxtjs.org/api/components/content-list) that we can <em>in theory</em> pass in a <a target="_blank" href="https://content.nuxtjs.org/api/composables/query-content">queryContent</a> attribute as a <code>:query</code> prop to query our blog posts.
Unfortunately, when I add</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> qc = <span class="hljs-keyword">await</span> queryContent(<span class="hljs-string">"blog"</span>).sort({<span class="hljs-attr">author</span>: <span class="hljs-number">1</span>}).find()
</code></pre>
<p>and remove the <code>path="/blog"</code> from the <code>&lt;ContentList</code> declaration while adding <code>:query="query"</code> then nothing will change.
I don't know if maybe I'm doing something incorrectly or if something is broken, but this doesn't work for me.
So, it is at this point where I'll remove the <code>&lt;ContentList</code> component, and instead just use a generic array of posts.
To understand what I mean, take a look at the updated component:</p>
<pre><code class="lang-vue">&lt;script setup&gt;
import { convertDate } from "../../utils";

const qc = await queryContent("blog").sort({ author: 0 }).find();
&lt;/script&gt;

&lt;template&gt;
  &lt;main class="bg-white px-4 pt-16 pb-20 sm:px-6 lg:px-8 lg:pt-24 lg:pb-28"&gt;
    &lt;div class="mx-auto max-w-lg lg:max-w-7xl"&gt;
      &lt;div class="border-b border-b-gray-200 pb-6"&gt;
        &lt;h2
          class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl"
        &gt;
          Recent Posts
        &lt;/h2&gt;
      &lt;/div&gt;
      &lt;div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12"&gt;
        &lt;div
          v-for="article in qc"
          :key="article._path"
          class="flex flex-col justify-between rounded-lg border border-gray-200 p-4"
        &gt;
          &lt;nuxt-link :href="article._path"&gt;
            &lt;p class="text-xl text-gray-900"&gt;{{ article.title }}&lt;/p&gt;
            &lt;p class="mt-3 text-gray-500"&gt;{{ article.description }}&lt;/p&gt;
          &lt;/nuxt-link&gt;
          &lt;div class="mt-6"&gt;
            &lt;a
              :href="`?author=${article.author}`"
              class="text-sm font-medium text-gray-900"
            &gt;
              {{ article.author }}
            &lt;/a&gt;
            &lt;div class="text-sm text-gray-500"&gt;
              &lt;time datetime="2020-03-16"&gt;{{ convertDate(article.date) }}&lt;/time&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p>This will sort the posts in ascending order by author, so <code>foo-baz.md</code> will appear first in the list.
If we replace <code>{author: 0}</code> with <code>{date: -1}</code>, though, the sorting will work with the dates sorting in descending order.
This works because of the way we've written the date field in each blog post.
What if we don't want to have to use <code>convertDate</code>, though, and instead we just want the date to be the human-readable format?
In that case we would not be able to sort on that because it wouldn't sort the string correctly.
What we can do instead is add a numeric value to the beginning of each blog post title.
For example, <code>foo-bar.md</code> becomes <code>1.foo-bar.md</code>, <code>hello-world.md</code> becomes <code>2.hello-world.md</code>, and <code>foo-baz.md</code> becomes <code>3.foo-baz.md</code> (this numbering was chosen because of the chronological order).
Nothing will change on the display order of the posts, but we can now add the following sorting parameter to the query:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> qc = <span class="hljs-keyword">await</span> queryContent(<span class="hljs-string">"blog"</span>).sort({ <span class="hljs-attr">_file</span>: <span class="hljs-number">-1</span>, <span class="hljs-attr">$numeric</span>: <span class="hljs-literal">true</span> }).find();
</code></pre>
<p>and this will order the posts in the opposite order of the file names, which are increasing with the number at the beginning.
Then, for any new post we create, which we will do in a few minutes with testing out the custom component functionality, we simply increase the number to the next one.</p>
<h3 id="heading-fetching-only-posts-by-a-specific-author">Fetching only posts by a specific author</h3>
<p>As you can see, we already have a link associated with the author field in the list view.
If you go to http://localhost:3000/?author=Jeff you will still get the entire list of posts.
We simply need to add a filter to the <code>queryContent</code> that I set up in the previous section to handle filtering by author.
First, we need to use the <code>useRoute</code> composable to get the "author" field from the query, if it exists.
Then we just add a <code>.where()</code> clause to the query, and this should handle everything for us.</p>
<pre><code class="lang-vue">&lt;script setup&gt;
import { convertDate } from '../../utils'

const { query } = useRoute();
const { author } = query;

const qc = await queryContent("blog").where({author}).sort()
&lt;/script&gt;
</code></pre>
<p>Now, if we go to http://localhost:3000/?author=Jeff we'll see
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928144110/3Lm50ksnA.png" alt="8-author-specific-page.png" /></p>
<h3 id="heading-custom-component">Custom component</h3>
<p>In order to handle this we'll first create a <code>/components/content</code> directory at the root of the project.
We don't need to create a new window URL component like I did in the last post because links external to your Nuxt Content blog will open in a new window regardless.
Instead, we'll create hero and card components (these are pulled directly from the documentation).
We'll create <code>/components/content/Hero.vue</code> and <code>/components/content/Card.vue</code> and set them up to look like</p>
<p><strong>/components/content/Hero.vue</strong></p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;section&gt;
    &lt;h1 class="text-4xl"&gt;&lt;slot /&gt;&lt;/h1&gt;
    &lt;slot name="description" /&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>
<p><strong>/components/content/Card.vue</strong></p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="p-6 border bg-white dark:bg-black dark:border-gray-700 rounded"&gt;
    &lt;slot /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>We can then use them in the newly created blog post, <code>/content/blog/4.test-custom-component.md</code></p>
<pre><code class="lang-markdown">---
title: Testing a Custom Component
author: Bob
<span class="hljs-section">date: 2022-06-03
---</span>

Hi there! Check out my some custom components below!

::hero
:::card
A nested card
::card
A super nested card
::
:::
::
</code></pre>
<p>and we are left with
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654928155341/n3C0uhdE3.png" alt="9-custom-component-page.png" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>And there you have it, a very similar blog setup to what was done in the last post, but this time using Nuxt instead of SvelteKit and using Content v2 instead of MDsveX.
Ultimately I think the route you go depends on the tech with which you're more familiar, but I will say that I am a <em>huge</em> fan of what they've done with Nuxt v3 and Content v2, which makes sense considering I spent the first 4 years of my professional life as a Vue developer.
There will definitely be more content around Nuxt down the road but this is all for now.</p>
]]></content:encoded></item><item><title><![CDATA[Building a blog with SvelteKit, TailwindCSS, and MDsveX]]></title><description><![CDATA[Notes
This post can also be found on my main page at https://www.jvp.design/blog/building-a-blog-with-sveltekit-tailwind-mdsvex
For those who want to follow along in code, you can get it from https://github.com/jvp-design/sveltekit-tailwind-mdsvex-bl...]]></description><link>https://blog.jeffpohlmeyer.com/building-a-blog-with-sveltekit-tailwindcss-and-mdsvex</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/building-a-blog-with-sveltekit-tailwindcss-and-mdsvex</guid><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Svelte]]></category><category><![CDATA[mdsvex]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Fri, 03 Jun 2022 13:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/ylveRpZ8L1s/upload/v1654198289314/eGoJLHqB2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-notes">Notes</h1>
<p>This post can also be found on my main page at https://www.jvp.design/blog/building-a-blog-with-sveltekit-tailwind-mdsvex</p>
<p>For those who want to follow along in code, you can get it from https://github.com/jvp-design/sveltekit-tailwind-mdsvex-blog</p>
<p>A YouTube video accompanying this article has been uploaded. Check it out: https://youtu.be/-OSTAkjGVng</p>
<h1 id="heading-intro">Intro</h1>
<p>In the <a target="_blank" href="https://www.jvp.design/blog/self-hosting-a-font-with-tailwind-and-sveltekit">last blog post</a> I described how to self-host a font in SvelteKit using Tailwind CSS.
I then <em>tried</em> to do a <a target="_blank" href="https://www.youtube.com/watch?v=U5bMAW7SINM">live-stream</a> in which I used the aforementioned functionality and extended it by installing <a target="_blank" href="https://mdsvex.pngwn.io/">MDsveX</a> and setting it up.
I left off the video with a <em>very</em> trimmed down blog sample and noted that I may add styling if people wanted it.
In this post I will not only add in styling but will also include things like pagination and search.</p>
<h1 id="heading-updates-to-the-last-blog-post">Updates to the last blog post</h1>
<p>There were a few things that I did in the video that need to be updated from the last blog post.</p>
<h2 id="heading-different-fonts">Different fonts</h2>
<p>The first thing that is different from the blog post is the fonts that were used.
In the last blog post I used <a target="_blank" href="https://www.fontsquirrel.com/fonts/Walkway">Walkway</a> and <a target="_blank" href="https://www.fontsquirrel.com/fonts/lobster-two">Lobster Two</a> and in the blog post I used <a target="_blank" href="https://www.fontsquirrel.com/fonts/uncut-sans">Uncut Sans</a> and <a target="_blank" href="https://www.fontsquirrel.com/fonts/sprat">Sprat</a>.
As such, the <code>app.css</code> file should replace the <code>@font-face</code> elements and look like</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;

<span class="hljs-keyword">@layer</span> base {
    <span class="hljs-selector-tag">h1</span>,
    <span class="hljs-selector-tag">h2</span>,
    <span class="hljs-selector-tag">h3</span>,
    <span class="hljs-selector-tag">h4</span>,
    <span class="hljs-selector-tag">h5</span>,
    <span class="hljs-selector-tag">h6</span> {
        @apply font-serif;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/sprat/Sprat-Regular.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/sprat/Sprat-RegularMedium.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'sprat'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Sprat'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'sprat'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/sprat/Sprat-RegularBold.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }

    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
            <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/uncut-sans/Uncut-Sans-Regular.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
            <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/uncut-sans/Uncut-Sans-Medium.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'uncut-sans'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut-Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Uncut Sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut-sans'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'uncut sans'</span>),
            <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/uncut-sans/Uncut-Sans-Semibold.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
        <span class="hljs-attribute">font-display</span>: swap;
    }
}
</code></pre>
<p>Here I'm using a few more fonts and I've also set it up where any header tags will automatically use the <em>Sprat</em> font by default.
Then the <code>tailwind.config.js</code> file should now look like this</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> defaultTheme = <span class="hljs-built_in">require</span>(<span class="hljs-string">'tailwindcss/defaultTheme'</span>);

<span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [<span class="hljs-string">'./src/**/*.{html,js,svelte,ts}'</span>],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {
            <span class="hljs-attr">fontFamily</span>: {
                <span class="hljs-attr">sans</span>: [<span class="hljs-string">'uncut-sans'</span>, ...defaultTheme.fontFamily.sans],
                <span class="hljs-attr">serif</span>: [<span class="hljs-string">'sprat'</span>, ...defaultTheme.fontFamily.serif]
            }
        }
    },
    <span class="hljs-attr">plugins</span>: []
};
</code></pre>
<h2 id="heading-mdsvex">MDsveX</h2>
<h3 id="heading-installation">Installation</h3>
<p>The next thing that needs to be done to approach where we were in the video is installing and setting up MDsveX.
The first thing you need to do is install the package from npm</p>
<pre><code class="lang-bash">npm i -D mdsvex
</code></pre>
<h3 id="heading-configuring">Configuring</h3>
<p>Then you'll need to create a file called <code>mdsvex.config.js</code> at the root of your project.
I'll display the file in its entirety here and explain it below</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { defineMDSveXConfig <span class="hljs-keyword">as</span> defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'mdsvex'</span>;
<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">'url'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;

<span class="hljs-keyword">const</span> dirname = path.resolve(fileURLToPath(<span class="hljs-keyword">import</span>.meta.url), <span class="hljs-string">'../'</span>);

<span class="hljs-keyword">const</span> config = defineConfig({
    <span class="hljs-attr">extensions</span>: [<span class="hljs-string">'.md'</span>, <span class="hljs-string">'.svx'</span>],
    <span class="hljs-attr">smartypants</span>: { <span class="hljs-attr">dashes</span>: <span class="hljs-string">'oldschool'</span> },
    <span class="hljs-attr">layout</span>: { <span class="hljs-attr">blog</span>: path.join(dirname, <span class="hljs-string">'./src/routes/blog/_layout.svelte'</span>) }
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<p>The <code>dirname</code> variable effectively converts the "current" location of the project in your own file-system.
This value is then used to point directly to the layout we'll be using for blog posts, called "blog", in the file system of the project.
So, we're telling MDsveX that any file we set up with the template named "blog" should use the <code>_layout.svelte</code> file that we'll be creating and saving in the <code>./src/routes/blog/</code> folder.
The <code>extensions</code> entry in the config just tells MDsveX to look convert files with either the <code>.md</code> or <code>.svx</code> extension.
Information about <em>smartypants</em> functionality can be found at https://mdsvex.pngwn.io/docs#smartypants.</p>
<h3 id="heading-adding-to-sveltekit-project">Adding to SvelteKit Project</h3>
<p>Now we need to tell our SvelteKit project to actually use this config that we've just set up.
In <code>svelte.config.js</code> you'll need to add two things:</p>
<ul>
<li><code>extensions: ['.svelte', ...mdsvexConfig.extensions]</code></li>
<li><code>mdsvex(mdsvexConfig)</code> to the <code>preprocess</code> attribute</li>
</ul>
<p>Your <code>svelte.config.js</code> file should now look like</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { mdsvex } <span class="hljs-keyword">from</span> <span class="hljs-string">'mdsvex'</span>;
<span class="hljs-keyword">import</span> adapter <span class="hljs-keyword">from</span> <span class="hljs-string">'@sveltejs/adapter-auto'</span>;
<span class="hljs-keyword">import</span> preprocess <span class="hljs-keyword">from</span> <span class="hljs-string">'svelte-preprocess'</span>;
<span class="hljs-keyword">import</span> mdsvexConfig <span class="hljs-keyword">from</span> <span class="hljs-string">'./mdsvex.config.js'</span>;

<span class="hljs-comment">/** @type {import('@sveltejs/kit').Config} */</span>
<span class="hljs-keyword">const</span> config = {
    <span class="hljs-attr">extensions</span>: [<span class="hljs-string">'.svelte'</span>, ...mdsvexConfig.extensions],
    <span class="hljs-attr">preprocess</span>: [
        preprocess({
            <span class="hljs-attr">postcss</span>: <span class="hljs-literal">true</span>
        }),
        mdsvex(mdsvexConfig)
    ],
    <span class="hljs-attr">kit</span>: {
        <span class="hljs-attr">adapter</span>: adapter()
    }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<h3 id="heading-adding-a-blog-layout">Adding a Blog Layout</h3>
<p>Finally, we need to add the aforementioned <code>_layout.svelte</code> file to the <code>/src/routes/blog</code> folder.
For now it will simply have a <code>&lt;slot /&gt;</code> element, but we'll add styling and functionality later.</p>
<h2 id="heading-fetching-blog-posts">Fetching Blog Posts</h2>
<h3 id="heading-sample-blog-posts">Sample Blog Posts</h3>
<p>First, let's create a few very simple markdown files to act as blog posts:</p>
<p><strong>/src/routes/blog/foo-bar.md</strong></p>
<pre><code class="lang-markdown">---
title: Foo Bar
author: Jeff
date: 2022-04-15
layout: blog
<span class="hljs-section">excerpt: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in.
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in. Quisque tincidunt egestas libero rhoncus blandit. Etiam elit leo, facilisis id magna sit amet, vestibulum finibus nulla. Vestibulum porttitor nisl id ligula accumsan, et dapibus justo cursus. Phasellus congue mauris vitae dictum auctor. Sed vitae mollis quam. Morbi venenatis metus ligula, sit amet consectetur eros pharetra vel.

Maecenas efficitur mauris eu ex viverra, ut consequat metus ultrices. Sed imperdiet leo odio, in aliquam orci sagittis ut. Vivamus eget sem et nibh faucibus luctus vel a enim. Sed orci tortor, semper ut vulputate at, hendrerit dapibus dolor. Pellentesque tincidunt tempor efficitur. Etiam efficitur pellentesque nisi, sit amet feugiat nisi. Maecenas nisl odio, viverra vitae rhoncus eu, placerat vitae ante. Quisque suscipit nibh lacus, sit amet facilisis tellus fermentum in. Integer nec lacinia risus, ut lobortis ex.

Integer nec ultricies nisi. Curabitur odio mauris, scelerisque at luctus a, bibendum eget velit. Vivamus id tellus lectus. Nulla in purus sit amet mi tincidunt commodo. Ut auctor ante a mauris dignissim laoreet. Nullam magna arcu, tincidunt nec risus et, mattis fringilla augue. Suspendisse imperdiet, purus vel pharetra bibendum, enim purus convallis quam, ut faucibus nibh libero in enim. Curabitur feugiat leo ac accumsan tempor. Ut non convallis mauris, sed rutrum libero.

Maecenas vehicula maximus justo, pellentesque consequat sem dignissim a. Proin quis lectus molestie, pellentesque massa in, egestas orci. Vestibulum facilisis enim at magna scelerisque, quis suscipit quam ultrices. Proin a rutrum tortor. Proin vel scelerisque nunc. Nullam condimentum sit amet magna eu rutrum. Quisque magna enim, aliquet ut blandit et, viverra eu leo. Sed molestie sem et quam consequat mattis. Donec elit velit, cursus at ipsum nec, ullamcorper tincidunt neque.

Nunc convallis odio justo, non interdum dolor ultricies interdum. Curabitur accumsan sem a iaculis placerat. Donec eu facilisis sem, vel bibendum risus. Aliquam non tincidunt est, a auctor magna. Ut erat libero, commodo non malesuada quis, porttitor sit amet libero. Curabitur pulvinar ornare leo id efficitur. Donec sollicitudin arcu venenatis odio elementum, at venenatis erat efficitur. In porta mi et sollicitudin faucibus. Vivamus vel metus interdum, facilisis nisl at, ullamcorper mauris. Sed ac nisl at dolor varius aliquam. In facilisis pretium interdum. Sed tempus purus at pulvinar scelerisque. Etiam eu purus eleifend, commodo turpis eget, aliquet turpis. Mauris fermentum magna dictum lorem bibendum tempor.
</code></pre>
<p><strong>/src/routes/blog/foo-baz.md</strong></p>
<pre><code class="lang-markdown">---
title: Foo Baz
author: Jeff
date: 2022-07-15
layout: blog
<span class="hljs-section">excerpt: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in.
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vestibulum odio nisl, nec pretium dolor varius in. Quisque tincidunt egestas libero rhoncus blandit. Etiam elit leo, facilisis id magna sit amet, vestibulum finibus nulla. Vestibulum porttitor nisl id ligula accumsan, et dapibus justo cursus. Phasellus congue mauris vitae dictum auctor. Sed vitae mollis quam. Morbi venenatis metus ligula, sit amet consectetur eros pharetra vel.

Maecenas efficitur mauris eu ex viverra, ut consequat metus ultrices. Sed imperdiet leo odio, in aliquam orci sagittis ut. Vivamus eget sem et nibh faucibus luctus vel a enim. Sed orci tortor, semper ut vulputate at, hendrerit dapibus dolor. Pellentesque tincidunt tempor efficitur. Etiam efficitur pellentesque nisi, sit amet feugiat nisi. Maecenas nisl odio, viverra vitae rhoncus eu, placerat vitae ante. Quisque suscipit nibh lacus, sit amet facilisis tellus fermentum in. Integer nec lacinia risus, ut lobortis ex.

Integer nec ultricies nisi. Curabitur odio mauris, scelerisque at luctus a, bibendum eget velit. Vivamus id tellus lectus. Nulla in purus sit amet mi tincidunt commodo. Ut auctor ante a mauris dignissim laoreet. Nullam magna arcu, tincidunt nec risus et, mattis fringilla augue. Suspendisse imperdiet, purus vel pharetra bibendum, enim purus convallis quam, ut faucibus nibh libero in enim. Curabitur feugiat leo ac accumsan tempor. Ut non convallis mauris, sed rutrum libero.

Maecenas vehicula maximus justo, pellentesque consequat sem dignissim a. Proin quis lectus molestie, pellentesque massa in, egestas orci. Vestibulum facilisis enim at magna scelerisque, quis suscipit quam ultrices. Proin a rutrum tortor. Proin vel scelerisque nunc. Nullam condimentum sit amet magna eu rutrum. Quisque magna enim, aliquet ut blandit et, viverra eu leo. Sed molestie sem et quam consequat mattis. Donec elit velit, cursus at ipsum nec, ullamcorper tincidunt neque.

Nunc convallis odio justo, non interdum dolor ultricies interdum. Curabitur accumsan sem a iaculis placerat. Donec eu facilisis sem, vel bibendum risus. Aliquam non tincidunt est, a auctor magna. Ut erat libero, commodo non malesuada quis, porttitor sit amet libero. Curabitur pulvinar ornare leo id efficitur. Donec sollicitudin arcu venenatis odio elementum, at venenatis erat efficitur. In porta mi et sollicitudin faucibus. Vivamus vel metus interdum, facilisis nisl at, ullamcorper mauris. Sed ac nisl at dolor varius aliquam. In facilisis pretium interdum. Sed tempus purus at pulvinar scelerisque. Etiam eu purus eleifend, commodo turpis eget, aliquet turpis. Mauris fermentum magna dictum lorem bibendum tempor.
</code></pre>
<p><strong>/src/routes/blog/hello-world.md</strong></p>
<pre><code class="lang-markdown">---
title: Hello World
author: Jeff
date: 2022-05-27
layout: blog
<span class="hljs-section">excerpt: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eget massa sit amet arcu varius lacinia nec quis lacus.
---</span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eget massa sit amet arcu varius lacinia nec quis lacus. Proin auctor lectus a volutpat porta. Nullam eget ipsum convallis, elementum orci sodales, blandit velit. In imperdiet, ligula sed ultricies pharetra, metus mi consequat dui, vitae luctus dolor ligula eu nunc. Fusce consequat mauris ac egestas iaculis. Quisque pharetra et ante maximus convallis. Nulla sollicitudin velit molestie mauris dignissim, at hendrerit diam fringilla. Donec mollis eget ex non iaculis. In a vehicula nisl. Donec dapibus orci in enim posuere, non rhoncus risus ultrices. Pellentesque elementum metus ipsum, ut scelerisque mauris ultrices vel.

Aliquam ullamcorper est vehicula, suscipit nulla pellentesque, convallis odio. Praesent eget elit eget magna fringilla pharetra tempor quis magna. Proin et est vestibulum neque rhoncus mattis non vel lacus. Proin vulputate risus vel dignissim vestibulum. Quisque id sollicitudin neque, sed sagittis urna. Vestibulum vehicula metus sed eros venenatis, sit amet facilisis nunc porta. Nam pharetra luctus sapien, ut venenatis nibh tincidunt mollis. Phasellus efficitur, felis vitae mattis cursus, sapien diam vulputate dui, sit amet pulvinar ante ipsum non urna.

Fusce est nulla, efficitur vitae turpis eget, pretium rutrum turpis. Fusce at lectus eros. Phasellus convallis condimentum dolor ac rutrum. Integer commodo augue et dui efficitur tincidunt. Nam scelerisque egestas quam, vitae ultrices turpis tincidunt rhoncus. Duis rutrum placerat erat. Ut ac tincidunt elit. In laoreet dictum mauris nec posuere. Curabitur tempus, dolor malesuada ultrices feugiat, ipsum eros faucibus tellus, eu ultricies nunc est sed dolor. Suspendisse nisi eros, vehicula vitae iaculis sit amet, aliquet sit amet leo. Sed euismod urna at eros posuere laoreet. Curabitur in sodales lorem. Nulla rutrum aliquam felis ac tempor.

Ut pretium vitae elit ac facilisis. Aliquam nisi tortor, feugiat at lacus sed, condimentum egestas urna. Vestibulum hendrerit augue non urna volutpat, et fermentum tortor pellentesque. Aenean eget pharetra leo. Vestibulum ut laoreet dui. Phasellus nec nunc imperdiet, mollis urna eget, interdum lacus. Nulla ac neque pulvinar ex vestibulum venenatis at sed mi. Aliquam faucibus risus eget dolor porttitor interdum. Phasellus rutrum augue ex, vel tempus velit sollicitudin vitae. Pellentesque libero sapien, ullamcorper nec elementum nec, pharetra sed nisl. Nullam egestas arcu et ex vulputate, pretium vestibulum odio convallis. Nam auctor risus nec fermentum ultricies.

Donec porttitor quis ipsum ut imperdiet. Fusce ac pretium felis, sit amet pharetra orci. Donec vitae quam ac tellus pellentesque fringilla. Curabitur placerat quam a leo imperdiet tincidunt. Nunc porta pulvinar orci sit amet varius. Suspendisse dapibus ipsum nec magna ultricies gravida. Maecenas varius justo ac sem rhoncus lobortis. Integer eget cursus diam. Vestibulum sollicitudin enim at metus scelerisque blandit. In sit amet pulvinar nunc. Sed sit amet rutrum ex, efficitur imperdiet nunc.
</code></pre>
<h3 id="heading-fetch-the-blog-posts-for-svelte">Fetch the Blog Posts for Svelte</h3>
<p>The way the latest version of SvelteKit works is that you no longer need to use <code>&lt;script context="module"&gt;</code> and export an async load function, etc.
We can very simply grab all the posts and return them as a body element from a JavaScript file, and then use the same key as a prop in the Svelte component.
Before we can parse the files I want to introduce a simple function to convert a raw UTC date to a more human-friendly date time.
This is fairly straightforward and I just include it here for reference.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/lib/utils.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> convertDate = <span class="hljs-function">(<span class="hljs-params">published</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> months = {
        <span class="hljs-number">1</span>: <span class="hljs-string">'Jan'</span>,
        <span class="hljs-number">2</span>: <span class="hljs-string">'Feb'</span>,
        <span class="hljs-number">3</span>: <span class="hljs-string">'Mar'</span>,
        <span class="hljs-number">4</span>: <span class="hljs-string">'Apr'</span>,
        <span class="hljs-number">5</span>: <span class="hljs-string">'May'</span>,
        <span class="hljs-number">6</span>: <span class="hljs-string">'Jun'</span>,
        <span class="hljs-number">7</span>: <span class="hljs-string">'Jul'</span>,
        <span class="hljs-number">8</span>: <span class="hljs-string">'Aug'</span>,
        <span class="hljs-number">9</span>: <span class="hljs-string">'Sep'</span>,
        <span class="hljs-number">10</span>: <span class="hljs-string">'Oct'</span>,
        <span class="hljs-number">11</span>: <span class="hljs-string">'Nov'</span>,
        <span class="hljs-number">12</span>: <span class="hljs-string">'Dec'</span>
    };
    <span class="hljs-keyword">const</span> date = published.substring(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
    <span class="hljs-keyword">const</span> [year, month, day] = date.split(<span class="hljs-string">'-'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${day}</span>-<span class="hljs-subst">${months[<span class="hljs-built_in">parseInt</span>(month)]}</span>-<span class="hljs-subst">${year}</span>`</span>;
};
</code></pre>
<p>We then use this function in the next file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// /src/routes/blog/index.js</span>
<span class="hljs-keyword">import</span> { convertDate } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/utils'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> allPostFiles = <span class="hljs-keyword">import</span>.meta.globEager(<span class="hljs-string">'./*.{svx,md}'</span>);
    <span class="hljs-keyword">const</span> allPosts = <span class="hljs-built_in">Object</span>.entries(allPostFiles).map(<span class="hljs-function">(<span class="hljs-params">[path, post]</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> postPath = path.slice(<span class="hljs-number">2</span>, <span class="hljs-number">-3</span>);
        <span class="hljs-keyword">return</span> { ...post.metadata, <span class="hljs-attr">path</span>: postPath, <span class="hljs-attr">published</span>: convertDate(post.metadata.date) };
    });
    <span class="hljs-keyword">const</span> posts = allPosts.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(b.date) - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(a.date));
    <span class="hljs-keyword">if</span> (!posts.length) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-number">404</span> };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">body</span>: { posts } };
}
</code></pre>
<p>The first line fetches all files that are in <code>/src/routes/blog</code> that end with either an <code>.svx</code> or <code>.md</code> extension.
The resulting <code>allPostFiles</code> is somewhat nonsensical for our purposes and looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654198558296/qRF6Z3cj-.png" alt="allPostFiles.png" /></p>
<p>What we can do, though, is cycle through each entry, and fetch the post path (the <code>slice</code> takes off the beginning <code>./</code> characters and ending <code>.md</code> characters) and grabs any metadata created by MDsveX.
The metadata created by MDsveX is all the content in between the <code>---</code> characters, also called "front matter" in the markdown file.
We then simply sort the files descending by date (i.e. most recent first) and if there are no posts then we simply return a 404 error, otherwise we return the posts as the body of the response, with the key of <code>posts</code>.</p>
<h3 id="heading-create-list-view">Create "List View"</h3>
<p>We now need to create a file where we can view all the available blog posts.
This file name needs to match the base (without the extension) of the JavaScript file we just created.
Thus, we will create a <code>/src/routes/blog/index.svelte</code> file and add render the posts.</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    export let posts;
&lt;/script&gt;

&lt;ul&gt;
    {#each posts as post}
        &lt;li&gt;
            &lt;a href="/blog/{post.path}" sveltekit:prefetch&gt;{post.title}&lt;/a&gt;
        &lt;/li&gt;
    {/each}
&lt;/ul&gt;
</code></pre>
<p>and if we navigate to <code>http://localhost:3000/blog/</code> we should then see
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654198573960/SLox3tG17.png" alt="listViewBefore.png" /></p>
<h1 id="heading-new-stuff">New "Stuff"</h1>
<p>Now that we've essentially gotten up-to-date relative to the last video, let's start styling things.</p>
<h2 id="heading-blog-list-view">Blog List View</h2>
<p>The first thing to do is remove the grid layout from the main <code>/src/routes/__layout.svelte</code> file because I want to be able to set the layout in the individual page.
Now <code>__layout.svelte</code> looks like</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    import '../styles/app.css';
&lt;/script&gt;

&lt;div class="h-screen"&gt;
    &lt;slot /&gt;
&lt;/div&gt;
</code></pre>
<p>Now we can update <code>/src/routes/blog/index.svelte</code> with a bit more pleasing styling</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    export let posts;
&lt;/script&gt;

&lt;div class="bg-white pt-16 pb-20 px-4 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8"&gt;
    &lt;div class="max-w-lg mx-auto lg:max-w-7xl"&gt;
        &lt;div class="border-b border-b-gray-200 pb-6"&gt;
            &lt;h2 class="text-3xl tracking-tight font-semibold text-gray-900 sm:text-4xl"&gt;Recent Posts&lt;/h2&gt;
        &lt;/div&gt;
        &lt;div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12"&gt;
            {#each posts as post}
                &lt;div class="border border-gray-200 p-4 rounded-lg flex flex-col justify-between"&gt;
                    &lt;a href="/blog/{post.path}" sveltekit:prefetch&gt;
                        &lt;p class="text-xl text-gray-900"&gt;{post.title}&lt;/p&gt;
                        &lt;p class="mt-3 text-gray-500"&gt;{post.excerpt}&lt;/p&gt;
                    &lt;/a&gt;
                    &lt;div class="mt-6"&gt;
                        &lt;p class="text-sm font-medium text-gray-900"&gt;
                            {post.author}
                        &lt;/p&gt;
                        &lt;div class="text-sm text-gray-500"&gt;
                            &lt;time datetime="2020-03-16"&gt;{post.published}&lt;/time&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            {/each}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>There is a lot to work through in all of the classes above and I encourage you to go to the <a target="_blank" href="https://tailwindcss.com/">Tailwind website</a> to explore what each thing is doing, but you can see what the resulting image looks like here
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654198586842/jM7bH2TFZ.png" alt="listViewAfter.png" /></p>
<h2 id="heading-blog-detail-view">Blog "Detail View"</h2>
<p>We now want to update the styling for each blog post.
The first thing we need to do is install another dependency because we'll be using some built-in Tailwind functionality.
We can install the <em>typography</em> package by typing</p>
<pre><code class="lang-bash">npm i -D @tailwindcss/typography
</code></pre>
<p>and we then add this as a plugin in <code>tailwind.config.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// 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-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [<span class="hljs-string">'./src/**/*.{html,js,svelte,ts}'</span>],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {
            <span class="hljs-attr">fontFamily</span>: {
                <span class="hljs-attr">sans</span>: [<span class="hljs-string">'uncut-sans'</span>, ...defaultTheme.fontFamily.sans],
                <span class="hljs-attr">serif</span>: [<span class="hljs-string">'sprat'</span>, ...defaultTheme.fontFamily.serif]
            }
        }
    },
    <span class="hljs-attr">plugins</span>: [<span class="hljs-built_in">require</span>(<span class="hljs-string">'@tailwindcss/typography'</span>)]
};
</code></pre>
<h3 id="heading-update-srcroutesbloglayoutsvelte">Update <code>/src/routes/blog/_layout.svelte</code></h3>
<p>Now that we actually have some blog posts we'll need to update our blog <code>_layout.svelte</code> file to be able to consume them.
The first thing we're going to need to do is expose the props that we'll want to use in the post content, namely</p>
<ul>
<li><code>title</code></li>
<li><code>author</code></li>
<li><code>date</code></li>
</ul>
<p>and we'll also need to import <code>convertDate</code> to make a "pretty" date on the post.
The script section of <code>/src/routes/blog/_layout.svelte</code> will then look like</p>
<pre><code class="lang-javascript">&lt;script&gt;
    <span class="hljs-keyword">import</span> {convertDate} <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/utils.js'</span>; <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> title; <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> author; <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> date;
&lt;/script&gt;
</code></pre>
<p>Then we'll set certain display characteristics on the main content, such as centering, setting max widths, etc</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative mx-auto max-w-prose overflow-hidden bg-white py-16 px-4 sm:px-6 lg:px-8"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>We then add a back button to go to the previous page, and for this we'll also directly use the svg code from <a target="_blank" href="https://heroicons.com/">Heroicons</a> for <code>arrow-left</code>.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"block cursor-pointer"</span>
    <span class="hljs-attr">on:click</span>=<span class="hljs-string">{()</span> =&gt;</span> {
        history.back();
    }}
&gt;
    <span class="hljs-tag">&lt;<span class="hljs-name">svg</span>
        <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"inline h-6 w-6"</span>
        <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span>
        <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span>
        <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span>
        <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"2"</span>
    &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">stroke-linecap</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">stroke-linejoin</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M11 17l-5-5m0 0l5-5m-5 5h12"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
    Back
<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
</code></pre>
<p>Finally we'll add a bit of simple styling for the title, author/date combination, and the main content</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-4 text-4xl font-semibold"</span>&gt;</span>{title}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-4 text-gray-500"</span>&gt;</span>by {author}, {convertDate(date)}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"prose prose-lg first-letter:text-3xl first-letter:text-blue-600"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">slot</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Thus, now the whole <code>_layout.svelte</code> component looks like</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    import { convertDate } from '$lib/utils.js';

    export let title;
    export let author;
    export let date;
&lt;/script&gt;

&lt;div class="relative mx-auto max-w-prose overflow-hidden bg-white py-16 px-4 sm:px-6 lg:px-8"&gt;
    &lt;span
        class="block cursor-pointer"
        on:click={() =&gt; {
            history.back();
        }}
    &gt;
        &lt;svg
            xmlns="http://www.w3.org/2000/svg"
            class="inline h-6 w-6"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
            stroke-width="2"
        &gt;
            &lt;path stroke-linecap="round" stroke-linejoin="round" d="M11 17l-5-5m0 0l5-5m-5 5h12" /&gt;
        &lt;/svg&gt;
        Back
    &lt;/span&gt;
    &lt;h2 class="my-4 text-4xl font-semibold"&gt;{title}&lt;/h2&gt;
    &lt;p class="my-4 text-gray-500"&gt;by {author}, {convertDate(date)}&lt;/p&gt;
    &lt;div class="prose prose-lg first-letter:text-3xl first-letter:text-blue-600"&gt;
        &lt;slot /&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>and if we visit <code>http://localhost:3000/blog/foo-baz</code> then we're greeted by the following image
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654198609641/QtlKp_RX9.png" alt="postScreenShot.png" /></p>
<h2 id="heading-extra-functionality">Extra Functionality</h2>
<p>There are two main things that can be helpful in what we're doing</p>
<ul>
<li>Search</li>
<li>Custom Svelte components in markdown</li>
</ul>
<h3 id="heading-search">Search</h3>
<p>To demonstrate this functionality we will do a search by author.
So, go ahead and change the author of any of the already existing blog posts (I'll be doing this on <code>foo-baz.md</code>) to any other name (I'm using "Alice").
Next we need to go into <code>/src/routes/blog/index.js</code> and look for this search functionality.
The first step is to add an argument called <code>{url}</code> to the main <code>get</code> function.
Now the function signature looks like <code>export async function get({ url }) {</code></p>
<p>We can next add in a filter to check the author, if one is passed in from the <code>url</code> argument.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { convertDate } <span class="hljs-keyword">from</span> <span class="hljs-string">'$lib/utils.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get</span>(<span class="hljs-params">{ url }</span>) </span>{
    <span class="hljs-keyword">const</span> allPostFiles = <span class="hljs-keyword">import</span>.meta.globEager(<span class="hljs-string">'./*.{svx,md}'</span>);
    <span class="hljs-keyword">const</span> allPosts = <span class="hljs-built_in">Object</span>.entries(allPostFiles).map(<span class="hljs-function">(<span class="hljs-params">[path, post]</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> postPath = path.slice(<span class="hljs-number">2</span>, <span class="hljs-number">-3</span>);
        <span class="hljs-keyword">return</span> { ...post.metadata, <span class="hljs-attr">path</span>: postPath, <span class="hljs-attr">published</span>: convertDate(post.metadata.date) };
    });
    <span class="hljs-comment">// New Code Starts Here</span>
    <span class="hljs-keyword">const</span> authorPosts = allPosts.filter(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> author = url.searchParams.get(<span class="hljs-string">'author'</span>);
        <span class="hljs-keyword">if</span> (!author) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">return</span> post.author === author;
    });
    <span class="hljs-keyword">const</span> posts = authorPosts.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(b.date) - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(a.date));
    <span class="hljs-comment">// New Code Ends Here</span>
    <span class="hljs-keyword">if</span> (!posts.length) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-number">404</span> };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">body</span>: { posts } };
}
</code></pre>
<p>In the last line of the "New Code" block, I've simply replaced the array on which I'm sorting <em>from</em> <code>allPosts</code> to <code>authorPosts</code>.
The way this works is that if there is no <code>author</code> parameter included in the search then just return all posts, otherwise only return posts where the author matches the author term queried for.</p>
<p>Next we need to add functionality where a user can search by author from the main blog list view.
This is done very simply in that we just need to replace the <code>p</code> tag that displays the author name in <code>/src/routes/blog/index.svelte</code> with an <code>a</code> tag and set the <code>href</code> attribute to be <code>?author={post.author}</code>.
The updated component looks like</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    export let posts;
&lt;/script&gt;

&lt;div class="bg-white pt-16 pb-20 px-4 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8"&gt;
    &lt;div class="max-w-lg mx-auto lg:max-w-7xl"&gt;
        &lt;div class="border-b border-b-gray-200 pb-6"&gt;
            &lt;h2 class="text-3xl tracking-tight font-semibold text-gray-900 sm:text-4xl"&gt;Recent Posts&lt;/h2&gt;
        &lt;/div&gt;
        &lt;div class="mt-12 grid gap-16 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12"&gt;
            {#each posts as post}
                &lt;div class="border border-gray-200 p-4 rounded-lg"&gt;
                    &lt;a href="/blog/{post.path}" sveltekit:prefetch&gt;
                        &lt;p class="text-xl text-gray-900"&gt;{post.title}&lt;/p&gt;
                        &lt;p class="mt-3 text-gray-500"&gt;{post.excerpt}&lt;/p&gt;
                    &lt;/a&gt;
                    &lt;div class="mt-6"&gt;
                        &lt;a href="?author={post.author}" class="text-sm font-medium text-gray-900"&gt;
                            {post.author}
                        &lt;/a&gt;
                        &lt;div class="text-sm text-gray-500"&gt;
                            &lt;time datetime="2020-03-16"&gt;{post.published}&lt;/time&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            {/each}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>This functionality can be extended to any number of search parameters as well as things like pagination, but that will be handled another time.</p>
<h3 id="heading-custom-svelte-components">Custom Svelte Components</h3>
<p>One problem that I had when I tried using "simple" CMSes was that any time I wanted to include a URL it would open in the current page.
This defeats one of the goals of a blog as it pertains to SEO: the amount of time spent on your site.
Thus, ultimately, it would be beneficial to be able to have a way to tell SvelteKit to open URLs in a new page.
We can do this by creating a custom Svelte component that we can use in our markdown files.</p>
<h4 id="heading-newwindowurl-component">NewWindowUrl Component</h4>
<p>This will be a fairly simple component in that all we need it to render is a pre-defined url with a description and have the <code>target="_blank"</code> attribute on it.</p>
<pre><code class="lang-sveltehtml">&lt;script&gt;
    // /src/lib/blog-components/NewWindowUrl.svelte

    export let url;
    export let description;

    if (!description) {
        description = url
    }
&lt;/script&gt;

&lt;a href={url} target="_blank"&gt;{description}&lt;/a&gt;
</code></pre>
<p>Now we can use this in one of our blog posts (markdown files).
Let's add a link to the root route of my site, but have it open in a new page.
We'll create a new blog post:
<strong>/src/routes/blog/test-custom-url.md</strong></p>
<pre><code class="lang-markdown">---
title: Testing a Custom URL Component
author: Bob
date: 2022-06-03
layout: blog
<span class="hljs-section">excerpt: Blah Blah testing a simple custom URL component
---</span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span></span>
import NewWindowUrl from '$lib/blog-components/NewWindowUrl.svelte'
<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

Hi there! Go ahead and give my <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">NewWindowUrl</span> <span class="hljs-attr">url</span>=<span class="hljs-string">"https://www.jvp.design"</span> <span class="hljs-attr">description</span>=<span class="hljs-string">"main site"</span> /&gt;</span></span> a visit!
</code></pre>
<p>Then if we visit <code>http://localhost:3000/blog/test-custom-url</code> and click on the link it should open my main site on a new page.</p>
<p>We can use this for any number of things.
For example, for the blog posts for this site I have a custom component to handle different image formats (<code>.webp</code> and <code>.png</code>) which use the <code>picture</code> tag with a <code>source</code> tag for each item in an image's <code>srcset</code> group.
Maybe that can be another (shorter) blog post.</p>
]]></content:encoded></item><item><title><![CDATA[Self-hosting a font with Tailwind and SvelteKit]]></title><description><![CDATA[Note: This post can also be found on my main page at https://www.jvp.design/blog/self-hosting-a-font-with-tailwind-and-sveltekit
A few months ago I wrote a post where I described how to self-host a font in SvelteKit. In that post I noted that it seem...]]></description><link>https://blog.jeffpohlmeyer.com/self-hosting-a-font-with-tailwind-and-sveltekit</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/self-hosting-a-font-with-tailwind-and-sveltekit</guid><category><![CDATA[Svelte]]></category><category><![CDATA[CSS]]></category><category><![CDATA[fonts]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Tue, 24 May 2022 17:01:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/LIxVYpcBqhE/upload/v1653406419079/vr2U1bgr8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> This post can also be found on my main page at https://www.jvp.design/blog/self-hosting-a-font-with-tailwind-and-sveltekit</p>
<p>A few months ago I wrote a <a target="_blank" href="https://jeffpohlmeyer.com/self-hosting-a-font-with-tailwind-jit-and-sveltekit">post</a> where I described how to self-host a font in SvelteKit. In that post I noted that it seemed that <code>.ttf</code> font files weren't being properly recognized and that you would need to convert to <code>.woff</code> files. Well, I don't know if I was just doing something wrong (most likely) or it was a function of <a target="_blank" href="https://tailwindcss.com/">Tailwind v3</a> being officially released or maybe an updated version of SvelteKit (both less likely), but there is an easier way to do this.</p>
<h2 id="heading-a-quick-thanks">A quick thanks</h2>
<p>First things first, I want to thank <a class="user-mention" href="https://hashnode.com/@danxcraig">Dan</a> for the comment on my other post, which caused me to come back and look at it again. The method in the previous post very well may have worked but it was going to be a pain in the butt to have to always go and convert the files when <code>.tff</code> files are so much more widely available.</p>
<h2 id="heading-a-note-to-site-builders">A note to site builders</h2>
<p>If you're using SvelteKit the reason is likely because you care about performance. I wrote a bit about the benefits of custom sites over no-code or low-code builders on my other blog (https://www.jvp.design/blog/the-importance-of-a-web-presence-in-2022) but one thing I will mention is that if you're linking to Google Fonts in your site it <em>will</em> slow down the page load and will hurt performance. If getting that famed 💯 on the pagespeed/lighthouse performance metric is a goal then you <em>need</em> to self-host. That's not to say that if you don't self-host then the performance is going to be bad, but this is a very quick and easy way to squeeze just a bit more performance out of your site.</p>
<h1 id="heading-the-process">The process</h1>
<p>I'm going to go in just a little more detail here than I did before. Here's what I'll do</p>
<ul>
<li>Set up a new SvelteKit app</li>
<li>Install Tailwind v3</li>
<li>Download a specific font</li>
<li>Show how to use said font on your site</li>
</ul>
<h2 id="heading-setting-up-a-new-sveltekit-app">Setting up a new SvelteKit app</h2>
<p>This is the easy part. You can follow along with two very simple descriptions; the one on <a target="_blank" href="https://kit.svelte.dev/">SvelteKit's site</a> or the one specific to SvelteKit on <a target="_blank" href="https://tailwindcss.com/docs/guides/sveltekit">Tailwind's site</a>. I'm going to stick with the former and then go over to Tailwind for part 2.</p>
<p>In case you don't want to go to another site (I know, why not just stay here???), you simply need to run <code>npm init svelte my-app</code> and select "Skeleton project" and then choose whatever options you want. For the purposes of this project I'm not using any type checking nor am I including linting, prettier, or playwright.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653401619923/zi_wwIA02.png" alt="Screen Shot 2022-05-24 at 10.13.30 AM.png" />
Then you type</p>
<pre><code>cd my<span class="hljs-operator">-</span>app
npm i
npm run dev <span class="hljs-operator">-</span><span class="hljs-operator">-</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span>open
</code></pre><p>and you should have a running SvelteKit app that looks something like this
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653401711475/o6sx6brWj.png" alt="Screen Shot 2022-05-24 at 10.14.56 AM.png" /></p>
<h2 id="heading-installing-tailwind">Installing Tailwind</h2>
<p>As previously mentioned, we can go to <a target="_blank" href="https://tailwindcss.com/docs/guides/sveltekit">Tailwind's site</a> and skip to part 2 of the description. If you don't want to go there and just want to follow here, this is what you do.</p>
<h3 id="heading-install-dependencies">Install dependencies</h3>
<p>You'll need to install tailwind itself, as well as a couple of other dependencies.</p>
<pre><code>npm i <span class="hljs-operator">-</span>D tailwindcss postcss autoprefixer svelte<span class="hljs-operator">-</span>preprocess
npx tailwindcss init tailwind.config.cjs <span class="hljs-operator">-</span>p
mv postcss.config.js postcss.config.cjs
</code></pre><h3 id="heading-update-svelteconfigjs">Update <code>svelte.config.js</code></h3>
<p>Then you'll need to open the <code>svelte.config.js</code> file and change it <strong>from</strong> this</p>
<pre><code>import adapter <span class="hljs-keyword">from</span> <span class="hljs-string">'@sveltejs/adapter-auto'</span>;

<span class="hljs-comment">/** <span class="hljs-doctag">@type</span> {import('<span class="hljs-doctag">@sveltejs</span>/kit').Config} */</span>
<span class="hljs-keyword">const</span> config = {
    kit: {
        adapter: adapter()
    }
};

export <span class="hljs-keyword">default</span> config;
</code></pre><p><strong>to</strong> this</p>
<pre><code><span class="hljs-keyword">import</span> adapter <span class="hljs-keyword">from</span> <span class="hljs-string">'@sveltejs/adapter-auto'</span>;
<span class="hljs-keyword">import</span> { preprocess } <span class="hljs-keyword">from</span> <span class="hljs-string">'svelte-preprocess'</span>;

<span class="hljs-comment">/** @type {import('@sveltejs/kit').Config} */</span>
<span class="hljs-keyword">const</span> config = {
    <span class="hljs-attr">preprocess</span>: [
        preprocess({ <span class="hljs-attr">postcss</span>: <span class="hljs-literal">true</span> })
    ],
    <span class="hljs-attr">kit</span>: {
        <span class="hljs-attr">adapter</span>: adapter()
    }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre><h3 id="heading-update-tailwindconfigcjs">Update <code>tailwind.config.cjs</code></h3>
<p>Then open <code>tailwind.config.cjs</code> and add <code>'./src/**/*.{html,js,svelte,ts}'</code> to the content field. It should look like this</p>
<pre><code><span class="hljs-string">module.exports</span> <span class="hljs-string">=</span> {
  <span class="hljs-attr">content:</span> [<span class="hljs-string">'./src/**/*.{html,js,svelte,ts}'</span>],
  <span class="hljs-attr">theme:</span> {
    <span class="hljs-attr">extend:</span> {}
  },
  <span class="hljs-attr">plugins:</span> []
}<span class="hljs-string">;</span>
</code></pre><h3 id="heading-set-up-css-file">Set up CSS file</h3>
<p>Next you'll want to create a file called <code>app.css</code> (or whatever you want to name it) and save it wherever you want. I tend to save it in <code>./src/styles/app.css</code> but you can save it anywhere. Then you'll want to add the "big 3" tailwind directives to this file.</p>
<pre><code><span class="hljs-comment">// app.css</span>

<span class="hljs-variable">@tailwind</span> base;
<span class="hljs-variable">@tailwind</span> components;
<span class="hljs-variable">@tailwind</span> utilities;
</code></pre><h3 id="heading-import-appcss-into-your-layout">Import <code>app.css</code> into your layout</h3>
<p>Finally, create a <code>__layout.svelte</code> file in the <code>./src/routes</code> directory and import this newly-created css file.</p>
<pre><code><span class="hljs-operator">&lt;</span>script<span class="hljs-operator">&gt;</span>
  <span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/app.css"</span>;
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>script<span class="hljs-operator">&gt;</span>

<span class="hljs-operator">&lt;</span>slot <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
</code></pre><p>remember where you saved your css file and replace what I've written as the file path with whatever you've chosen. Now your app should look like this
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653402455187/4uqPz3xDO.png" alt="Screen Shot 2022-05-24 at 10.27.19 AM.png" />
In order to make it a little more pleasing, let's open the <code>./src/routes/index.svelte</code> file and add a bit of styling. We'll wrap both elements in a <code>div</code> so we can center the elements and add some basic text styling to both individual elements</p>
<pre><code><span class="hljs-operator">&lt;</span>div class<span class="hljs-operator">=</span><span class="hljs-string">"h-screen grid place-content-center bg-gray-300"</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>h1 class<span class="hljs-operator">=</span><span class="hljs-string">"text-5xl text-blue-600"</span><span class="hljs-operator">&gt;</span>Welcome to SvelteKit<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>h1<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>p class<span class="hljs-operator">=</span><span class="hljs-string">"text-xl text-red-500"</span><span class="hljs-operator">&gt;</span>Visit <span class="hljs-operator">&lt;</span>a href<span class="hljs-operator">=</span><span class="hljs-string">"https://kit.svelte.dev"</span><span class="hljs-operator">&gt;</span>kit.svelte.dev&lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">&gt;</span> to read the documentation<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>p<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
</code></pre><p>The app now looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653403378366/3mc7EExit.png" alt="Screen Shot 2022-05-24 at 10.42.38 AM.png" />
which is not great, but it's a little more visually appealing.</p>
<h2 id="heading-using-custom-fonts">Using custom fonts</h2>
<h3 id="heading-download">Download</h3>
<p>I'm going to try this with two separate fonts: <a target="_blank" href="https://www.fontsquirrel.com/fonts/Walkway">Walkway</a> and <a target="_blank" href="https://www.fontsquirrel.com/fonts/lobster-two">Lobster Two</a>. One is a <code>.ttf</code> file format and the other is <code>.otf</code>. I've created a new folder within the top-level <code>static</code> folder called "fonts" and have saved both <code>.zip</code> files there and subsequently extracted them there.
Now the file structure should look a bit like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653405405251/MaSGGnHFW.png" alt="Screen Shot 2022-05-24 at 11.00.30 AM.png" /></p>
<h3 id="heading-configure-font-face-in-css-file">Configure <code>font-face</code> in CSS file</h3>
<p>Now that we have the font files downloaded, we need to add the font face rules for the ones we want to use. Open up <code>./src/styles/app.css</code> and add the following:</p>
<pre><code><span class="hljs-keyword">@layer</span> base {
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Lobster Two'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Lobster Two'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Lobster-Two'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'lobster two'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'lobster-two'</span>),
            <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/lobster-two/LobsterTwo-Regular.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Lobster Two'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Lobster Two Bold'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'Lobster-Two-Bold'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'lobster two bold'</span>),
            <span class="hljs-built_in">local</span>(<span class="hljs-string">'lobster-two-bold'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/lobster-two/LobsterTwo-Bold.otf'</span>);
        <span class="hljs-attribute">font-weight</span>: bold;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Walkway'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Walkway'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'walkway'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/Walkway/Walkway_Bold.ttf'</span>);
        <span class="hljs-attribute">font-weight</span>: normal;
        <span class="hljs-attribute">font-display</span>: swap;
    }
    <span class="hljs-keyword">@font-face</span> {
        <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Walkway'</span>;
        <span class="hljs-attribute">src</span>: <span class="hljs-built_in">local</span>(<span class="hljs-string">'Walkway'</span>), <span class="hljs-built_in">local</span>(<span class="hljs-string">'walkway'</span>), <span class="hljs-built_in">url</span>(<span class="hljs-string">'/fonts/Walkway/Walkway_Black.ttf'</span>);
        <span class="hljs-attribute">font-weight</span>: bold;
        <span class="hljs-attribute">font-display</span>: swap;
    }
}
</code></pre><p>A few things to discuss:</p>
<ul>
<li><code>@layer base</code> is the simplest way to incorporate these. For a bit more detail visit this site: https://tailwindcss.com/docs/adding-custom-styles#using-css-and-layer</li>
<li>I've added a bunch of possible spellings of the font names just in case the user already has them saved locally on their machine. I tend to like to replace spaces with hyphens and include title and lower case.</li>
<li>I've added in <code>bold</code> and <code>normal</code> font weights, but you can add in whatever you need. For example, if you want to add a semi bold style to <code>Walkway</code> you can directly set the <code>font-weight</code> property to be whichever numeric value would work (for semibold it's 600).<h3 id="heading-add-font-families-to-tailwindconfigcjs">Add font families to <code>tailwind.config.cjs</code></h3>
Now that we've added them to the CSS, we need to indicate to Tailwind what we want to call them. For example, if you want to change the style of a specific line of text you can use the Tailwind class <code>font-&lt;font-name&gt;</code> where <code>&lt;font-name&gt;</code> is whatever you're calling it. You'll see what I mean in a minute.</li>
</ul>
<p>In <code>tailwind.config.cjs</code> we're going to modify the <code>extend</code> property like so:</p>
<pre><code><span class="hljs-attribute">theme</span>: {
  <span class="hljs-attribute">extend</span>: {
    <span class="hljs-attribute">fontFamily</span>: {
      <span class="hljs-attribute">walkway</span>: [<span class="hljs-string">'Walkway'</span>],
      <span class="hljs-attribute">lobster</span>: [<span class="hljs-string">'Lobster Two'</span>]
    }
  }
}
</code></pre><p>The values inside the array are the values that we set in the <code>font-family</code> lines inside the CSS file. If, in <code>app.css</code> we had replaced <code>Lobster Two</code> with just <code>Lobster</code> then we would also have to change that in <code>tailwind.config.cjs</code>.</p>
<h3 id="heading-style-our-app">Style our app</h3>
<p>Now that we've downloaded, installed, and declared our fonts the only thing left to do is to use them in the app itself. Open up <code>./src/routes/index.svelte</code> and use the fonts as you see fit. I've chosen to use <code>font-walkway</code> on the <code>h1</code> and <code>font-lobster font-bold</code> on the <code>p</code> tag. The code looks like this</p>
<pre><code><span class="hljs-operator">&lt;</span>div class<span class="hljs-operator">=</span><span class="hljs-string">"h-screen grid place-content-center bg-gray-300"</span><span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span>h1 class<span class="hljs-operator">=</span><span class="hljs-string">"text-5xl text-blue-600 font-walkway"</span><span class="hljs-operator">&gt;</span>Welcome to SvelteKit<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>h1<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span>p class<span class="hljs-operator">=</span><span class="hljs-string">"text-xl text-red-500 font-lobster font-bold"</span><span class="hljs-operator">&gt;</span>Visit <span class="hljs-operator">&lt;</span>a href<span class="hljs-operator">=</span><span class="hljs-string">"https://kit.svelte.dev"</span><span class="hljs-operator">&gt;</span>kit.svelte.dev&lt;<span class="hljs-operator">/</span>a<span class="hljs-operator">&gt;</span> to read the documentation<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>p<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
</code></pre><p>and the resulting app looks like this
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653406223419/Cwb9gW1bC.png" alt="Screen Shot 2022-05-24 at 11.29.53 AM.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>There you have it, a very simple way to include not only <code>.ttf</code> files but also <code>.otf</code> files in your SvelteKit app. This may have seemed like a somewhat long process, but once you do it once or twice you get the hang of it and, like I said, it's a quick and easy way to add a small performance boost to the site you're working on, which is likely the reason you're using SvelteKit for your app instead of something like Next or Remix.</p>
]]></content:encoded></item><item><title><![CDATA[My Weight Loss Journey]]></title><description><![CDATA[As I mentioned in my last post, what I've been doing with the bulk of whatever free time I have over the past ~6 months or so has been building a website. Before I get into detail about why and how I built it, here it is: https://www.weightpoints.plu...]]></description><link>https://blog.jeffpohlmeyer.com/my-weight-loss-journey</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/my-weight-loss-journey</guid><category><![CDATA[personal]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Mon, 11 Apr 2022 15:38:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649691643463/_3RIdtYGW.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As I mentioned in my last post, what I've been doing with the bulk of whatever free time I have over the past ~6 months or so has been building a website. Before I get into detail about why and how I built it, here it is: <a target="_blank" href="https://www.weightpoints.plus">https://www.weightpoints.plus</a></p>
<h2 id="heading-why-i-built-it">Why I Built It</h2>
<h3 id="heading-backstory">Backstory</h3>
<p>I'm a fairly large man. I'm 6'3" (1.9m) and I've always been fairly muscular/athletic/husky depending on the stage in my life. As a point of reference, when I got on the scale for football weigh-ins in 8th grade I was 6'0" and 196 pounds. By the time I was a freshman in high school (14 years old) I was 6'3" and 240 pounds and for the next 10 years or so my weight would fluctuate up or down. There were times I was up to 250-260 and then when I was 19 I had an aversion to eating (I'm not going to disrespect people suffering from <em>actual</em> eating disorders to compare what I had with their struggle) and got down to 196. I know the <a target="_blank" href="https://www.cdc.gov/healthyweight/assessing/bmi/adult_bmi/english_bmi_calculator/bmi_calculator.html">CDC</a> indicates that 148-199 is healthy for someone my height, but I can easily tell you that 196 was not a healthy weight for me. The sweet spot should be anywhere from 210-235 for me.</p>
<p>I met my wife in June 2005 (I actually know the exact day), and I was ~240 at the time, which felt like a decent weight, but after a few years of being together I started to put on pounds. My wife has always been a very good cook and for a while I had trouble with willpower and portion control. When we got married in 2008 I was pushing 275-280, and by the time I went to study in England for a spell while in grad school in 2012, I was up to 300 pounds.</p>
<h3 id="heading-the-revelation">The Revelation</h3>
<p>While I was in England I walked <em>everywhere</em>. I obviously didn't have a car and the walk was fairly pleasant through much of Oxford and down Cornmarket St, and it ended up being about 2 miles each way. When you combine going to get lunch and such, I generally walked 4.5-5 miles each day. I still hadn't had any revelations about eating healthy or anything like that, so I would have sandwiches for lunch every day, pasta for dinner, cookies after, and other fun stuff. The thing was, though, because I was walking everywhere I ended up losing 25 pounds in about 7 weeks.</p>
<p>When I got back I wanted to try to do something similar so I got a bike to ride outside, I tried exercise programs like <a target="_blank" href="https://www.teambeachbody.com/shop/us/d/p90x-base-kit-P90XBase">P90X</a> and <a target="_blank" href="https://www.beachbodyondemand.com/programs/insanity/start-here?referralprogramid=SAN&amp;trainername=ShaunT">Insanity</a> but none of it worked because I was still eating whatever I wanted thinking that I could replace 5 miles of walking every day with 30-45 minutes of cardio workouts. Side note, is it just me or is anyone else bothered that in the older versions of Insanity that Shaun T's beard is uneven on the sides of his face?</p>
<p>It wasn't until July 2012 when I saw Charles Barkley selling Weight Watchers for men that I realized I probably needed to modify my eating habits, as well. So I signed up for Weight Watchers and started doing <a target="_blank" href="https://ddpyoga.com/">DDP Yoga</a>, although it was called YRG back then. The driver for using DDP Yoga was two-fold: the workouts were shorter than P90X, and the intensity on my joints was lower than something like Insanity. This video also didn't hurt with the motivation</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=qX9FSZJu448">https://www.youtube.com/watch?v=qX9FSZJu448</a></div>
<p>I was using their Points Plus program, and from July 2012 to February 2013 I went from ~275 pounds all the way down to 195 pounds. I had finally found something that worked.</p>
<h3 id="heading-the-switch">The Switch</h3>
<p>I figured that 195 was a good weight at the time, although as I mentioned before it really wasn't, and I figured I'd be okay to go into maintenance mode. This worked great for me, I was chugging right along maintaining a healthy weight for a while, and then out of the blue one day, my points changed. Weight Watchers, without any warning, had decided to do away with Points Plus and switch everyone over to what they called SmartPoints.</p>
<p>I understood the motivation for them to do it, but it didn't help me. I had success with the old system, and my breakfast which amounted to 11 points on the old system was now 15. Combine that with the fact that my daily allowance of points dropped ~10 points or so and this was a no go for me. Luckily I found a couple of sites online that had published unofficial formulas for all things points plus, so I put together a handy spreadsheet so I could track on my own. I did this on and off for years, with my weight hovering in the range of 210-230 in general.</p>
<h3 id="heading-the-pandemic">The Pandemic</h3>
<p>Just prior to the pandemic, in December 2019, we purchased a fairly well-known exercise bike whose name starts with "P" (not naming since they seemingly have trademarked the word as it refers to exercise equipment) and it was a fun and very convenient way to get some easy exercise in. As the pandemic set in I stopped tracking what I was eating as much, as many people did, but because we had the bike it didn't matter as much; the amount of effort I put in on the bike generally offset what I was eating.</p>
<p>I would unofficially track food in my makeshift spreadsheet off and on for a while, but it wasn't until late 2021 that I noticed that the macro I had coded in Excel wasn't actually calculating correctly in the spreadsheet. The formula was correct but for some reason when the foods were being included in recipes the formula was somehow breaking. Since I was removing Excel from my computer and migrating to cloud spreadsheets (that don't natively support macros) and I wanted to add a site to my portfolio, I figured I'd build the site myself.</p>
<h3 id="heading-building-the-site">Building the Site</h3>
<h4 id="heading-first-steps">First Steps</h4>
<p>The plan was to try to build this quickly but to also learn some new tech. Furthermore, I initially was considering using a relational database but trying to figure out how to relate foods and recipes with actual instances of foods and recipes as well as including them all in meals seemed nonsensical. So, at first I tried building the backend in <a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a> using a NoSQL database. I used <a target="_blank" href="https://roman-right.github.io/beanie/">Beanie</a> and <a target="_blank" href="https://fastapi-users.github.io/fastapi-users/configuration/overview/">FastAPI Users</a> for general scaffolding with a Mongo database. The plan was to then build the frontend in either React or (preferentially) Vue.</p>
<h4 id="heading-indexing-problems">Indexing Problems</h4>
<p>This all worked generally well until I got to the point where I realized I was going to want to search for foods by name, as well as include partial searches. For example, if you want to search for "mayo" it should return all items that include those 4 letters, case-insensitive. The main problem that ended up happening was trying to set up an index in Beanie. It's possible, I'm sure, but as I was messing around with it I started to get frustrated and was already realizing that I didn't want to have to worry about handling authentication tokens, hook in routing, and deal with all of the complicated stuff that can go into building a decoupled frontend and backend.</p>
<h4 id="heading-django-to-the-rescue">Django to the Rescue</h4>
<p>Ultimately I came to the conclusion that for something as simple, from a user perspective, as I wanted to build there was no need to over-complicate the situation. Vue, Svelte, React, et al. are great tools for building web applications, but the site I was building was effectively meant to be what websites used to be 10-20 years ago: a simple set of documents with embedded forms. There was <em>very</em> little interactivity that was needed, so Django's highly opinionated nature lended itself to extremely fast development. </p>
<p>Without getting into too much detail, since I had spent a lot of time building custom functionality in Python already when I was building it in FastAPI (for example, converting from volumetric units to weight/mass units) I was able to get the bulk of the site up and running in less than a month in my spare time. There was a lot of minutiae that needed to be addressed, but thanks to <a target="_blank" href="https://render.com/">Render</a> getting the site and database deployed was very simple. It can be found at https://www.weightpoints.plus for anyone who wants to try it out.</p>
<h3 id="heading-progress">Progress</h3>
<p>I started actually tracking my weight on 17-March-2022 and I weighed in at 231.1 pounds. My weigh-in day is Thursday, so as of 4 days ago I was already down to 225.8 pounds. <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649691387582/KrHOCqzUP.png" alt="Screen Shot 2022-04-11 at 11.31.44 AM.png" />
It turns out when you get good cardio in every day <em>and</em> you watch what you eat it can have a decent effect on weight loss. I've been eating a lot more fruit and a lot less candy/cookies/chocolate. I would venture to say that maybe I'm eating too much fruit, but I have a sweet tooth and this helps.</p>
<p>I'll be moving my blog over to my own site at some point in the near future (and I'll be recording a video about setting up a blog in SvelteKit and a headless CMS) and I'm planning on blogging a bit more about my progress going forward. There will be content related to weight loss as well as my exercise routine, but discussion about that will be in a subsequent post.</p>
]]></content:encoded></item><item><title><![CDATA[Hitting Reset]]></title><description><![CDATA[So I had started this blog back in November (I actually joined in August) with the intent of building up to some sort of following/content creation. I wrote an article series and a couple of other random things and then everything sort of died down. ...]]></description><link>https://blog.jeffpohlmeyer.com/hitting-reset</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/hitting-reset</guid><category><![CDATA[personal]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Sat, 09 Apr 2022 17:27:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/fswQZLlHC3Y/upload/v1649525051948/77AtAKhfWu.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So I had started this blog back in November (I actually joined in August) with the intent of building up to some sort of following/content creation. I wrote an article series and a couple of other random things and then everything sort of died down. Well, there's a good reason for that: life happened.</p>
<p>You see, I have two kids, 8 and 5, and a full-time job. I have other responsibilities as well, but here's what my day looks like on a regular basis:</p>
<ul>
<li>6:45 AM: wake up and get the kids up for school</li>
<li>7:00 AM: make breakfast, coffee, get lunches ready, try to unload the dishwasher, get the kids ready for school (my wife does this last part)</li>
<li>7:45 AM: drive the kids to school<ul>
<li>Thankfully since January they've been going to the same school. What was a 1.25-hour commute is now down to ~45 minutes</li>
</ul>
</li>
<li>9:00 AM: get to work</li>
<li>11:30 AM: lunch</li>
<li>12:00 PM: stand-up</li>
<li>12:30 PM: back to work</li>
<li>4:30 PM: break for a bit to exercise</li>
<li>5:30 PM: kids are home, homework, start prepping dinner</li>
<li>6:30 PM - 8:30 PM: dinner (this fluctuates between these hours depending on how late exercise happened, spending time actually <em>talking</em> to my wife, etc.)</li>
<li>8:30 PM - 9:00 PM: get the kids ready for bed</li>
<li>9:30 PM: dishes, work for a bit longer</li>
<li>10:30 PM - 11:30 PM: shower and bed</li>
</ul>
<p>As a note, I tend to not have too many meetings on a daily basis, which is a change from my last job. There are some odd days when I do have to have ad hoc calls with team members, or where there's a grooming session or sprint planning/review, but those aren't every day.</p>
<p>The thing is, this is a normal day. By no means am I complaining because there are myriad things for which I'm very fortunate. The point of mentioning this, as I write this by "candlelight" in the wee hours of Saturday morning, is that I have <strong>no clue</strong> how some content creators have the time to create content to the extent that they have. I would <em>love</em> ❤️ to be able to blog, come up with witty tweets, create YouTube tutorials, and all of that other good stuff. But the thing is, what I really enjoy doing is building actual websites.</p>
<p>And therein lies the point of this post. I've managed to take some spare time, what little I have, to build a site and start recording some content for YouTube, and I'm planning on trying to do more in the coming weeks and months. I'll share a little more about the actual plans soon, because otherwise this would end up being a novel, but I have a few things that I'm going to be touching on in the very near future. Check back in the next few days for the first update.</p>
]]></content:encoded></item><item><title><![CDATA[Django Filters with Pagination]]></title><description><![CDATA[As I mentioned in my last post I tend to always have this desire to go back to Django, and I'm sort of acting on that now. I wrote a full API for the weight tracking app that I mentioned in that post in FastAPI only to realize that I didn't want to d...]]></description><link>https://blog.jeffpohlmeyer.com/django-filters-with-pagination</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/django-filters-with-pagination</guid><category><![CDATA[Django]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Sat, 26 Feb 2022 19:23:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/g0RtZc1IBtU/upload/v1645903403031/cseW2DNxF.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As I mentioned in <a target="_blank" href="https://jeffpohlmeyer.com/my-mind-is-a-runaway-train">my last post</a> I tend to always have this desire to go back to Django, and I'm sort of acting on that now. I wrote a full API for the weight tracking app that I mentioned in that post in FastAPI only to realize that I didn't want to deal with the baggage of a front end framework for something that should work very well in an MVC setup. Thankfully Django and FastAPI are both Python so a lot of the functionality ports over well.</p>
<p>That said, I <em>did</em> run into a weird little hiccup earlier today and I thought I'd share what I ended up doing. I have a view that lists out all foods that are either system foods or user-created foods, but as I have a fairly large database of foods to begin with, since I had kept this in a spreadsheet before and I scraped all of the restaurant foods from http://www.exercise4weightloss.com/weight-watchers-points.html, it amounted to a list of just shy of 50,000 foods so I clearly needed to add pagination.</p>
<h1 id="heading-pagination">Pagination</h1>
<p>This is easy to do in Django if you're using a <a target="_blank" href="https://docs.djangoproject.com/en/4.0/ref/class-based-views/generic-display/#listview">ListView</a> in that you only need to add one line to your class-based view</p>
<pre><code><span class="hljs-comment"># food/views.py</span>

<span class="hljs-keyword">from</span> django.db.models <span class="hljs-keyword">import</span> QuerySet, Q
<span class="hljs-keyword">from</span> django.views.generic <span class="hljs-keyword">import</span> ListView

<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Food

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FoodListView</span>(<span class="hljs-params">ListView</span>):</span>
    model = Food
    paginate_by = <span class="hljs-number">20</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_queryset</span>(<span class="hljs-params">self</span>) -&gt; QuerySet[Food]:</span>
        self.queryset = Food.objects.filter(
            Q(user_created=<span class="hljs-literal">False</span>) | Q(user=self.request.user)
        )
        <span class="hljs-keyword">return</span> super().get_queryset()
</code></pre><p>The <code>get_queryset</code> method is to set it to return all system foods and the foods created by the logged in user.</p>
<p>Then you simply need to update your template to include pagination as described in https://docs.djangoproject.com/en/4.0/topics/pagination/#paginating-a-listview</p>
<pre><code>{# food_list.html #}

{% extends 'base.html' %}

{% block content %}
  <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
    {% for food in food_list %}
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>{{ food }}<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    {% endfor %}
  <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>


  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"pagination"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"step-links"</span>&gt;</span>
        {% if page_obj.has_previous %}
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page=1"</span>&gt;</span><span class="hljs-symbol">&amp;laquo;</span> first<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.previous_page_number }}"</span>&gt;</span>previous<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        {% endif %}

      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"current"</span>&gt;</span>
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>

      {% if page_obj.has_next %}
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.next_page_number }}"</span>&gt;</span>next<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.paginator.num_pages }}"</span>&gt;</span>last <span class="hljs-symbol">&amp;raquo;</span><span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      {% endif %}
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
{% endblock content %}
</code></pre><h1 id="heading-filtering">Filtering</h1>
<p>If you want to include filtering, though, and you don't want to manually handle it, this can create a problem.</p>
<h2 id="heading-replacing-generic-listview">Replacing Generic ListView</h2>
<p>The first thing you would need to do is install <a target="_blank" href="https://django-filter.readthedocs.io/en/stable/index.html">django-filter</a>. Then after that you can simply create a filter and replace your <code>ListView</code> with a <code>FilterView</code></p>
<pre><code><span class="hljs-comment"># food/views.py</span>

<span class="hljs-keyword">import</span> django_filters
<span class="hljs-keyword">from</span> django.db.models <span class="hljs-keyword">import</span> QuerySet, Q
<span class="hljs-keyword">from</span> django_filters.views <span class="hljs-keyword">import</span> FilterView

<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Food

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FoodFilter</span>(<span class="hljs-params">django_filters.FilterSet</span>):</span>
    name = django_filters.CharFilter(lookup_expr=<span class="hljs-string">"icontains"</span>)

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meta</span>:</span>
        model = Food
        fields = [<span class="hljs-string">"name"</span>]

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FoodListView</span>(<span class="hljs-params">FilterView</span>):</span>
    model = Food
    paginate_by = <span class="hljs-number">20</span>
    filterset_class = FoodFilter
    template_name_suffix = <span class="hljs-string">"_list"</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_queryset</span>(<span class="hljs-params">self</span>) -&gt; QuerySet[Food]:</span>
        self.queryset = Food.objects.filter(
            Q(user_created=<span class="hljs-literal">False</span>) | Q(user=self.request.user)
        )
        <span class="hljs-keyword">return</span> super().get_queryset()
</code></pre><p>This, in theory, should return the same functionality as before, but now we can call <code>http://localhost:8000/foods/?name=chocolate</code> or anything like that and it will properly filter the queryset.</p>
<h2 id="heading-the-problem">The problem</h2>
<p>The issue that we run into, though, has to do with going to the next page. Currently, the next and previous page links on the template don't have any way of knowing what the current query is. So, while the first link will go to <code>http://localhost:8000/foods/?name=chocolate</code> but if I click on the "next page" link it will go to <code>http://localhost:8000/foods/?page=2</code> and it will forget about the filter that I applied.</p>
<h2 id="heading-the-solution">The solution</h2>
<p>The simplest way I found to do this, as of this moment, is two-fold.</p>
<h3 id="heading-update-the-context">Update the context</h3>
<p>Within the view itself we need to add a <code>get_context_data</code> method</p>
<pre><code>class FoodListView(FilterView):
    model <span class="hljs-operator">=</span> Food
    paginate_by <span class="hljs-operator">=</span> <span class="hljs-number">20</span>
    filterset_class <span class="hljs-operator">=</span> FoodFilter
    template_name_suffix <span class="hljs-operator">=</span> <span class="hljs-string">"_list"</span>

    def get_queryset(<span class="hljs-built_in">self</span>) <span class="hljs-operator">-</span><span class="hljs-operator">&gt;</span> QuerySet[Food]:
        <span class="hljs-built_in">self</span>.queryset <span class="hljs-operator">=</span> Food.objects.filter(
            Q(user_created<span class="hljs-operator">=</span>False) <span class="hljs-operator">|</span> Q(user<span class="hljs-operator">=</span><span class="hljs-built_in">self</span>.request.user)
        )
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">super</span>().get_queryset()

    def get_context_data(<span class="hljs-built_in">self</span>, <span class="hljs-operator">*</span><span class="hljs-operator">*</span>kwargs: Any) <span class="hljs-operator">-</span><span class="hljs-operator">&gt;</span> Dict[str, Any]:
        context <span class="hljs-operator">=</span> <span class="hljs-built_in">super</span>().get_context_data(<span class="hljs-operator">*</span><span class="hljs-operator">*</span>kwargs)
        context[<span class="hljs-string">"query"</span>] <span class="hljs-operator">=</span> dict()
        <span class="hljs-keyword">for</span> k, v in context[<span class="hljs-string">"filter"</span>].data.items():
            <span class="hljs-keyword">if</span> k <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-string">"page"</span>:
                context[<span class="hljs-string">"query"</span>][k] <span class="hljs-operator">=</span> v

        <span class="hljs-keyword">return</span> context
</code></pre><p>What this will do is it will check to see what the items are in the <code>QueryDict</code> that Django filter uses, and create a <code>query</code> object on the context that will include all existing query parameters but <strong>not</strong> including any that have <code>page</code> as the key because we want to let the built-in pagination handle that.</p>
<h3 id="heading-update-the-template">Update the template</h3>
<p>Then we just update the template to include all of these query objects on the <code>next</code> and <code>prev</code> anchor tags:</p>
<pre><code>{# food_list.html #}

{% extends 'base.html' %}

{% block content %}
  <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
    {% for food in food_list %}
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>{{ food }}<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    {% endfor %}
  <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>


  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"pagination"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"step-links"</span>&gt;</span>
      {% if page_obj.has_previous %}
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page=1{% for k, v in query.items %}&amp;{{ k }}={{ v }}{% endfor %}"</span>&gt;</span><span class="hljs-symbol">&amp;laquo;</span> first<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.previous_page_number }}{% for k, v in query.items %}&amp;{{ k }}={{ v }}{% endfor %}"</span>&gt;</span>previous<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      {% endif %}

      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"current"</span>&gt;</span>
        Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
      <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>

      {% if page_obj.has_next %}
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.next_page_number }}{% for k, v in query.items %}&amp;{{ k }}={{ v }}{% endfor %}"</span>&gt;</span>next<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"?page={{ page_obj.paginator.num_pages }}{% for k, v in query.items %}&amp;{{ k }}={{ v }}{% endfor %}"</span>&gt;</span>last <span class="hljs-symbol">&amp;raquo;</span><span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      {% endif %}
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
{% endblock content %}
</code></pre><p>What this will now do is append an <code>&amp;k=v</code> for each key/value pair that previously existed in the Django filter query. I'm sure I'm going to abstract this out into a snippet that will be more easily reusable down the road, but for now this was the easiest way to get it working quickly.</p>
]]></content:encoded></item><item><title><![CDATA[My mind is a runaway train]]></title><description><![CDATA[So I saw a tweet by the venerable swyx yesterday:
https://twitter.com/swyx/status/1494467065478258688?s=20
and it actually kind of resonated with me. You see, my "first love" in web development is Django. For as "antiquated" as it is when you compare...]]></description><link>https://blog.jeffpohlmeyer.com/my-mind-is-a-runaway-train</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/my-mind-is-a-runaway-train</guid><category><![CDATA[work]]></category><category><![CDATA[Django]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Fri, 18 Feb 2022 23:33:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/BY34glOW7wA/upload/v1645227161752/1QFAENdHx.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So I saw a tweet by the venerable <a class="user-mention" href="https://hashnode.com/@swyx">swyx</a> yesterday:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/swyx/status/1494467065478258688?s=20">https://twitter.com/swyx/status/1494467065478258688?s=20</a></div>
<p>and it actually kind of resonated with me. You see, my "first love" in web development is <a target="_blank" href="https://www.djangoproject.com/">Django</a>. For as "antiquated" as it is when you compare it to things like Node, Go, or even things like FastAPI, something about the whole setup just works. Sure, there are myriad frustrations when you want to venture outside of what Django's core functionality is, but if you need to build something and you don't care about the minutiae, Django is fantastic. It provides built-in auth, routing is easy enough to do, it gives easy access to the request object, among many other things. There are definitely problems with it, though, too like dealing with static files, where to put code if you need <em>any</em> custom configuration, and other things. That said, since I knew a little python when I started web development this seemed the obvious first choice.</p>
<p>This also seems to be the way my mind wants to go any time I think about a new project to build that is not a static site. For example, in my spare time (what little of it I have) I'm currently building an app that is a weight tracker that uses the old Points Plus formula from Weight Watchers. I used Weight Watchers nearly 10 years ago now and I lost about 80 pounds in ~8 months and I had gone into maintenance mode until they overnight changed everyone in their system to their new (at the time) <a target="_blank" href="https://www.weightwatchers.com/us/how-it-works/smartpoints">Smartpoints</a> system. The problem for me was that I was on a system that worked, and all of a sudden my breakfast that only cost me 7 points on the old system was now 11 points. If there's one thing that is helpful when it comes to Weight Watchers systems it's if you follow it then it can bring about a lifestyle shift which helps keep weight off long-term. I went from 275 in July, 2012 to 195 in February, 2013 and while I'm back up to nearly 230 now, I was generally able to do a decent job keeping it off.</p>
<p>This seems tailor made to use Django because each user would have their own experience requiring authentication, and there is a very straight-forward schema from a database perspective. The problem that I run into, though, is that whenever I'm building a Django site I have this weird need to always want to ship a site with as <em>little</em> JavaScript as is humanly possible. I have this weird thought in my mind that if I'm going to be shipping JavaScript I might as well use something that's built for client-side interactivity instead of trying to mimic it myself. In the weight app, as an example, if you're setting up a recipe and you're trying to add a food to the recipe that doesn't exist then how do you go and create a new food (on a different page) while saving the user's progress up to that point in the recipe? It's easy enough with JavaScript but <strong>HOW DO YOU DO IT WITHOUT JAVASCRIPT</strong>? I'm genuinely curious.</p>
<p>That said, whenever I'm learning about a SSR framework like Next/Nuxt/SvelteKit and the instructor (usually <a target="_blank" href="https://academind.com/">Max at Academind</a>) talks about rendering HTML on the server to be sent to the user I can't help but think "well why the hell am I even bothering with the frontend framework?" There's a very valid reason why I'm bothering with it, but that's just where my mind goes. My mind is going many places today.</p>
<p>That leads into the last point: as a developer who <em>desperately</em> wants to just build cool websites, there are many things I'm wrestling with that I want to build.</p>
<ul>
<li>I'm currently recording a FastAPI tutorial to be released on YouTube soon</li>
<li>I'm working on the weight tracking app (backend in FastAPI is done, working on the frontend)</li>
<li>I will be recording a video about setting up a blog in Wordpress vs one in Strapi/SvelteKit</li>
<li>I have ideas for a couple of other apps/sites to build</li>
</ul>
<p>This is all while having a full-time job and two young kids at home. Hopefully I can get some of this done before 2023 but who knows? I'm posting this for accountability: I <em>have</em> to get some of it done.</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 10]]></title><description><![CDATA[In the last article we set up the event emitter so
that when a symbol's data was fetched we could eventually display it in our chart components. The next step will be to
create said chart components. We'll want to create two types of charts:

A candl...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-10</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-10</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[charts]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Tue, 15 Feb 2022 19:38:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/ZzOa5G8hSPI/upload/v1644953863572/MG7HYXeuy.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-9">last article</a> we set up the event emitter so
that when a symbol's data was fetched we could eventually display it in our chart components. The next step will be to
create said chart components. We'll want to create two types of charts:</p>
<ul>
<li>A <a target="_blank" href="https://en.wikipedia.org/wiki/Candlestick_chart">candlestick chart</a></li>
<li>A <a target="_blank" href="https://www.incrediblecharts.com/indicators/volume.php">volume chart</a></li>
</ul>
<p>These can be split out into two separate components, but we first need to install the charting library we'll use for
this. There are many options one can choose from for charting but the one we're going to use here is a Vue version
of <a target="_blank" href="https://apexcharts.com/">ApexCharts</a>, called (creatively)
, <a target="_blank" href="https://www.npmjs.com/package/vue-apexcharts">vue-apexcharts</a>. To install this we simply run</p>
<pre><code class="lang-bash">npm i apexcharts vue3-apexcharts
</code></pre>
<p>and then we create two components to split out functionality.</p>
<h2 id="heading-jvpcandlestickvue">JVPCandlestick.vue</h2>
<p>The first chart will contain the candlestick functionality, which will be called <code>JVPCandlestick.vue</code>. ApexCharts has
a <a target="_blank" href="https://apexcharts.com/vue-chart-demos/candlestick-charts/basic/">built-in candlestick chart</a> which we'll use so the
component itself will be fairly simple. In the link you'll see the format the data needs to be, which we've already
addressed in a previous post.</p>
<h3 id="heading-component-setup">Component setup</h3>
<p>The <code>template</code> section of the component will be fairly straight-forward:</p>
<pre><code class="lang-html">
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">apexchart</span>
      <span class="hljs-attr">type</span>=<span class="hljs-string">"candlestick"</span>
      <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span>
      <span class="hljs-attr">height</span>=<span class="hljs-string">"80%"</span>
      <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span>
      <span class="hljs-attr">:options</span>=<span class="hljs-string">"chartOptions"</span>
  &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">apexchart</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>where the <code>series</code> and <code>chartOptions</code> values will be set in a little bit. Before we do anything else, though, we need to
update our <code>main.js</code> to globally declare the <code>apexchart</code> component.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// src/main.js</span>
<span class="hljs-keyword">import</span> {createApp} <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.vue'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./index.css'</span>;

<span class="hljs-keyword">import</span> VueApexCharts <span class="hljs-keyword">from</span> <span class="hljs-string">'vue3-apexcharts'</span>;

createApp(App).use(VueApexCharts).mount(<span class="hljs-string">'#app'</span>);
</code></pre>
<p>You can see above we've imported <code>VueApexCharts</code> and are using it in the app itself. Now we need to handle the <code>script</code>
section of the <code>JVPCandlestick</code> component.
We will need to include the <code>series</code> to display the data, as well as any other information we want including the symbol and interval, all of which need to be declared as props.
We then will set the chart title to be a computed property that displays nothing if we don't have any series data but will display the ticker and interval whenever we <em>do</em> have data.
Finally, we will set some chart options for displaying the title, the format of the x and y axes, as well as the tooltip.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {computed, defineComponent} <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineComponent({
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">symbol</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
    <span class="hljs-attr">interval</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
    <span class="hljs-attr">series</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Array</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  setup(props) {
    <span class="hljs-keyword">const</span> interval = computed(<span class="hljs-function">() =&gt;</span> props.interval);
    <span class="hljs-keyword">const</span> symbol = computed(<span class="hljs-function">() =&gt;</span> props.symbol);
    <span class="hljs-keyword">const</span> series = computed(<span class="hljs-function">() =&gt;</span> props.series);

    <span class="hljs-keyword">const</span> title = computed(<span class="hljs-function">() =&gt;</span>
      !series.value.length ? <span class="hljs-string">''</span> : <span class="hljs-string">`<span class="hljs-subst">${interval.value}</span> Chart of $<span class="hljs-subst">${symbol.value}</span>`</span>
    );

    <span class="hljs-keyword">const</span> chartOptions = computed(<span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">chart</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">'candlestick'</span>,
        <span class="hljs-attr">id</span>: <span class="hljs-string">'candles'</span>,
        <span class="hljs-attr">zoom</span>: {
          <span class="hljs-attr">enabled</span>: <span class="hljs-literal">false</span>,
        },
      },
      <span class="hljs-attr">title</span>: {
        <span class="hljs-attr">text</span>: title.value,
        <span class="hljs-attr">align</span>: <span class="hljs-string">'left'</span>,
        <span class="hljs-attr">floating</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">offsetY</span>: <span class="hljs-number">25</span>,
        <span class="hljs-attr">offsetX</span>: <span class="hljs-number">10</span>,
        <span class="hljs-attr">style</span>: {
          <span class="hljs-attr">fontSize</span>: <span class="hljs-string">'2rem'</span>,
        },
      },
      <span class="hljs-attr">xaxis</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">'datetime'</span>,
      },
      <span class="hljs-attr">yaxis</span>: {
        <span class="hljs-attr">tooltip</span>: {
          <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
        },
        <span class="hljs-attr">labels</span>: {
          <span class="hljs-attr">formatter</span>: <span class="hljs-function">(<span class="hljs-params">val</span>) =&gt;</span> val.toFixed(<span class="hljs-number">0</span>),
        },
      },
    }));

    <span class="hljs-keyword">return</span> { series, chartOptions };
  },
});
</code></pre>
<p>With this component now set up, we can include it in the <code>Chart.vue</code> component.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</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">JVPCandlestick</span> <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span> <span class="hljs-attr">:interval</span>=<span class="hljs-string">"interval"</span> <span class="hljs-attr">:symbol</span>=<span class="hljs-string">"symbol"</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">template</span>&gt;</span>
</code></pre>
<p>Now a simple search of any ticker should yield the following:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644953642346/8wdhpJRIW.png" alt="localhost_3000_.png" /></p>
<h4 id="heading-note"><em>Note</em></h4>
<p>There is a line in the <code>Selections.vue</code> component that provided some broken behavior.
Within the <code>postProcessData</code> method, the <code>state.endDate</code> value, if always fetching a daily chart, would constantly re-set the <code>endDate</code> value to be one day earlier.
A quick fix around this is to simply replace <code>state.endDate = formatDate(dates[dates.length - 1]);</code> with </p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (!state.endDate) {
  state.endDate = formatDate(dates[dates.length - <span class="hljs-number">1</span>]);
}
</code></pre>
<h2 id="heading-jvpvolumevue">JVPVolume.vue</h2>
<p>Next we need to set up the volume chart, which will go below the candlestick chart, and will be called <code>JVPVolume.vue</code>.
This is similarly simple, as we will just use a bar chart from <code>vue3-apexcharts</code>.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">apexchart</span> 
    <span class="hljs-attr">type</span>=<span class="hljs-string">"bar"</span>
    <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span>
    <span class="hljs-attr">height</span>=<span class="hljs-string">"20%"</span>
    <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span>
    <span class="hljs-attr">:options</span>=<span class="hljs-string">"chartOptions"</span>
  &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">apexchart</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>We then simply pull in the <code>volume</code> prop and set <code>series</code> as a computed property, and similarly set some chart options.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { computed, defineComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineComponent({
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">volume</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Array</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  setup(props) {
    <span class="hljs-keyword">const</span> series = computed(<span class="hljs-function">() =&gt;</span> props.volume);

    <span class="hljs-keyword">const</span> chartOptions = computed(<span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">type</span>: <span class="hljs-string">'bar'</span>,
      <span class="hljs-attr">brush</span>: {
        <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">target</span>: <span class="hljs-string">'candles'</span>,
      },
      <span class="hljs-attr">dataLabels</span>: {
        <span class="hljs-attr">enabled</span>: <span class="hljs-literal">false</span>,
      },
      <span class="hljs-attr">plotOptions</span>: {
        <span class="hljs-attr">bar</span>: {
          <span class="hljs-attr">columnWidth</span>: <span class="hljs-string">'80%'</span>,
        },
      },
      <span class="hljs-attr">stroke</span>: {
        <span class="hljs-attr">width</span>: <span class="hljs-number">0</span>,
      },
      <span class="hljs-attr">xaxis</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">'datetime'</span>,
        <span class="hljs-attr">axisBorder</span>: {
          <span class="hljs-attr">offsetX</span>: <span class="hljs-number">13</span>,
        },
        <span class="hljs-attr">categories</span>: props.volume[<span class="hljs-number">0</span>]?.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.x.getTime()) ?? [],
      },
    }));

    <span class="hljs-keyword">return</span> { series, chartOptions };
  },
});
</code></pre>
<p>We can then include the <code>JVPVolume.vue</code> component in <code>Chart.vue</code> as</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</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">JVPCandlestick</span> <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span> <span class="hljs-attr">:interval</span>=<span class="hljs-string">"interval"</span> <span class="hljs-attr">:symbol</span>=<span class="hljs-string">"symbol"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">JVPVolume</span> <span class="hljs-attr">:volume</span>=<span class="hljs-string">"volume"</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">template</span>&gt;</span>
</code></pre>
<p>and fetching the data for a ticker gives the following result
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644953661007/Bbksi6mGB.png" alt="localhost_3000_ (1).png" />
which, admittedly, looks fairly ugly.
To combat this, we'll add some styling to the <code>Chart.vue</code> component to make things look a bit nicer.</p>
<h2 id="heading-chartvue-styling"><code>Chart.vue</code> styling</h2>
<p>First, we will set the height on the wrapping div, as well as set some flex justification and alignment.</p>
<pre><code class="lang-html">
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mx-auto h-1/2 sm:h-4/6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">JVPCandlestick</span> <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span> <span class="hljs-attr">:interval</span>=<span class="hljs-string">"interval"</span> <span class="hljs-attr">:symbol</span>=<span class="hljs-string">"symbol"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">JVPVolume</span> <span class="hljs-attr">:volume</span>=<span class="hljs-string">"volume"</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">template</span>&gt;</span>
</code></pre>
<p>Setting this makes the resulting chart look much better
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644953673621/6q-bx2f28.png" alt="localhost_3000_ (2).png" />
The problem, though, is that when the page first loads and there is no data the page looks somewhat ugly.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644953684621/ZEbP-TTRI8.png" alt="localhost_3000_ (3).png" /></p>
<p>Thus, let's add some dynamic content that will render one thing if we have data, and will render something else if we do not.
First, we wrap the charts in a <code>div</code> with an <code>if</code> statement, and otherwise display a message.
We will then set some basic styling on these two elements.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"mx-auto h-1/2 sm:h-4/6 lg:h-5/6"</span>
    <span class="hljs-attr">:class</span>=<span class="hljs-string">"series.length ? '' : 'flex items-center justify-center'"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"series.length"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">JVPCandlestick</span> <span class="hljs-attr">:series</span>=<span class="hljs-string">"series"</span> <span class="hljs-attr">:interval</span>=<span class="hljs-string">"interval"</span> <span class="hljs-attr">:symbol</span>=<span class="hljs-string">"symbol"</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">JVPVolume</span> <span class="hljs-attr">:volume</span>=<span class="hljs-string">"volume"</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">v-else</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"chart"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-4xl flex items-center justify-center"</span>&gt;</span>
      Choose a company, interval, and dates below
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>The <code>:class</code> attribute in the wrapping div allows us to set styles dynamically.
It has the same functionality as the general <code>class</code> attribute above it, but this allows us to bind the class to something programmatically.
With this set-up, the initial page, prior to loading data, looks like this
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644953775144/95StetmGS.png" alt="localhost_3000_ (4).png" /></p>
<h2 id="heading-summary">Summary</h2>
<p>With that, we have a simple app that uses FastAPI on the server to fetch data and Vue with Vuelidate and ApexCharts to display data on the frontend.
The entire repo can be found at https://github.com/jvp-design/stock-chart-fastapi-vue/</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 9]]></title><description><![CDATA[In the last article we handled errors in the response from our FastAPI backend.
We also added in a bit of extra code to process the data, and the postProcessData method actually created data and volData elements, though we didn't do anything with it....]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-9</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-9</guid><category><![CDATA[Vue.js]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Wed, 05 Jan 2022 22:56:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/LJ9KY8pIH3E/upload/v1641423332770/dTbzsl5EF.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-8">last article</a> we handled errors in the response from our FastAPI backend.
We also added in a bit of extra code to process the data, and the <code>postProcessData</code> method actually created <code>data</code> and <code>volData</code> elements, though we didn't do anything with it.
In this article we'll touch on emitting an event back to the parent component and passing data down to another component to use for charting purposes.</p>
<h2 id="heading-passing-data-to-appvue">Passing data to <code>App.vue</code></h2>
<h3 id="heading-finishing-off-processing-data">Finishing off processing data</h3>
<p>We left off with <code>data</code> and <code>volData</code> elements defined in <code>Selections.vue</code>, and we need to somehow pass them up into <code>App.vue</code> and then down into another component that we'll create.
First, we need to actually return data from our <code>postProcessData</code> method.
We <em>could</em> just emit the event directly from that method but it's better to keep each method focused on its own purpose.
Thus, the updated method will look like</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> postProcessData = <span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
  <span class="hljs-comment">// Parse dates</span>
  <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(d).toISOString().substr(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
  <span class="hljs-keyword">const</span> dates = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.date).sort();
  state.startDate = formatDate(dates[<span class="hljs-number">0</span>]);
  state.endDate = formatDate(dates[dates.length - <span class="hljs-number">1</span>]);

  <span class="hljs-comment">// Format the data</span>
  <span class="hljs-keyword">const</span> data = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
    <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
    <span class="hljs-attr">y</span>: e.data,
  }));
  <span class="hljs-keyword">const</span> volData = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
    <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
    <span class="hljs-attr">y</span>: e.volume ?? <span class="hljs-number">0</span>,
  }));

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">series</span>: [{data}],
    <span class="hljs-attr">symbol</span>: state.symbol,
    <span class="hljs-attr">interval</span>: state.interval,
    <span class="hljs-attr">volume</span>: [{<span class="hljs-attr">name</span>: <span class="hljs-string">'volume'</span>, <span class="hljs-attr">data</span>: volData}]
  }
};
</code></pre>
<p>We are returning the data in this manner because we have already set four data elements in <code>App.vue</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> series = ref([]);
<span class="hljs-keyword">const</span> symbol = ref(<span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> interval = ref(<span class="hljs-string">'Daily'</span>);
<span class="hljs-keyword">const</span> volume = ref([])
</code></pre>
<p>Thus we will want to set these values for use in the eventual chart component, so formatting the emitted data in this manner will make things easier in general.
The <code>series</code> and <code>volume</code> data are formatted the way they are because the charting platform we'll be using is ApexCharts and the data structure needs to be in the format set above.</p>
<p>Then we need to update <code>handleSubmit</code> to get this data and emit it to the <code>App.vue</code> component.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> () =&gt; {
    v$.value.$validate();
    <span class="hljs-keyword">if</span> (!v$.value.$invalid) {
        loading.value = <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> url = preProcessData();
            <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetchData(url);
            <span class="hljs-keyword">const</span> payload = postProcessData(res);

            emit(<span class="hljs-string">'setData'</span>, payload);
        } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err);
            dialog.value = <span class="hljs-literal">true</span>;
        }
        loading.value = <span class="hljs-literal">false</span>;
    }
};
</code></pre>
<h3 id="heading-emitting-the-event">Emitting the event</h3>
<p>In order to do this, we need to update our code in two other places.
First, we need to update the <code>setup</code> method in <code>Selections.vue</code> to include the <code>emit</code> handler</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineComponent({
  <span class="hljs-attr">components</span>: { JVPDialog, JVPInput, JVPSelect },
  <span class="hljs-attr">emits</span>: [<span class="hljs-string">'setData'</span>],
  setup(_, { emit }) {
</code></pre>
<p>The <code>_</code> in the <code>setup</code> method is because we're not actually using props anywhere in this component.
You can pass in the <code>props</code> argument but it won't do anything.</p>
<p>We then also need to listen for the event in <code>App.vue</code>.
This will involve adding the event listener onto the <code>Selections</code> element in the <code>template</code>, but also adding a method to handle this data in the <code>setup</code> function.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="h-screen bg-gray-200"&gt;
    &lt;Selections class="px-4" @setData="setData" /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, ref } from 'vue';
import Selections from './components/Selections.vue';

export default defineComponent({
  components: { Selections },
  setup() {
    const series = ref([]);
    const symbol = ref('');
    const interval = ref('Daily');
    const volume = ref([]);

    const setData = (payload) =&gt; {
      console.log('in setData');
      console.log(payload);
    };

    return { series, symbol, interval, volume, setData };
  },
});
&lt;/script&gt;
</code></pre>
<p>Upon making these adjustments and submitting a request for data for $MSFT the resulting console looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641423136896/pYzKMsC0e.png" alt="Screen Shot 2022-01-05 at 5.24.24 PM.png" /></p>
<h2 id="heading-charting-the-data">Charting the data</h2>
<h3 id="heading-organizing-data-in-appvue">Organizing data in <code>App.vue</code></h3>
<p>Now that we have the data in <code>App.vue</code> we will need to store it instead of just logging it to the console.
This is fairly straightforward</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> setData = <span class="hljs-function">(<span class="hljs-params">payload</span>) =&gt;</span> {
  series.value = payload.series
  symbol.value = payload.symbol
  interval.value = payload.interval
  volume.value = payload.volume
}
</code></pre>
<p>This seems fairly verbose, though not debilitatingly so.</p>
<h3 id="heading-creating-a-chartvue-component">Creating a Chart.vue component</h3>
<p>We now need a component to hold this data and eventually render the chart.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;div&gt;
      {{ symbol }}
    &lt;/div&gt;
    &lt;div&gt;
      {{ interval }}
    &lt;/div&gt;
    &lt;div&gt;
      {{ series }}
    &lt;/div&gt;
    &lt;div&gt;
      {{ volume }}
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    symbol: {
      type: String,
      required: true,
    },
    interval: {
      type: String,
      required: true,
    },
    series: {
      type: Array,
      required: true,
    },
    volume: {
      type: Array,
      required: true,
    },
  },
  setup(props) {
    return {
      ...props,
    };
  },
});
&lt;/script&gt;
</code></pre>
<p>We then update <code>App.vue</code> to include this component</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="h-screen bg-gray-200"&gt;
    &lt;Chart
      :series="series"
      :symbol="symbol"
      :interval="interval"
      :volume="volume"
    /&gt;
    &lt;Selections class="px-4" @setData="setData" /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, ref, reactive } from 'vue';
import Selections from './components/Selections.vue';
import Chart from './components/Chart.vue';

export default defineComponent({
  components: { Selections, Chart },
  setup() {
    const series = ref([]);
    const symbol = ref('');
    const interval = ref('Daily');
    const volume = ref([]);

    const setData = (payload) =&gt; {
      series.value = payload.series
      symbol.value = payload.symbol
      interval.value = payload.interval
      volume.value = payload.volume
      console.log('data fetched and set')
    };

    return { series, symbol, interval, volume, setData };
  },
});
&lt;/script&gt;
</code></pre>
<p>And we click the button to fetch the data and we get the following
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641423176395/pWaayCSC8.png" alt="Screen Shot 2022-01-05 at 5.41.24 PM.png" />
which doesn't make any sense since the data has clearly been set in <code>App.vue</code>.
What's actually happening is</p>
<ul>
<li>The <code>Chart.vue</code> component is mounted with data that exists in <code>App.vue</code> at first</li>
<li>Data is fetched from <code>Selections.vue</code> and passed up to <code>App.vue</code></li>
<li>Data is then passed down to <code>Chart.vue</code></li>
<li><strong>Nothing is telling <code>Chart.vue</code> to check to see if props changed to trigger a re-render!!!</strong></li>
</ul>
<h3 id="heading-watching-for-data-changes">Watching for data changes</h3>
<p>There are a few ways that we can make sure that <code>Chart.vue</code> will watch the props and re-paint the DOM when data are updated.
The most straight-forward way is to make all four elements computed properties in <code>Chart.vue</code>.
We will need to import <code>computed</code> at the top of the <code>script</code> tag, and then do the following</p>
<pre><code class="lang-javascript">setup(props) {
  <span class="hljs-keyword">const</span> symbol = computed(<span class="hljs-function">() =&gt;</span> props.symbol.toUpperCase());
  <span class="hljs-keyword">const</span> interval = computed(<span class="hljs-function">() =&gt;</span> props.interval);
  <span class="hljs-keyword">const</span> series = computed(<span class="hljs-function">() =&gt;</span> props.series);
  <span class="hljs-keyword">const</span> volume = computed(<span class="hljs-function">() =&gt;</span> props.volume);
  <span class="hljs-keyword">return</span> {
    symbol,
    interval,
    series,
    volume,
  };
},
</code></pre>
<p><a target="_blank" href="https://v3.vuejs.org/guide/computed.html">Computed properties</a> are like reactive elements in a Vue component.
Any time the data inside the anonymous function for the computed property is updated then the resulting element will also update.
We could utilize <a target="_blank" href="https://v3.vuejs.org/guide/computed.html#watchers">watchers</a> but again, this is a fairly straightforward solution for what we need.
Upon doing this and fetching the data again we see
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641423199873/MXRF6ds7m.png" alt="Screen Shot 2022-01-05 at 5.49.02 PM.png" /></p>
<p>and we have the updated data as anticipated.</p>
<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-10">next article</a> we'll install and include <a target="_blank" href="https://www.npmjs.com/package/vue3-apexcharts">Vue3 ApexCharts</a> to render out the stock chart as well as the volume chart.</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 8]]></title><description><![CDATA[As mentioned in the last article, we have functionality that enables us to fetch data from the FastAPI server we previously set up, but as of this moment we have nothing that will catch an error.
The easiest way to demonstrate the problem with this i...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-8</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-8</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Thu, 23 Dec 2021 22:00:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/heNwUmEtZzo/upload/v1640296795400/QNVXrTK_Y.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As mentioned in the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-7">last article</a>, we have functionality that enables us to fetch data from the FastAPI server we previously set up, but as of this moment we have nothing that will catch an error.
The easiest way to demonstrate the problem with this is to first disable our client-side validation; the plan is to purposefully submit a request to the backend without an interval to see what happens when a bad request is sent.</p>
<p>To do this we simply update the section in <code>Selections.vue</code> that converts the interval into what can be parsed by the backend to look like</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> interval = <span class="hljs-literal">null</span>
  <span class="hljs-comment">// state.interval === 'Daily'</span>
  <span class="hljs-comment">//   ? '1d'</span>
  <span class="hljs-comment">//   : state.interval === 'Weekly'</span>
  <span class="hljs-comment">//     ? '1wk'</span>
  <span class="hljs-comment">//     : '1mo';</span>
</code></pre>
<p>If we do this and then try to submit a request to the backend with, for example, <code>MSFT</code> as the symbol, this is what the page will look like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640293517999/sGaZsILOe.png" alt="Screen Shot 2021-12-23 at 2.13.49 PM.png" />
If we didn't have the dev tools open (as the vast majority of users don't) then we would have no idea what is going on as it would look like the request never completes.
Thus, we need to add some sort of exception handling in this method.</p>
<h2 id="heading-trycatch">Try/Catch</h2>
<p>There are <em>generally</em> two ways to handle error handling in this project</p>
<ul>
<li><code>.then().catch().finally()</code> methodology</li>
<li><code>async/await</code> methodology</li>
</ul>
<p>Since we're already using <code>async/await</code> in the current method we'll discuss that here.
First, we don't need to wrap the entire <code>handleSubmit</code> method in a try/catch block.
We're processing data in the first 40% of the method, and also in the last 40% of the method.
The only place that we really <em>need</em> to have the try/catch block is around the async function call.
That said, it will make things a bit more organized if we can separate out some of the functionality.
We will split the bulk of the code in <code>handleSubmit</code> into three separate methods: <code>preProcessData</code>, <code>fetchData</code>, <code>postProcessData</code>.
This is unnecessary and may be overkill for smaller personal projects, but it will make it easier to keep track of everything.</p>
<h3 id="heading-sub-methods">Sub-methods</h3>
<p>The first is <code>preProcessData</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> preProcessData = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// Create the querystring</span>
    <span class="hljs-keyword">const</span> query = {}
    <span class="hljs-keyword">if</span> (!!state.startDate) query.start = state.startDate;
    <span class="hljs-keyword">if</span> (!!state.endDate) query.end = state.endDate;

    <span class="hljs-keyword">const</span> queryString = <span class="hljs-built_in">Object</span>.keys(query)
        .filter(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> !!query[key])
        .map(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> (
                <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(key)}</span>=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(query[key])}</span>`</span>
            );
        })
        .join(<span class="hljs-string">'&amp;'</span>)

    <span class="hljs-comment">// Convert human-readable interval into yfinance style</span>
    <span class="hljs-keyword">const</span> interval = <span class="hljs-literal">null</span>
    <span class="hljs-comment">// state.interval === 'Daily'</span>
    <span class="hljs-comment">//   ? '1d'</span>
    <span class="hljs-comment">//   : state.interval === 'Weekly'</span>
    <span class="hljs-comment">//     ? '1wk'</span>
    <span class="hljs-comment">//     : '1mo';</span>

    <span class="hljs-comment">// Create URL string and add query if it exists</span>
    <span class="hljs-keyword">let</span> url = <span class="hljs-string">`http://localhost:8000/quote/<span class="hljs-subst">${state.symbol}</span>/<span class="hljs-subst">${interval}</span>`</span>;
    <span class="hljs-keyword">if</span> (queryString.length) url = <span class="hljs-string">`<span class="hljs-subst">${url}</span>?<span class="hljs-subst">${queryString}</span>`</span>;
    <span class="hljs-keyword">return</span> url
}
</code></pre>
<p>We will get the <code>url</code> from this method and pass it into <code>fetchData</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> (url) =&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(url);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> response.json();
}
</code></pre>
<p>Which will, in turn, return the <code>json</code> data that we get from the backend and pass it to <code>postProcessData</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> postProcessData = <span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
  <span class="hljs-comment">// Parse dates</span>
  <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(d).toISOString().substr(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
  <span class="hljs-keyword">const</span> dates = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.date).sort();
  state.startDate = formatDate(dates[<span class="hljs-number">0</span>])
  state.endDate = formatDate(dates[dates.length - <span class="hljs-number">1</span>])

  <span class="hljs-comment">// Format the data</span>
  <span class="hljs-keyword">const</span> data = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
    <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
    <span class="hljs-attr">y</span>: e.data
  }));
  <span class="hljs-keyword">const</span> volData = res.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
    <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
    <span class="hljs-attr">y</span>: e.volume ?? <span class="hljs-number">0</span>
  }));
}
</code></pre>
<h3 id="heading-handlesubmit-refactor"><code>handleSubmit</code> refactor</h3>
<p>This allows us to refactor <code>handleSubmit</code> to look like this</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> () =&gt; {
  v$.value.$validate();
  <span class="hljs-keyword">if</span> (!v$.value.$invalid) {
    loading.value = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> url = preProcessData();
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetchData(url);
      postProcessData(res);
    } <span class="hljs-keyword">catch</span>(err) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err)
    }
    loading.value = <span class="hljs-literal">false</span>;
  }
};
</code></pre>
<p>We will now see that if we try clicking the button to get the chart that the loading spinner stops and we'll see a more descriptive error:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640293559188/lQulGGJbl.png" alt="Screen Shot 2021-12-23 at 3.50.05 PM.png" />
The problem that we'll run into in this situation, though, has to do with the loading spinner.
Again, we run into the problem where if we didn't have the dev tools open then we would have received no feedback about this error.
We need to somehow let the user know that something went wrong.</p>
<h2 id="heading-error-dialog">Error Dialog</h2>
<p>The first thing we're going to do is create a separate component and import it into our <code>Selections.vue</code> component.
This component will use the <a target="_blank" href="https://headlessui.dev/vue/dialog">Dialog (Modal)</a> component from Headless UI.
The new component, called <code>JVPDialog.vue</code>, the bulk of which (not including the open/close states) comes from the <a target="_blank" href="https://tailwindui.com/components/application-ui/overlays/modals">modal</a> section of <a target="_blank" href="https://tailwindui.com/">TailwindUI</a>.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;TransitionRoot as="template" :show="open"&gt;
    &lt;Dialog
        as="div"
        class="fixed z-10 inset-0 overflow-y-auto"
        @close="closeDialog"
    &gt;
      &lt;div
          class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
      &gt;
        &lt;TransitionChild
            as="template"
            enter="ease-out duration-300"
            enter-from="opacity-0"
            enter-to="opacity-100"
            leave="ease-in duration-200"
            leave-from="opacity-100"
            leave-to="opacity-0"
        &gt;
          &lt;DialogOverlay
              class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
          /&gt;
        &lt;/TransitionChild&gt;

        &lt;!-- This element is to trick the browser into centering the modal contents. --&gt;
        &lt;span
            class="hidden sm:inline-block sm:align-middle sm:h-screen"
            aria-hidden="true"
        &gt;
          &amp;#8203;
        &lt;/span&gt;
        &lt;TransitionChild
            as="template"
            enter="ease-out duration-300"
            enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
            enter-to="opacity-100 translate-y-0 sm:scale-100"
            leave="ease-in duration-200"
            leave-from="opacity-100 translate-y-0 sm:scale-100"
            leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
        &gt;
          &lt;div
              class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
          &gt;
            &lt;div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"&gt;
              &lt;div class="sm:flex sm:items-start"&gt;
                &lt;div
                    class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
                &gt;
                  &lt;ExclamationIcon
                      class="h-6 w-6 text-red-600"
                      aria-hidden="true"
                  /&gt;
                &lt;/div&gt;
                &lt;div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"&gt;
                  &lt;DialogTitle
                      as="h3"
                      class="text-lg leading-6 font-medium text-gray-900"
                  &gt;
                    Not found
                  &lt;/DialogTitle&gt;
                  &lt;div class="mt-2"&gt;
                    &lt;p class="text-sm text-gray-500"&gt;
                      There was an error fetching the data to populate that
                      chart. Please try again
                    &lt;/p&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div
                class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
            &gt;
              &lt;button
                  type="button"
                  class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
                  @click="closeDialog"
              &gt;
                Close
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/TransitionChild&gt;
      &lt;/div&gt;
    &lt;/Dialog&gt;
  &lt;/TransitionRoot&gt;
&lt;/template&gt;

&lt;script&gt;
import { computed } from 'vue';
import {
  Dialog,
  DialogOverlay,
  DialogTitle,
  TransitionChild,
  TransitionRoot,
} from '@headlessui/vue';
import { ExclamationIcon } from '@heroicons/vue/outline';

export default {
  components: {
    Dialog,
    DialogOverlay,
    DialogTitle,
    TransitionChild,
    TransitionRoot,
    ExclamationIcon,
  },
  props: {
    dialog: {
      type: Boolean,
      required: true,
    },
  },
  emits: ['closeDialog'],
  setup(props, { emit }) {
    const open = computed(() =&gt; props.dialog);
    const closeDialog = () =&gt; emit('closeDialog');

    return {
      open,
      closeDialog,
    };
  },
};
&lt;/script&gt;
</code></pre>
<p>We then add it to the beginning of <code>Selections.vue</code> like</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</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">JVPDialog</span> <span class="hljs-attr">:dialog</span>=<span class="hljs-string">"dialog"</span> @<span class="hljs-attr">closeDialog</span>=<span class="hljs-string">"closeDialog"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-5"</span> @<span class="hljs-attr">submit.prevent</span>=<span class="hljs-string">"handleSubmit"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">JVPInput</span>
          <span class="hljs-attr">v-model</span>=<span class="hljs-string">"state.symbol"</span>
          <span class="hljs-attr">label</span>=<span class="hljs-string">"Symbol"</span></span>
</code></pre>
<p>where we've imported it and registered it as necessary, and added the following lines to the <code>script</code> section of the component</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> dialog = ref(<span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> closeDialog = <span class="hljs-function">() =&gt;</span> (dialog.value = <span class="hljs-literal">false</span>)
</code></pre>
<p>and also included these in the return statement of the <code>setup</code> function</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">return</span> {
  intervals,
  state,
  disabled,
  loading,
  dialog,
  v$,
  handleSubmit,
  closeDialog,
};
</code></pre>
<p>For what it's worth, I know there is a lot going on in the <code>JVPDialog.vue</code> file and it would be worth it to discuss much of the functionality in depth, but I've never been good at CSS and I have found that paying for an account at TailwindUI has been more than worth it.
You can play around with some of the classes in that component and see how things change and update.
Also, it's worth noting that the modal I've used above is available as the free option on the main page, in case Adam Wathan ever reads this article.
Now, all we need to do is add in a simple line in the <code>catch</code> block of <code>handleSubmit</code> that activates the dialog when an error is received.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> () =&gt; {
    v$.value.$validate();
    <span class="hljs-keyword">if</span> (!v$.value.$invalid) {
        loading.value = <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> url = preProcessData();
            <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetchData(url);
            postProcessData(res);
        } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err);
            dialog.value = <span class="hljs-literal">true</span>;
        }
        loading.value = <span class="hljs-literal">false</span>;
    }
};
</code></pre>
<p>and the resulting dialog that we see is displayed below.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640296594780/FNaWGGN-G.png" alt="Screen Shot 2021-12-23 at 4.51.15 PM.png" />
Before signing off, we need to make sure that we re-set the interval to be an actual valid value instead of the null value we set for these testing purposes.</p>
<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-9">next article</a> we'll discuss handling the actual returned data and passing it to another component for rendering the chart.</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 7]]></title><description><![CDATA[The next step in our journey is to hook up our frontend to the backend.
In the last article we made things look a bit better and we set up some logic so that a loading spinner appears when we click on the submit button.
Now we need to use the handleS...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-7</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-7</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Python]]></category><category><![CDATA[fetch]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Wed, 15 Dec 2021 23:46:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/u9GEK0AuOU8/upload/v1639611894880/yf1rR0LwP.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The next step in our journey is to hook up our frontend to the backend.
In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-6">last article</a> we made things look a bit better and we set up some logic so that a loading spinner appears when we click on the <code>submit</code> button.
Now we need to use the <code>handleSubmit</code> method to actually fetch data from our server.</p>
<h2 id="heading-spin-up-the-server">Spin up the server</h2>
<p>In case we don't recall from <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-1">Part 1</a> of this series, our server is set up in a Python environment using <code>FastAPI</code>.
If our directory structure is like this</p>
<pre><code>.
├── fast<span class="hljs-operator">-</span>api<span class="hljs-operator">-</span>vue<span class="hljs-operator">-</span>stock
│   ├── client
│   └── server
│       └── main.py
</code></pre><p>we can open a terminal/command prompt and type the following</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /path/to/server
env\Scripts\activate
uvicorn main:app --reload
</code></pre>
<p>This assumes that we already have the same setup as was described in Part 1 of the tutorial.
By typing this then our server should be running at port 8000.</p>
<h2 id="heading-connecting-to-the-server">Connecting to the server</h2>
<h3 id="heading-create-a-querystring">Create a querystring</h3>
<p>For those who may not recall, the format of the url that we will be consuming in the backend is of the format <code>/quote/{ticker}/{interval}</code>.
Then, any date information, as it is optional, is passed in via query parameters in the format <code>YYYY-mm-dd</code>.
So, within the <code>handleSubmit</code> method in <code>Selections.vue</code> we first need to create a query object and add the start and end dates if they exist.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> query = {}
<span class="hljs-keyword">if</span> (!!state.startDate) query.start = state.startDate;
<span class="hljs-keyword">if</span> (!!state.endDate) query.end = state.endDate;
</code></pre>
<p>Then, we can just map through the keys of the query and use the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent">encodeURIComponent</a> built-in method to convert our dates into the correct format.
It should be noted that since we're only doing this with two values the method that will be presented below may be a little overkill, but the idea is that this method can be used for any number of query parameters so it's worth it to present.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> queryString = <span class="hljs-built_in">Object</span>.keys(query)
  .filter(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> !!query[key])
  .map(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> (
      <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(key)}</span>=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(query[key])}</span>`</span>
    );
  })
  .join(<span class="hljs-string">'&amp;'</span>)
</code></pre>
<p>To see what this method does, let's use the following object as an example</p>
<pre><code class="lang-javascript">query = {
  <span class="hljs-string">"hello"</span>: <span class="hljs-string">"world"</span>,
  <span class="hljs-string">"hashnode"</span>: [<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>],
  <span class="hljs-string">"vue"</span>: {
    <span class="hljs-string">"is"</span>: <span class="hljs-string">"cool"</span>
  },
  <span class="hljs-string">"fastapi"</span>: <span class="hljs-string">"is as well"</span>
}
</code></pre>
<p>If we then look at the resulting string, we get
<code>'hello=world&amp;hashnode=1%2C2%2C3&amp;vue=%5Bobject%20Object%5D&amp;fastapi=is%20as%20well'</code>
and we notice that there is an <code>object%20Object</code> where the value for the <code>vue</code> key should be.
This shows us that we can't just use this method for any deeply nested structures, but it instead works for simple key-value stores.
If we instead set </p>
<pre><code class="lang-javascript">query = {
  <span class="hljs-string">"hello"</span>: <span class="hljs-string">"world"</span>,
  <span class="hljs-string">"hashnode"</span>: [<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>],
  <span class="hljs-string">"vue"</span>: [<span class="hljs-string">"is"</span>, <span class="hljs-string">"cool"</span>],
  <span class="hljs-string">"fastapi"</span>: <span class="hljs-string">"is as well"</span>
}
</code></pre>
<p>then the resulting string would look like <code>'hello=world&amp;hashnode=1%2C2%2C3&amp;vue=is%2Ccool&amp;fastapi=is%20as%20well'</code> which would work much better.
There is an argument that you shouldn't need to do something like this when you're passing in the data yourself, but I've found that it can't hurt to just be extra careful with these sorts of things.
Now we should have a valid query string for our <code>start</code> and <code>end</code> dates, if they exist.</p>
<h3 id="heading-convert-human-readable-interval-into-yfinance-style">Convert human-readable interval into yfinance style</h3>
<p>The next step is to convert the interval into the style that <code>yfinance</code> needs.
For human readability, we're presenting our intervals as "Daily", "Weekly", and "Monthly", but these won't work in <code>yfinance</code> and we should actually get errors if we try to pass these values in because we've set the allowable input values to be one of <code>1d</code>, <code>1wk</code>, or <code>1mo</code>.
So, this is as simple as just using a nested JavaScript <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator">ternary operator</a>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> interval = 
  state.interval === <span class="hljs-string">'Daily'</span>
    ? <span class="hljs-string">'1d'</span>
    : state.interval === <span class="hljs-string">'Weekly'</span>
    ? <span class="hljs-string">'1wk'</span>
    : <span class="hljs-string">'1mo'</span>;
</code></pre>
<p>This sets the <code>interval</code> constant to <code>1d</code> if the state's interval value is <code>Daily</code>, otherwise if the state's interval value is <code>Weekly</code> it sets it to <code>1wk</code>, otherwise it sets it to <code>1mo</code>.
The equivalent code in <code>if</code> statements would be</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> interval;

<span class="hljs-keyword">if</span> (state.interval === <span class="hljs-string">'Daily'</span>) {
  interval = <span class="hljs-string">'1d'</span>
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (state.interval === <span class="hljs-string">'Weekly'</span>) {
  interval = <span class="hljs-string">'1wk'</span>
} <span class="hljs-keyword">else</span> {
  interval = <span class="hljs-string">'1mo'</span>
}
</code></pre>
<h3 id="heading-create-the-url-string-and-add-the-query-if-it-exists">Create the URL string and add the query if it exists</h3>
<p>We can now create the URL that will be used to fetch the data from our server.
We first set up the basic url with the <code>symbol</code> and <code>interval</code> values</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> url = <span class="hljs-string">`http://localhost:8000/quote/<span class="hljs-subst">${state.symbol}</span>/<span class="hljs-subst">${interval}</span>`</span>;
</code></pre>
<p>and we then need to append any query string, if it exists.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (queryString.length) url = <span class="hljs-string">`<span class="hljs-subst">${url}</span>?<span class="hljs-subst">${queryString}</span>`</span>;
</code></pre>
<p>We do this because we need to put a <code>?</code> at the beginning of the query string.
The next step is to fetch the data from the server using the built-in <code>fetch</code> API.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(url);
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> response.json();
</code></pre>
<h3 id="heading-parse-the-dates-and-format-data">Parse the dates and format data</h3>
<h4 id="heading-parse-dates">Parse dates</h4>
<p>The next step is to update the <code>state</code>'s <code>startDate</code> and <code>endDate</code> with the values obtained from the server.
This is done because one or both of the dates input by the user may have been weekends or holidays, or any other myriad possibilities.
We assume that the data we've received from the server contains valid dates, so the simplest way to do this is to just set <code>state.startDate</code> to the earliest date, and <code>state.endDate</code> to the latest date.
First, we want to create an anonymous function that will format the dates into the format we're currently using on the site.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(d).toISOString().substr(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
</code></pre>
<p>and then we sort the dates, and set each one</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> dates = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.date).sort();
state.startDate = formatDate(dates[<span class="hljs-number">0</span>])
state.endDate = formatDate(dates[dates.length - <span class="hljs-number">1</span>])
</code></pre>
<h4 id="heading-format-the-data-into-our-desired-output">Format the data into our desired output</h4>
<p>This last step is going to come slightly out of left field; we are going to format the data in a format that will eventually be used by our charting platform.
We're going to have the <code>x</code> coordinates be the dates and the <code>y</code> coordinates be the data, whether volume or stock prices.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> data = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
  <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
  <span class="hljs-attr">y</span>: e.data
}));
<span class="hljs-keyword">const</span> volData = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
  <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
  <span class="hljs-attr">y</span>: e.volume ?? <span class="hljs-number">0</span>
}));
</code></pre>
<p>The entire code of the <code>handleSubmit</code> method should now look like this</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> () =&gt; {
  v$.value.$validate();
  <span class="hljs-keyword">if</span> (!v$.value.$invalid) {
    loading.value = <span class="hljs-literal">true</span>;

    <span class="hljs-comment">// Create the querystring</span>
    <span class="hljs-keyword">const</span> query = {}
    <span class="hljs-keyword">if</span> (!!state.startDate) query.start = state.startDate;
    <span class="hljs-keyword">if</span> (!!state.endDate) query.end = state.endDate;

    <span class="hljs-keyword">const</span> queryString = <span class="hljs-built_in">Object</span>.keys(query)
      .filter(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> !!query[key])
      .map(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(key)}</span>=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(query[key])}</span>`</span>
        );
      })
      .join(<span class="hljs-string">'&amp;'</span>)

    <span class="hljs-comment">// Convert human-readable interval into yfinance style</span>
    <span class="hljs-keyword">const</span> interval =
      state.interval === <span class="hljs-string">'Daily'</span>
        ? <span class="hljs-string">'1d'</span>
        : state.interval === <span class="hljs-string">'Weekly'</span>
          ? <span class="hljs-string">'1wk'</span>
          : <span class="hljs-string">'1mo'</span>;

    <span class="hljs-comment">// Create URL string and add query if it exists</span>
    <span class="hljs-keyword">let</span> url = <span class="hljs-string">`http://localhost:8000/quote/<span class="hljs-subst">${state.symbol}</span>/<span class="hljs-subst">${interval}</span>`</span>;
    <span class="hljs-keyword">if</span> (queryString.length) url = <span class="hljs-string">`<span class="hljs-subst">${url}</span>?<span class="hljs-subst">${queryString}</span>`</span>;
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(url);
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> response.json();

    <span class="hljs-comment">// Parse dates</span>
    <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">d</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(d).toISOString().substr(<span class="hljs-number">0</span>, <span class="hljs-number">10</span>);
    <span class="hljs-keyword">const</span> dates = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.date).sort();
    state.startDate = formatDate(dates[<span class="hljs-number">0</span>])
    state.endDate = formatDate(dates[dates.length - <span class="hljs-number">1</span>])

    <span class="hljs-comment">// Format the data</span>
    <span class="hljs-keyword">const</span> data = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
      <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
      <span class="hljs-attr">y</span>: e.data
    }));
    <span class="hljs-keyword">const</span> volData = res.data.map(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> ({
      <span class="hljs-attr">x</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.date),
      <span class="hljs-attr">y</span>: e.volume ?? <span class="hljs-number">0</span>
    }));

    loading.value = <span class="hljs-literal">false</span>;
  }
};
</code></pre>
<p>You'll notice that we removed the setTimeout and added an entry at the bottom to set the <code>loading</code> spinner to false.
Also, we needed to make the function itself <code>async</code> in order to utilize the <code>await</code> keyword.</p>
<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-8">next article</a> we'll discuss error handling, because having this type of functionality not wrapped in <code>try</code> and <code>catch</code> blocks is a recipe for disaster.
We'll also add in a dialog to alert the user when an error has taken place.</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 6]]></title><description><![CDATA[In the last article we styled the form a bit better and added conditional styling based on the data validation from Vuelidate.
The button still looks pretty ugly, though, so we'll style that in this article as well as add in any necessary functionali...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-6</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-6</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Sat, 11 Dec 2021 16:17:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/W5qgKZj-qnk/upload/v1638915981992/XjgDYme00.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-5">last article</a> we styled the form a bit better and added conditional styling based on the data validation from Vuelidate.
The button still looks pretty ugly, though, so we'll style that in this article as well as add in any necessary functionality.
We'll also add in some icon functionality to make it easy to include them wherever we need, as well.</p>
<h2 id="heading-styling-the-button">Styling the button</h2>
<p>We initially added in "placeholder" classes so the button actually looked like something.
We'll remove the <code>border-4</code> and <code>border-indigo-500</code> classes and replace them with the following:</p>
<ul>
<li><code>w-full</code> -&gt; take up the full width of the parent container</li>
<li><code>h-12</code> -&gt; fixing height, this will be beneficial for a loading icon later</li>
<li><code>px-6</code> -&gt; some horizontal padding</li>
<li><code>mt-6</code> -&gt; margin on the top to clear it from the <code>JVPSelect.vue</code> component</li>
<li><code>lg:mt-0</code> -&gt; removing said margin when on <code>lg</code> or <code>xl</code> <a target="_blank" href="https://tailwindcss.com/docs/breakpoints">breakpoints</a>, as declared by Tailwind</li>
<li><code>rounded-lg</code> -&gt; just rounding the corners a bit to look nicer</li>
<li><code>bg-indigo-700</code> -&gt; background coloring</li>
<li><code>text-indigo-100</code> -&gt; a similar text shade to the background, but with enough contrast</li>
<li><code>transition-colors</code> -&gt; a transition class that will only transition the color attributes that will be updated</li>
<li><code>duration-150</code> -&gt; setting a duration for the aforementioned transitions</li>
<li><code>hover:bg-indigo-800</code> -&gt; a slightly darker color on hover (this is where the <code>transition-colors</code> will have an affect)</li>
<li><code>focus:shadow-outline</code> -&gt; a subtle outline when the button is focused, as in when tabbing through inputs</li>
<li><code>disabled:opacity-50</code> -&gt; setting the opacity to be 50% if the button is disabled</li>
<li><code>disabled:cursor-not-allowed</code> -&gt; disable the standard pointer cursor for a button when the button is disabled</li>
</ul>
<h3 id="heading-button-logic">Button logic</h3>
<p>In the previous article we didn't include any disabled functionality on the button because we wanted to include good feedback for error handling.
Now we can include disabled functionality, which we started in the previous article where we set up a computed property:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> disabled = computed(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> v$.value.$invalid;
});
</code></pre>
<p>All we need to do now is add a dynamic <code>:disabled</code> property to the button in the template.
Now the button markup looks like this</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"
    w-full
    h-12
    px-6
    mt-6
    lg:mt-0
    rounded-lg
    bg-indigo-700
    text-indigo-100
    transition-colors
    duration-150
    hover:bg-indigo-800
    focus:shadow-outline
    disabled:opacity-50
    disabled:cursor-not-allowed
  "</span>
  <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"disabled"</span>
&gt;</span>
  Get Chart
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>and the form, with the disabled button, looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638916608645/_LwWVG35b.png" alt="Screenshot hashnode button.png" /></p>
<h2 id="heading-icons">Icons</h2>
<p>We hold off on handling logic on a valid form submission until the next article because we first want to include display of icons.
We will use this functionality to add a loading spinner to the button while awaiting a response so it makes sense to handle this now.
We can use icons from libraries like <a target="_blank" href="https://fontawesome.com/">FontAwesome</a>, <a target="_blank" href="https://materialdesignicons.com/">Material Design Icons</a>, <a target="_blank" href="https://fonts.google.com/icons">Google Font Icons</a>, but since we're using Vue, and Tailwind CSS integrates very well with React and Vue (TailwindUI was built for those two frameworks), we can simply install <a target="_blank" href="https://heroicons.com/">heroicons</a>.
We open a terminal/command prompt and type</p>
<pre><code class="lang-bash">npm i -D @heroicons/vue
</code></pre>
<p>and we should have a set of free icons created by <a target="_blank" href="Steve Schoger">https://twitter.com/steveschoger</a>, who is a partner/designer at Tailwind Labs.
Now that we've installed the icons, we can import the icons we need.</p>
<h3 id="heading-exclamation">Exclamation</h3>
<p>We want to add in a little warning icon in the <code>JVPInput.vue</code> component any time data validation fails.
In order to do this, we just need to import the component and register it</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { defineComponent, computed } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> { ExclamationCircleIcon } <span class="hljs-keyword">from</span> <span class="hljs-string">'@heroicons/vue/solid'</span>;
<span class="hljs-keyword">import</span> inputProps <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils/input-props'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineComponent({
  <span class="hljs-attr">components</span>: { ExclamationCircleIcon },
  <span class="hljs-attr">props</span>: {
  ...
</code></pre>
<p>Then we can very simply add it, conditionally on error state, to the markup inside the <code>&lt;div class="mt-1 relative..."</code> but after the actual <code>input</code> tag</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
  <span class="hljs-attr">v-if</span>=<span class="hljs-string">"isError"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute inset-y-0 right-0 flex items-center pointer-events-none"</span>
  <span class="hljs-attr">:class</span>=<span class="hljs-string">"type === 'date' ? 'pr-9' : 'pr-3'"</span>
&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">ExclamationCircleIcon</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"h-5 w-5 text-red-500"</span>
    <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>
  /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>It is here that we see the importance of including the <code>relative</code> class on the wrapping div, because we want to absolutely position this icon inside of the input.
We also conditionally render the right-padding based on whether the input type is a date, because if it is a date it shows the calendar icon and we don't want to cover that.
Now an invalid input will include the exclamation icon as needed.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638916625661/60oS4pr_4.png" alt="Screenshot hashnode button icon.png" /></p>
<h3 id="heading-loading-spinner">Loading spinner</h3>
<p>This is not <em>technically</em> an icon, but we'll add it in the same section because it behaves similarly.
The first thing we need to do is add in an element called <code>loading</code> in the script itself.
It will be a simple boolean that will be false on initialization.
We then need to conditionally render either the loading spinner, if <code>loading === true</code>, or the previously set text, "Get Chart" if not.
Thus, the generic slot in the <code>&lt;button&gt;</code> element is replaced by</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>
  <span class="hljs-attr">v-if</span>=<span class="hljs-string">"loading"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"
    animate-spin-1.5
    ease-linear
    rounded-full
    border-4 border-t-4 border-gray-200
    h-10
    w-10
    mx-auto
    block
  "</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">v-else</span>&gt;</span>Get Chart<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
</code></pre>
<p>The <code>animate-spin-1.5</code> class is a custom class that we'll define in a minute, but the rest are all default Tailwind classes.</p>
<ul>
<li><code>ease-linear</code> -&gt; the linear transition-timing-function</li>
<li><code>rounded-full</code> -&gt; makes the element a circle</li>
<li><code>border-4 border-t-4 border-gray-200</code> -&gt; border styling around this empty element; this will be what is actually displayed</li>
<li><code>h-10 w-10</code> -&gt; forcing a specific height and width</li>
<li><code>mx-auto</code> -&gt; centering the element in its container</li>
<li><code>block</code> -&gt; making the element a block-level (span is an inline element)</li>
</ul>
<p>We then need to set up the custom <code>loader</code> class.
This is a simple addition of a <code>&lt;style&gt;</code> tag at the bottom of the component</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="css">
<span class="hljs-selector-class">.loader</span> {
  <span class="hljs-attribute">border-top-color</span>: <span class="hljs-number">#6366f1</span>;
  <span class="hljs-attribute">-webkit-animation</span>: spinner <span class="hljs-number">1.5s</span> linear infinite;
  <span class="hljs-attribute">animation</span>: spinner <span class="hljs-number">1.5s</span> linear infinite;
}

<span class="hljs-keyword">@-webkit-keyframes</span> spinner {
  0% {
    <span class="hljs-attribute">-webkit-transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">0deg</span>);
  }
  100% {
    <span class="hljs-attribute">-webkit-transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">360deg</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> spinner {
  0% {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">0deg</span>);
  }
  100% {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">360deg</span>);
  }
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>We are setting a separate color for the top border, which will look like it's the only thing rotating.
Now to test this we can update <code>handleSubmit</code> with to look like</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-function">() =&gt;</span> {
  v$.value.$validate();
  <span class="hljs-keyword">if</span> (!v$.value.$invalid) {
    loading.value = <span class="hljs-literal">true</span>;
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'triggered handleSubmit'</span>);
      loading.value = <span class="hljs-literal">false</span>
    }, <span class="hljs-number">5000</span>)
  }
}
</code></pre>
<p>We should now have a loader that is running for 5 seconds, and the console still logs "triggered handleSubmit" on a valid form submission.
In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-7">next article</a> we'll handle form submission properly and fetch the data from the FastAPI server that we previously set up.</p>
<h2 id="heading-code">Code</h2>
<p>As a reference, the updated <code>Selections.vue</code> and <code>JVPInput.vue</code> components should look like</p>
<p><code>Selections.vue</code></p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;form class="ml-5" @submit.prevent="handleSubmit"&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.symbol"
        label="Symbol"
        id="symbol"
        name="symbol"
        type="text"
        placeholder="eg. MSFT"
        :vuelidate="v$.symbol"
        hint="Required, less than 6 characters"
        :autofocus="true"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.startDate"
        label="Start Date"
        id="start-date"
        name="startDate"
        type="date"
        :vuelidate="v$.startDate"
        hint="Optional"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.endDate"
        label="End Date"
        id="end-date"
        name="endDate"
        type="date"
        :vuelidate="v$.endDate"
        hint="Optional"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPSelect
        v-model="state.interval"
        label="Interval"
        id="interval"
        :options="intervals"
      /&gt;
    &lt;/div&gt;
    &lt;button
      type="submit"
      class="w-full h-12 px-6 mt-6 lg:mt-0 rounded-lg bg-indigo-700 text-indigo-100 transition-colors duration-150 hover:bg-indigo-800 focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed"
      :disabled="disabled"
    &gt;
      &lt;span
        v-if="loading"
        class="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-10 w-10 mx-auto block"
      &gt;&lt;/span&gt;
      &lt;span v-else&gt;Get Chart&lt;/span&gt;
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, reactive, ref, computed } from 'vue';
import useVuelidate from '@vuelidate/core';
import { required, maxLength, helpers } from '@vuelidate/validators';
import JVPInput from './JVPInput.vue';
import JVPSelect from './JVPSelect.vue';

export default defineComponent({
  components: { JVPInput, JVPSelect },
  setup() {
    const intervals = ['Daily', 'Weekly', 'Monthly'];

    const state = reactive({
      symbol: '',
      interval: 'Daily',
      startDate: '',
      endDate: '',
    });

    const mustBeEarlierDate = helpers.withMessage(
      'Start date must be earlier than end date.',
      (value) =&gt; {
        const endDate = computed(() =&gt; state.endDate);
        if (!value || !endDate.value) return true;
        if (!checkDateFormat(endDate.value)) return true;
        return new Date(value) &lt; new Date(endDate.value);
      }
    );

    const checkDateFormat = (param) =&gt; {
      if (!param) return true;
      return new Date(param).toString() !== 'Invalid Date';
    };

    const validateDateFormat = helpers.withMessage(
      'Please enter a valid date.',
      checkDateFormat
    );

    const rules = {
      symbol: { required, maxLength: maxLength(5) },
      startDate: {
        validateDateFormat,
        mustBeEarlierDate,
      },
      endDate: {
        validateDateFormat,
      },
    };
    const v$ = useVuelidate(rules, state);

    const disabled = computed(() =&gt; {
      return v$.value.$invalid;
    });

    const loading = ref(false);

    const handleSubmit = () =&gt; {
      v$.value.$validate();
      if (!v$.value.$invalid) {
        loading.value = true;
        setTimeout(() =&gt; {
          console.log('triggered handleSubmit');
          loading.value = false;
        }, 5000);
      }
    };

    return {
      intervals,
      state,
      disabled,
      loading,
      v$,
      handleSubmit,
    };
  },
});
&lt;/script&gt;

&lt;style scoped&gt;
.loader {
  border-top-color: #6366f1;
  -webkit-animation: spinner 1.5s linear infinite;
  animation: spinner 1.5s linear infinite;
}

@-webkit-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
  }
}

@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
&lt;/style&gt;
</code></pre>
<p><code>JVPInput.vue</code></p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id" class="block text-sm font-medium text-gray-700"&gt;
      {{ label }}
    &lt;/label&gt;
    &lt;div class="mt-1 relative rounded-md shadow-sm"&gt;
      &lt;input
        :value="modelValue"
        :type="type"
        :name="id"
        :id="id"
        class="block w-full sm:text-sm rounded-md shadow-sm"
        :class="dynamicClass"
        :aria-invalid="isError"
        :aria-describedby="hintId"
        :autofocus="autofocus"
        @input="updateValue"
        @blur="vuelidate.$touch"
      /&gt;
      &lt;div
        v-if="isError"
        class="absolute inset-y-0 right-0 flex items-center pointer-events-none"
        :class="type === 'date' ? 'pr-9' : 'pr-3'"
      &gt;
        &lt;ExclamationCircleIcon
          class="h-5 w-5 text-red-500"
          aria-hidden="true"
        /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;p v-if="hasHint" class="mt-0 text-sm" :class="hintClass" :id="hintId"&gt;
      {{ hintText }}
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, computed } from 'vue';
import { ExclamationCircleIcon } from '@heroicons/vue/solid';
import inputProps from '../utils/input-props';

export default defineComponent({
  components: { ExclamationCircleIcon },
  props: {
    type: {
      type: String,
      required: false,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: false,
    },
    vuelidate: {
      type: Object,
      required: false,
      default: () =&gt; ({}),
    },
    autofocus: {
      type: Boolean,
      required: false,
      default: false,
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // This is simply cleaner than putting emit code in the HTML
    const updateValue = (e) =&gt; emit('update:modelValue', e.target.value);

    // Vuelidate is used as a validation library.
    // We use built-in functionality to determine if any rules are violated
    // as well as display of the associated error text
    const isError = computed(() =&gt; props.vuelidate.$error);
    const errorText = computed(() =&gt; {
      const messages =
        props.vuelidate.$errors?.map((err) =&gt; err.$message) ?? [];
      return messages.join(' ');
    });

    // This is solely to style the input based on whether it is in an error state or not
    const dynamicClass = computed(() =&gt; {
      // This is to remove padding on the right for the built-in calendar icon when using a date type
      let val = props.type === 'date' ? '' : 'pr-10';
      return isError.value
        ? `${val} border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500`
        : `${val} focus:ring-indigo-500 focus:border-indigo-500 border-gray-300`;
    });

    // The "hint" text will display any necessary hints as well as any error messages.
    // Styling and values of said hint text are primarily based on whether an input is in an error state.
    const hintClass = computed(() =&gt;
      isError.value ? 'text-red-600' : 'text-gray-500'
    );
    const hintId = computed(() =&gt;
      isError.value ? `${props.id}-error` : `${props.id}-input`
    );

    const hintText = computed(() =&gt; {
      if (errorText.value.length &gt; 0) return errorText.value;
      if (!!props.hint) return props.hint;
      return '';
    });

    const hasHint = computed(() =&gt; !!hintText.value.length);

    return {
      hasHint,
      hintText,
      hintClass,
      hintId,
      dynamicClass,
      isError,
      autofocus: props.autofocus,
      vuelidate: props.vuelidate,
      updateValue,
    };
  },
});
&lt;/script&gt;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 5]]></title><description><![CDATA[In the last article we added validation to the JVPInput.vue components using a library called Vuelidate.
Upon an invalid entry to any of these components, we had a line of text appear below the input to indicate that it was invalid.
Now that we have ...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-5</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-5</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[styling]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Tue, 07 Dec 2021 22:28:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1638503101915/yRyw9PqU7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-4">last article</a> we added validation to the <code>JVPInput.vue</code> components using a library called Vuelidate.
Upon an invalid entry to any of these components, we had a line of text appear below the input to indicate that it was invalid.
Now that we have a place to put error text, we should make use of that space in case we want a hint to go there.  </p>
<p>We have already included the <code>hint</code> prop in our <code>input-props.js</code> file, so we don't need to add anything there.
Now all we need to do to pass the hint to the <code>JVPInput</code> component is add it anywhere we want it displayed.
For example, we want to tell the user that the <code>symbol</code> attribute is required and the dates are optional, so we would pass these in as hints.</p>
<pre><code class="lang-vue">&lt;div class="my-1"&gt;
  &lt;JVPInput
    v-model="state.symbol"
    label="Symbol"
    id="symbol"
    name="symbol"
    type="text"
    placeholder="eg. MSFT"
    :vuelidate="v$.symbol"
    hint="Required, less than 6 characters"
  /&gt;
&lt;/div&gt;
&lt;div class="my-1"&gt;
  &lt;JVPInput
    v-model="state.startDate"
    label="Start Date"
    id="start-date"
    name="startDate"
    type="text"
    :vuelidate="v$.startDate"
    hint="Optional"
  /&gt;
&lt;/div&gt;
&lt;div class="my-1"&gt;
  &lt;JVPInput
    v-model="state.endDate"
    label="End Date"
    id="end-date"
    name="endDate"
    type="text"
    :vuelidate="v$.endDate"
    hint="Optional"
  /&gt;
&lt;/div&gt;
</code></pre>
<h3 id="heading-hinterror-logic">Hint/error logic</h3>
<p>The next thing we need to do is incorporate the hint vs. error logic within the component script itself.
We will be using the same space to display hints and errors, but we don't want to display a hint when an input is in an error state.
We also want the text displayed to be dynamic depending on the state of the input.
To that end, we'll add another computed property to <code>JVPInput.vue</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> hintText = computed(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (errorText.value.length &gt; <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> errorText.value;
  <span class="hljs-keyword">if</span> (!!props.hint) <span class="hljs-keyword">return</span> props.hint;
  <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>;
})
</code></pre>
<p>What this little property does is it displays any error messages, if they exist.
If they don't, but a hint was passed down as a prop, then display the hint, otherwise just remain empty.
We can then add</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> hasHint = computed(<span class="hljs-function">() =&gt;</span> !!hintText.value.length);
</code></pre>
<p>which will be truthy whether there is a hint <em>or</em> there are any error messages.
Now the error text markup will be replaced with</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"hasHint"</span>&gt;</span>{{ hintText }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>after having included both <code>hasHint</code> and <code>hintText</code> in the <code>return</code> section of the <code>setup</code> function.</p>
<h3 id="heading-styling">Styling</h3>
<p>Now we need to add some styling.
The app still looks pretty horrendous so we need to clean it up a bit.
The first thing we notice, though, is that there seems to be no highlighting around the <code>JVPSelect.vue</code> element.
If we think back a bit, we included <code>focus:ring-indigo-500</code> and <code>focus:border-indigo-500</code> as classes on this element but as we tab through we don't see any of those.
The reason for this is because we need to install</p>
<pre><code class="lang-bash">npm i @tailwindcss/forms
</code></pre>
<p>and include an extra plugin for Tailwind</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// tailwind.config.js</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">mode</span>: <span class="hljs-string">'jit'</span>,
  <span class="hljs-attr">purge</span>: [<span class="hljs-string">'./index.html'</span>, <span class="hljs-string">'./src/**/*.{vue,js,ts,jsx,tsx}'</span>],
  <span class="hljs-attr">darkMode</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// or 'media' or 'class'</span>
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {},
  },
  <span class="hljs-attr">variants</span>: {
    <span class="hljs-attr">extend</span>: {},
  },
  <span class="hljs-attr">plugins</span>: [<span class="hljs-built_in">require</span>(<span class="hljs-string">'@tailwindcss/forms'</span>)], <span class="hljs-comment">// &lt;-- *** New line ***</span>
}
</code></pre>
<p>By simply adding this plugin the form looks a bit better and we can see that in the focus state the <code>JVPSelect.vue</code> component has the correct highlighting.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638502851725/EtZT1QrVC.png" alt="Screenshot 2021-12-02 221120.png" />
What we want to do next for the <code>JVPInput.vue</code> component is create a sort of dynamic class that will display certain properties when in a valid state and other properties in an error state.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// This is solely to style the input based on whether it is in an error state or not</span>
<span class="hljs-keyword">const</span> dynamicClass = computed(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// This is to remove padding on the right for the built-in calendar icon when using a date type</span>
  <span class="hljs-keyword">let</span> val = props.type === <span class="hljs-string">'date'</span> ? <span class="hljs-string">''</span> : <span class="hljs-string">'pr-10'</span>;
  <span class="hljs-keyword">return</span> isError.value
    ? <span class="hljs-string">`<span class="hljs-subst">${val}</span> border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500`</span>
    : <span class="hljs-string">`<span class="hljs-subst">${val}</span> focus:ring-indigo-500 focus:border-indigo-500 border-gray-300`</span>;
});
</code></pre>
<p>We will then update the class of the hint slightly and dynamically set the <code>id</code> value for the hint</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// The "hint" text will display any necessary hints as well as any error messages.</span>
<span class="hljs-comment">// Styling and values of said hint text are primarily based on whether an input is in an error state.</span>
<span class="hljs-keyword">const</span> hintClass = computed(<span class="hljs-function">() =&gt;</span>
  isError.value ? <span class="hljs-string">'text-red-600'</span> : <span class="hljs-string">'text-gray-500'</span>
);
<span class="hljs-keyword">const</span> hintId = computed(<span class="hljs-function">() =&gt;</span>
  isError.value ? <span class="hljs-string">`<span class="hljs-subst">${props.id}</span>-error`</span> : <span class="hljs-string">`<span class="hljs-subst">${props.id}</span>-input`</span>
);
</code></pre>
<p>We then pass the <code>dynamicClass</code> variable into the <code>return</code> statement of the <code>setup</code> function and add an extra line to the <code>input</code> tag</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
  <span class="hljs-attr">:type</span>=<span class="hljs-string">"type"</span>
  <span class="hljs-attr">:name</span>=<span class="hljs-string">"id"</span>
  <span class="hljs-attr">:id</span>=<span class="hljs-string">"id"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"block w-full sm:text-sm rounded-md shadow-sm"</span>
  <span class="hljs-attr">:class</span>=<span class="hljs-string">"dynamicClass"</span>
  @<span class="hljs-attr">input</span>=<span class="hljs-string">"updateValue"</span>
/&gt;</span>
</code></pre>
<p>What this will do is always set the class to be <code>block w-full sm:text-sm rounded-md shadow-sm</code> regardless of the error state, but then it also adds on the extra elements defined in <code>dynamicClass</code>, which are dependent on the error state of the <code>input</code>.  </p>
<p>We then want to add a bit of extra styling to the hint text itself just to give it a secondary focus.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"hasHint"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-0 text-sm"</span> <span class="hljs-attr">:class</span>=<span class="hljs-string">"hintClass"</span> <span class="hljs-attr">:id</span>=<span class="hljs-string">"hintId"</span>&gt;</span>
  {{ hintText }}
<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<p>after having added the <code>hintClass</code> and <code>hintId</code> elements to the <code>return</code> statement of the <code>setup</code> function, of course.  </p>
<h3 id="heading-finishing-touches-on-jvpinputvue">Finishing touches on <code>JVPInput.vue</code></h3>
<p>To tie off the loose ends on the <code>JVPInput.vue</code> component there are just a few more things we want to add</p>
<ul>
<li>A couple of <code>aria</code> attributes</li>
<li>Tell <code>vuelidate</code> to validate each input on a <code>blur</code> event</li>
<li>Add autofocus as a prop and bind it to the input</li>
</ul>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
  <span class="hljs-attr">:type</span>=<span class="hljs-string">"type"</span>
  <span class="hljs-attr">:name</span>=<span class="hljs-string">"id"</span>
  <span class="hljs-attr">:id</span>=<span class="hljs-string">"id"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"block w-full sm:text-sm rounded-md shadow-sm"</span>
  <span class="hljs-attr">:class</span>=<span class="hljs-string">"dynamicClass"</span>
  <span class="hljs-attr">:aria-invalid</span>=<span class="hljs-string">"isError"</span>
  <span class="hljs-attr">:aria-describedby</span>=<span class="hljs-string">"hintId"</span>
  <span class="hljs-attr">:autofocus</span>=<span class="hljs-string">"autofocus"</span>
  @<span class="hljs-attr">input</span>=<span class="hljs-string">"updateValue"</span>
  @<span class="hljs-attr">blur</span>=<span class="hljs-string">"vuelidate.$touch"</span>
/&gt;</span>
</code></pre>
<p>You can see in the code snippet above we're accessing <code>vuelidate</code> and <code>autofocus</code> directly instead of via the <code>props</code>.
In order to do this we simply need to update the <code>return</code> statement like so</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">return</span> {
  hasHint,
  hintText,
  hintClass,
  hintId,
  dynamicClass,
  isError,
  <span class="hljs-attr">autofocus</span>: props.autofocus,
  <span class="hljs-attr">vuelidate</span>: props.vuelidate,
  updateValue,
};
</code></pre>
<p>To tie a final bow on this component, we will actually update two things in <code>Selections.vue</code>.
You'll notice when creating the <code>dynamicClass</code> variable we allowed for a class of <code>pr-10</code> if the <code>type</code> prop passed in is "date".
This will automatically add in the calendar icon, so in <code>Selections.vue</code> we'll change the type of <code>startDate</code> and <code>endDate</code> to be "date".</p>
<h2 id="heading-summary">Summary</h2>
<p>Now that all of that is done, we've <em>nearly</em> finished with the <code>form</code> components.
What we will do in the next, short, article will be adding in svg icons to the project so that we can easily include them in our code wherever we need.
We will also style the button because the rest of the form is starting to look nice and clean and the button is still pretty ugly.
For the purpose of copy-pasting, the two components we updated in this section are <code>Selections.vue</code> and <code>JVPInput.vue</code>, the updated versions of both of which are presented now.</p>
<h4 id="heading-selectionsvue"><code>Selections.vue</code></h4>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;form class="ml-5" @submit.prevent="handleSubmit"&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.symbol"
        label="Symbol"
        id="symbol"
        name="symbol"
        type="text"
        placeholder="eg. MSFT"
        :vuelidate="v$.symbol"
        hint="Required, less than 6 characters"
        :autofocus="true"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.startDate"
        label="Start Date"
        id="start-date"
        name="startDate"
        type="date"
        :vuelidate="v$.startDate"
        hint="Optional"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.endDate"
        label="End Date"
        id="end-date"
        name="endDate"
        type="date"
        :vuelidate="v$.endDate"
        hint="Optional"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPSelect
        v-model="state.interval"
        label="Interval"
        id="interval"
        :options="intervals"
      /&gt;
    &lt;/div&gt;
    &lt;button type="submit" class="border-4 border-indigo-500"&gt;Get Chart&lt;/button&gt;
  &lt;/form&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, reactive, computed } from 'vue';
import useVuelidate from '@vuelidate/core';
import { required, maxLength, helpers } from '@vuelidate/validators';
import JVPInput from './JVPInput.vue';
import JVPSelect from './JVPSelect.vue';

export default defineComponent({
  components: { JVPInput, JVPSelect },
  setup() {
    const intervals = ['Daily', 'Weekly', 'Monthly'];

    const state = reactive({
      symbol: '',
      interval: 'Daily',
      startDate: '',
      endDate: '',
    });

    const mustBeEarlierDate = helpers.withMessage(
      'Start date must be earlier than end date.',
      (value) =&gt; {
        const endDate = computed(() =&gt; state.endDate);
        if (!value || !endDate.value) return true;
        if (!checkDateFormat(endDate.value)) return true;
        return new Date(value) &lt; new Date(endDate.value);
      }
    );

    const checkDateFormat = (param) =&gt; {
      if (!param) return true;
      return new Date(param).toString() !== 'Invalid Date';
    };

    const validateDateFormat = helpers.withMessage(
      'Please enter a valid date.',
      checkDateFormat
    );

    const rules = {
      symbol: { required, maxLength: maxLength(5) },
      startDate: {
        validateDateFormat,
        mustBeEarlierDate,
      },
      endDate: {
        validateDateFormat,
      },
    };
    const v$ = useVuelidate(rules, state);

    const disabled = computed(() =&gt; {
      return v$.value.$invalid;
    });

    const handleSubmit = () =&gt; {
      v$.value.$validate();
      if (!v$.value.$invalid) {
        console.log('triggered handleSubmit');
      }
    };

    return {
      intervals,
      state,
      disabled,
      v$,
      handleSubmit,
    };
  },
});
&lt;/script&gt;
</code></pre>
<h4 id="heading-jvpinputvue"><code>JVPInput.vue</code></h4>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id" class="block text-sm font-medium text-gray-700"&gt;
      {{ label }}
    &lt;/label&gt;
    &lt;div class="mt-1 relative rounded-md shadow-sm"&gt;
      &lt;input
        :value="modelValue"
        :type="type"
        :name="id"
        :id="id"
        class="block w-full sm:text-sm rounded-md shadow-sm"
        :class="dynamicClass"
        :aria-invalid="isError"
        :aria-describedby="hintId"
        :autofocus="autofocus"
        @input="updateValue"
        @blur="vuelidate.$touch"
      /&gt;
    &lt;/div&gt;
    &lt;p v-if="hasHint" class="mt-0 text-sm" :class="hintClass" :id="hintId"&gt;
      {{ hintText }}
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    type: {
      type: String,
      required: false,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: false,
    },
    vuelidate: {
      type: Object,
      required: false,
      default: () =&gt; ({}),
    },
    autofocus: {
      type: Boolean,
      required: false,
      default: false
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // This is simply cleaner than putting emit code in the HTML
    const updateValue = (e) =&gt; emit('update:modelValue', e.target.value);

    // Vuelidate is used as a validation library.
    // We use built-in functionality to determine if any rules are violated
    // as well as display of the associated error text
    const isError = computed(() =&gt; props.vuelidate.$error);
    const errorText = computed(() =&gt; {
      const messages =
        props.vuelidate.$errors?.map((err) =&gt; err.$message) ?? [];
      return messages.join(' ');
    });

    // This is solely to style the input based on whether it is in an error state or not
    const dynamicClass = computed(() =&gt; {
      // This is to remove padding on the right for the built-in calendar icon when using a date type
      let val = props.type === 'date' ? '' : 'pr-10';
      return isError.value
        ? `${val} border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500`
        : `${val} focus:ring-indigo-500 focus:border-indigo-500 border-gray-300`;
    });

    // The "hint" text will display any necessary hints as well as any error messages.
    // Styling and values of said hint text are primarily based on whether an input is in an error state.
    const hintClass = computed(() =&gt;
      isError.value ? 'text-red-600' : 'text-gray-500'
    );
    const hintId = computed(() =&gt;
      isError.value ? `${props.id}-error` : `${props.id}-input`
    );

    const hintText = computed(() =&gt; {
      if (errorText.value.length &gt; 0) return errorText.value;
      if (!!props.hint) return props.hint;
      return '';
    });

    const hasHint = computed(() =&gt; !!hintText.value.length);

    return {
      hasHint,
      hintText,
      hintClass,
      hintId,
      dynamicClass,
      isError,
      autofocus: props.autofocus,
      vuelidate: props.vuelidate,
      updateValue,
    };
  },
});
&lt;/script&gt;
</code></pre>
<p>and the updated form looks much better
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638502889722/C5IzKOLuB.png" alt="Screenshot 2021-12-02 224116.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 4]]></title><description><![CDATA[In the last article we created custom JVPSelect and JVPInput components that look horrendous and also that have no data validation.
In this article we will add input validation using a third-party library called Vuelidate.
Initial setup
We will be in...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-4</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-4</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Validation]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Thu, 02 Dec 2021 22:32:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1638484141005/9xC_NWveG.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-3">last article</a> we created custom <code>JVPSelect</code> and <code>JVPInput</code> components that look horrendous and also that have no data validation.
In this article we will add input validation using a third-party library called <a target="_blank" href="https://vuelidate-next.netlify.app/">Vuelidate</a>.</p>
<h2 id="heading-initial-setup">Initial setup</h2>
<p>We will be instantiating the <code>Vuelidate</code> object in <code>Selections.vue</code> instead of in <code>JVPInput.vue</code>.
We do this because the <code>JVPInput</code> component is, in effect, simply a wrapper.
We want to be able to use validation, but the rules and validation state for the purposes of form submission need to reside where the full form data resides.
The first thing we need to do is install Vuelidate</p>
<pre><code class="lang-bash">npm install @vuelidate/core @vuelidate/validators
</code></pre>
<p>From here we can either incorporate it globally in the project or import it into each component we want to use it.
We will use Vuelidate in the latter way.
We first import the library and necessary validators into our component</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> useVuelidate <span class="hljs-keyword">from</span> <span class="hljs-string">'@vuelidate/core'</span>;
<span class="hljs-keyword">import</span> { required, maxLength } <span class="hljs-keyword">from</span> <span class="hljs-string">'@vuelidate/validators'</span>;
</code></pre>
<p>Then we instantiate the object and set the validation rules for the <code>symbol</code> attribute of the form</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> rules = {
  <span class="hljs-attr">symbol</span>: { required, <span class="hljs-attr">maxLength</span>: maxLength(<span class="hljs-number">5</span>) },
};
<span class="hljs-keyword">const</span> v$ = useVuelidate(rules, state);
</code></pre>
<p>where the <code>state</code> element is the <code>reactive</code> element that we've already set in the component.
The above rules set the <code>symbol</code> attribute to be required and to be no longer than 5 characters, though you can set it to whatever you want.</p>
<h3 id="heading-custom-validators">Custom validators</h3>
<p>We also want to check that if we've included data for both the <code>startDate</code> and <code>endDate</code> attributes that the former is earlier than the latter.</p>
<p>We first need to extend the <code>rules</code> that were initially created before</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> rules = {
  <span class="hljs-attr">symbol</span>: { required, <span class="hljs-attr">maxLength</span>: maxLength(<span class="hljs-number">5</span>) },
  <span class="hljs-attr">startDate</span>: {
    validateDateFormat,
    mustBeEarlierDate,
  },
  <span class="hljs-attr">endDate</span>: {
    validateDateFormat,
  },
};
<span class="hljs-keyword">const</span> v$ = useVuelidate(rules, state });
</code></pre>
<p>The first thing we want to do is first check to see if the value passed in is a valid date.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> checkDateFormat = <span class="hljs-function">(<span class="hljs-params">param</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!param) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(param).toString() !== <span class="hljs-string">'Invalid Date'</span>;
};

<span class="hljs-keyword">const</span> validateDateFormat = helpers.withMessage(
  <span class="hljs-string">'Please enter a valid date.'</span>,
  checkDateFormat
);
</code></pre>
<p>The <code>checkDateFormat</code> function follows methodology in the <a target="_blank" href="https://vuelidate-next.netlify.app/custom_validators.html#custom-error-messages">Custom error messages</a> documentation on the Vuelidate docs.  </p>
<p>Then we want to check that the value passed in is earlier than the <code>reactive</code> state's <code>endDate</code> value.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> mustBeEarlierDate = helpers.withMessage(
  <span class="hljs-string">'Start date must be earlier than end date.'</span>,
  <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> endDate = computed(<span class="hljs-function">() =&gt;</span> state.endDate);
    <span class="hljs-keyword">if</span> (!value || !endDate.value) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">if</span> (!checkDateFormat(endDate.value)) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(value) &lt; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(endDate.value)
  }
);
</code></pre>
<p>The <code>mustBeEarlierDate</code> function follows methodology in the <a target="_blank" href="https://vuelidate-next.netlify.app/custom_validators.html#passing-extra-properties-to-validators">Passing extra properties to validators</a> documentation on the Vuelidate docs.
We need to be careful, though, as we don't care about this validation if either of the dates is not included, but <em>only</em> if both are set.</p>
<p>We can now check to see that the functionality works properly by inputting valid and invalid values to the form and clicking the <code>Get Chart</code> button.
We should see the <code>triggered handleSubmit</code> text any time we click the button except when both the start and end date being included <strong>and</strong> the start date is not strictly less than the end date.</p>
<p>The updated <code>Selections.vue</code> component should now look like this</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;form class="ml-5" @submit.prevent="handleSubmit"&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.symbol"
        label="Symbol"
        id="symbol"
        name="symbol"
        type="text"
        placeholder="eg. MSFT"
        :vuelidate="v$.symbol"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.startDate"
        label="Start Date"
        id="start-date"
        name="startDate"
        type="text"
        :vuelidate="v$.startDate"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.endDate"
        label="End Date"
        id="end-date"
        name="endDate"
        type="text"
        :vuelidate="v$.endDate"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPSelect
        v-model="state.interval"
        label="Interval"
        id="interval"
        :options="intervals"
      /&gt;
    &lt;/div&gt;
    &lt;button type="submit" class="border-4 border-indigo-500"&gt;Get Chart&lt;/button&gt;
  &lt;/form&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, reactive, computed } from 'vue';
import useVuelidate from '@vuelidate/core';
import { required, maxLength, helpers } from '@vuelidate/validators';
import JVPInput from './JVPInput.vue';
import JVPSelect from './JVPSelect.vue';

export default defineComponent({
  components: { JVPInput, JVPSelect },
  setup() {
    const intervals = ['Daily', 'Weekly', 'Monthly'];

    const state = reactive({
      symbol: '',
      interval: 'Daily',
      startDate: '',
      endDate: '',
    });

    const mustBeEarlierDate = helpers.withMessage(
      'Start date must be earlier than end date.',
      (value) =&gt; {
        const endDate = computed(() =&gt; state.endDate);
        if (!value || !endDate.value) return true;
        if (!checkDateFormat(endDate.value)) return true;
        return new Date(value) &lt; new Date(endDate.value);
      }
    );

    const checkDateFormat = (param) =&gt; {
      if (!param) return true;
      return new Date(param).toString() !== 'Invalid Date';
    };

    const validateDateFormat = helpers.withMessage(
      'Please enter a valid date.',
      checkDateFormat
    );

    const rules = {
      symbol: { required, maxLength: maxLength(5) },
      startDate: {
        validateDateFormat,
        mustBeEarlierDate,
      },
      endDate: {
        validateDateFormat,
      },
    };
    const v$ = useVuelidate(rules, state);

    const disabled = computed(() =&gt; {
      return v$.value.$invalid;
    });

    const handleSubmit = () =&gt; {
      v$.value.$validate();
      if (!v$.value.$invalid) {
        console.log('triggered handleSubmit');
      }
    };

    return {
      intervals,
      state,
      disabled,
      v$,
      handleSubmit,
    };
  },
});
&lt;/script&gt;
</code></pre>
<h2 id="heading-display-error-messages">Display error messages</h2>
<p>This is all fine and good, but we need to somehow let the user know when the form is in an error state.
In order to do this we need to add <code>v$</code> to the return of the <code>setup</code> function in the component so that we can access it in the template and pass it down to the <code>JVPInput.vue</code> components.
Next we need to update the props in the <code>JVPInput.vue</code> component to include this new object, which we'll simply call <code>vuelidate</code>.
The props for this component should now look like</p>
<pre><code class="lang-javascript">props: {
  <span class="hljs-attr">type</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-string">'text'</span>,
  },
  <span class="hljs-attr">placeholder</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span>,
  },
  <span class="hljs-attr">vuelidate</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-function">() =&gt;</span> ({})
  },
  ...inputProps,
},
</code></pre>
<p>and we'll add in an extra attribute to each <code>JVPInput</code> instance in <code>Selections.vue</code>, calling the specific element of the <code>v$</code> object.
It will look something like</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">JVPInput</span>
  <span class="hljs-attr">v-model</span>=<span class="hljs-string">"state.startDate"</span>
  <span class="hljs-attr">label</span>=<span class="hljs-string">"Start Date"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"start-date"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"startDate"</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
  <span class="hljs-attr">:vuelidate</span>=<span class="hljs-string">"v$.startDate"</span>
/&gt;</span>
</code></pre>
<p>Once this is done we can then set some computed properties in the <code>JVPInput.vue</code> component to check if the input is in an error state and, if it is, create a dynamic <code>errorText</code> element.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Vuelidate is used as a validation library.</span>
<span class="hljs-comment">// We use built-in functionality to determine if any rules are violated</span>
<span class="hljs-comment">// as well as display of the associated error text</span>
<span class="hljs-keyword">const</span> isError = computed(<span class="hljs-function">() =&gt;</span> props.vuelidate.$error);
<span class="hljs-keyword">const</span> errorText = computed(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> messages =
    props.vuelidate.$errors?.map(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> err.$message) ?? [];
  <span class="hljs-keyword">return</span> messages.join(<span class="hljs-string">' '</span>);
});
</code></pre>
<p>We then want to add the <code>isError</code> and <code>errorText</code> elements to the <code>return</code> statement in the <code>setup</code> function in <code>JVPInput.vue</code> and add in some markup to display the text if and only if there is an error.
We can add in a simple <code>&lt;p&gt;</code> tag to conditionally display the error messages, and the updated <code>JVPInput.vue</code> component should look like</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id" class="block text-sm font-medium text-gray-700"&gt;
      {{ label }}
    &lt;/label&gt;
    &lt;div class="mt-1 relative rounded-md shadow-sm"&gt;
      &lt;input
        :value="modelValue"
        :type="type"
        :name="id"
        :id="id"
        class="block w-full sm:text-sm rounded-md shadow-sm"
        @input="updateValue"
      /&gt;
    &lt;/div&gt;
    &lt;p v-if="isError"&gt;{{ errorText }}&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    type: {
      type: String,
      required: false,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: false,
    },
    vuelidate: {
      type: Object,
      required: false,
      default: () =&gt; ({}),
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // This is simply cleaner than putting emit code in the HTML
    const updateValue = (e) =&gt; emit('update:modelValue', e.target.value);

    // Vuelidate is used as a validation library.
    // We use built-in functionality to determine if any rules are violated
    // as well as display of the associated error text
    const isError = computed(() =&gt; props.vuelidate.$error);
    const errorText = computed(() =&gt; {
      const messages =
        props.vuelidate.$errors?.map((err) =&gt; err.$message) ?? [];
      return messages.join(' ');
    });

    return { updateValue, isError, errorText };
  },
});
&lt;/script&gt;
</code></pre>
<p>Now we can see that if we don't include a symbol, set the date fields to be random text, and click the <code>Get Chart</code> button then we should see something that looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638484175205/eHbJljg9N.png" alt="Screenshot 2021-12-02 172542.png" />
In the next article we'll add in hint text, then we'll add in some conditional styling based on whether the form is in an error state or not.</p>
]]></content:encoded></item><item><title><![CDATA[Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 3]]></title><description><![CDATA[This short article will continue along what was alluded to in part 2.Within Selections.vue we have three input elements and one select element that are used to receive input regarding stock selection.
We would like to have uniform styling and will be...]]></description><link>https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-3</link><guid isPermaLink="true">https://blog.jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-3</guid><category><![CDATA[Vue.js]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Jeffrey Pohlmeyer]]></dc:creator><pubDate>Wed, 24 Nov 2021 21:16:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637788767697/NlX1-TAof.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This short article will continue along what was alluded to in <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-2">part 2</a>.<br />Within <code>Selections.vue</code> we have three input elements and one select element that are used to receive input regarding stock selection.
We would like to have uniform styling and will be implementing things like input validation so it make sense to compartmentalize these components.
Also, what the hell is the point of using Vue if we're not going to do something like this?</p>
<h2 id="heading-custom-jvpinputvue-component">Custom <code>JVPInput.vue</code> Component</h2>
<p>We start by creating a simple, generic component</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id"&gt;{{ label }}&lt;/label&gt;
    &lt;input
      :value="modelValue"
      :type="type"
      :name="id"
      :id="id"
      :placeholder="placeholder"
      class="mx-2 border"
      @input="updateValue"
    /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    type: {
      type: String,
      required: false,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: false,
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // This is simply cleaner than putting emit code in the HTML
    const updateValue = (e) =&gt; emit('update:modelValue', e.target.value);

    return { updateValue };
  },
});
&lt;/script&gt;
</code></pre>
<p>The functionality of the component is fairly simple: split up the <code>v-model</code> directive into <code>:value</code> and <code>@input</code> functionality, and use passed-in props for the <code>type</code>, <code>id</code>, and <code>name</code>.  </p>
<p>We can see that it looks like some information is missing, namely the <code>modelValue</code>, <code>id</code>, and <code>label</code> values that are referenced within the <code>template</code> tag.
These can be found in the <code>inputProps</code> object, which is defined in a separate file as</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// input-props.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">id</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
  },
  <span class="hljs-attr">modelValue</span>: {
    <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
  },
  <span class="hljs-attr">label</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
  },
  <span class="hljs-attr">hint</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-string">''</span>,
  },
};
</code></pre>
<p>The <code>hint</code> that is included in <code>input-props.js</code> is something that will be used later.</p>
<h2 id="heading-custom-jvpselectvue-component">Custom <code>JVPSelect.vue</code> Component</h2>
<p>In a similar manner, but somewhat simpler, we create the <code>JVPSelect.vue</code> component</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id"&gt;{{ label }}&lt;/label&gt;
    &lt;select :value="item" :id="id" class="mx-2 border" @input="updateItem"&gt;
      &lt;option
        v-for="option in options"
        :key="option"
        :name="option"
        :selected="modelValue"
      &gt;
        {{ option }}
      &lt;/option&gt;
    &lt;/select&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    options: {
      type: Array,
      required: true,
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const item = computed(() =&gt; props.modelValue);
    const updateItem = (e) =&gt; emit('update:modelValue', e.target.value);
    return { item, updateItem };
  },
});
&lt;/script&gt;
</code></pre>
<p>Like the <code>JVPInput.vue</code> component, this is simply moving the functionality that existed in <code>Selections.vue</code> into its own component.</p>
<h3 id="heading-updating-selectionsvue">Updating <code>Selections.vue</code></h3>
<p>Now with these two components created we will update <code>Selections.vue</code> to utilize them.
We then update <code>Selections.vue</code> to use this component instead of the generic input component.</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;form class="ml-5" @submit.prevent="handleSubmit"&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.symbol"
        label="Symbol"
        id="symbol"
        name="symbol"
        type="text"
        placeholder="eg. MSFT"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.startDate"
        label="Start Date"
        id="start-date"
        name="startDate"
        type="text"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPInput
        v-model="state.endDate"
        label="End Date"
        id="end-date"
        name="endDate"
        type="text"
      /&gt;
    &lt;/div&gt;
    &lt;div class="my-1"&gt;
      &lt;JVPSelect v-model="state.interval" label="Interval" id="interval" :options="intervals" /&gt;
    &lt;/div&gt;
    &lt;button type="submit" class="border"&gt;Get Chart&lt;/button&gt;
  &lt;/form&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, reactive } from 'vue';
import JVPInput from './JVPInput.vue';
import JVPSelect from './JVPSelect.vue';

export default defineComponent({
  components: { JVPInput, JVPSelect },
  setup() {
    const intervals = ['Daily', 'Weekly', 'Monthly'];

    const state = reactive({
      symbol: '',
      interval: 'Daily',
      startDate: '',
      endDate: '',
    });

    const handleSubmit = () =&gt; {
      console.log('triggered handleSubmit');
    };

    return {
      intervals,
      state,
      handleSubmit,
    };
  },
});
&lt;/script&gt;
</code></pre>
<p>and the app will look no different than before, which is the goal, but it will make further customization much easier.</p>
<h2 id="heading-styling">Styling</h2>
<p>As we saw in part 2, the general app look is fairly ugly.  We'll show it again here for reference.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637620433351/i_XISfzLvy.png" alt="img_1.png" />
We want to add some Tailwind styles to clean this up a little.  In regard to the <code>JVPSelect.vue</code> component it won't make much difference than if we had left functionality in the <code>Selections.vue</code> component, but for organization it's better this way.</p>
<h3 id="heading-jvpselectvue"><code>JVPSelect.vue</code></h3>
<p>We'll add simple font styling to the <code>label</code>:</p>
<ul>
<li>block</li>
<li>text-sm</li>
<li>font-medium</li>
<li>text-gray-700</li>
</ul>
<p>and slightly more complex styling to the actual <code>&lt;select&gt;</code> tag:</p>
<ul>
<li>block</li>
<li>w-full</li>
<li>pl-3</li>
<li>pr-10</li>
<li>py-2</li>
<li>mt-1</li>
<li>text-base</li>
<li>border-gray-300</li>
<li>focus:outline-none</li>
<li>focus:ring-indigo-500</li>
<li>focus:border-indigo-500</li>
<li>sm:text-sm</li>
<li>rounded-md</li>
</ul>
<p>To see what each of these classes do to the element I encourage you to check out the <a target="_blank" href="https://tailwindcss.com/docs">Tailwind docs</a>.
Full disclosure, though, I have purchased a license to <a target="_blank" href="https://tailwindui.com/">Tailwind UI</a> so a lot of this came from there because I'm terrible when it comes to design independently.
Now, the updated <code>JVPSelect.vue</code> component looks like this</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id" class="block text-sm font-medium text-gray-700"&gt;
      {{ label }}
    &lt;/label&gt;
    &lt;select
      :value="item"
      :id="id"
      class="
        mt-1
        block
        w-full
        pl-3
        pr-10
        py-2
        text-base
        border-gray-300
        focus:outline-none focus:ring-indigo-500 focus:border-indigo-500
        sm:text-sm
        rounded-md
      "
      @input="updateItem"
    &gt;
      &lt;option
        v-for="option in options"
        :key="option"
        :name="option"
        :selected="modelValue"
      &gt;
        {{ option }}
      &lt;/option&gt;
    &lt;/select&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent, computed } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    options: {
      type: Array,
      required: true,
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const item = computed(() =&gt; props.modelValue);
    const updateItem = (e) =&gt; emit('update:modelValue', e.target.value);
    return { item, updateItem };
  },
});
&lt;/script&gt;
</code></pre>
<h3 id="heading-jvpinputvue"><code>JVPInput.vue</code></h3>
<p>This component is going to be slightly more complicated.
One of the reasons for this is because we're going to be handling input validation in the <code>JVPInput.vue</code> component so we want to have <code>&lt;p&gt;</code> tag to display error messages or hints (remember the <code>hint</code> element in <code>input-props.js</code>???).
We want the labels to match what we already created for <code>JVPSelect.vue</code>, though, so that will be the same.
We will also wrap the <code>&lt;input&gt;</code> in a <code>div</code> element to make alignment a bit better as well as placement of any messages.
To this <code>div</code> we will add just a couple of classes:</p>
<ul>
<li>mt-1</li>
<li>relative</li>
<li>rounded-md</li>
<li>shadow-sm</li>
</ul>
<p>and then to the actual <code>input</code> element we will add</p>
<ul>
<li>block</li>
<li>w-full</li>
<li>sm:text-sm</li>
<li>rounded-md</li>
<li>shadow-sm</li>
</ul>
<p>Now the <code>JVPInput.vue</code> component looks like</p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;label :for="id" class="block text-sm font-medium text-gray-700"&gt;
      {{ label }}
    &lt;/label&gt;
    &lt;div class="mt-1 relative rounded-md shadow-sm"&gt;
      &lt;input
        :value="modelValue"
        :type="type"
        :name="id"
        :id="id"
        class="block w-full sm:text-sm rounded-md shadow-sm"
        @input="updateValue"
      /&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { defineComponent } from 'vue';
import inputProps from '../utils/input-props';

export default defineComponent({
  props: {
    type: {
      type: String,
      required: false,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: false,
    },
    ...inputProps,
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // This is simply cleaner than putting emit code in the HTML
    const updateValue = (e) =&gt; emit('update:modelValue', e.target.value);

    return { updateValue };
  },
});
&lt;/script&gt;
</code></pre>
<p>And in order to make the actual app look half decent, we'll add a <code>bg-gray-200</code> class to the main <code>div</code> in <code>App.vue</code> and a <code>px-4</code> class to the <code>&lt;Selections /&gt;</code> element in <code>App.vue</code></p>
<pre><code class="lang-vue">&lt;template&gt;
  &lt;div class="h-screen bg-gray-200"&gt;
    &lt;Selections class="px-4"/&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>and the app now looks like
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637620413676/QBxYF37tE.png" alt="img_2.png" />
which is still objectively terrible, but we're making progress.
The benefit of what we've done in this step is that we will be able to compartmentalize the error display in the <code>JVPInput.vue</code> component and it will keep <code>Selections.vue</code> relatively clean.  </p>
<p>In the <a target="_blank" href="https://jeffpohlmeyer.com/candlestick-docker-fastapi-vue-part-4">next article</a> we'll add input validation to the three <code>JVPInput.vue</code> elements, add error and hint message display, and add dynamic styling of the component itself for when it is in an error state.</p>
]]></content:encoded></item></channel></rss>