<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <atom:link href="https://rabdulwahhab.com/feed.xml" rel="self" type="application/rss+xml" />
  <title>Rayyan Abdul-Wahhab</title>
  <link>https://rabdulwahhab.com</link>
  <description>The official website of Rayyan Abdul-Wahhab.</description>
  <language>en-US</language>
  <generator>Tableau v0.30.0</generator>
    <item>
       <title>Building GRapple (Part 1): Bringing Real-Time Race Data to Life with Elixir</title>
       <link>https://rabdulwahhab.com/blog/building-grapple-part-1/</link>
       <pubDate>Sun, 26 Oct 2025 23:02:47 -04</pubDate>
       <guid>https://rabdulwahhab.com/blog/building-grapple-part-1/</guid>
       <description><![CDATA[ <h2><a href="#-the-starting-line" aria-hidden="true" class="anchor" id="-the-starting-line"></a>🟢 The Starting Line</h2>
<p>It was October 15th and I was spending a few minutes scrolling my LinkedIn feed when something caught my attention — an announcement for the kickoff of <a href="https://hackthetrack.devpost.com/">Toyota Racing Development's Hack the Track hackathon</a>.</p>
<p>I don’t know much about racing, but I’ve always loved cars since I was a boy. I looked into this hackathon some more and started to become increasingly interested. It was an opportunity to build something using a unique dataset from the world of motorsports.</p>
<p>This was certainly something out of left field for me, but the more I learned about how racing teams deal with massive amounts of <strong>live-streamed data</strong> — from car sensors to weather systems — the more I realized something:</p>
<blockquote>
<p>this was a perfect opportunity to push the <strong>BEAM’s real-time strengths</strong> into a completely new domain — motorsport telemetry.</p>
</blockquote>
<p>After reading the contest description, one category stood out immediately:</p>
<p><strong>Real-Time Analytics</strong> — <em>Design a tool that simulates real-time decision-making for a race engineer.</em></p>
<p>That was it. The perfect match for Elixir, Phoenix, and OTP.</p>
<p>I hesitated for a night — it was a big leap into a domain I knew nothing about — but the next day, I joined the hackathon.</p>
<p>Now the question was: <em>what would I build?</em></p>
<h2><a href="#-making-sense-of-the-data-jungle" aria-hidden="true" class="anchor" id="-making-sense-of-the-data-jungle"></a>🧩 Making Sense of the Data Jungle</h2>
<p>The organizers released gigabytes of CSVs: lap timing data, race results, track weather, and — most interestingly — <strong>telemetry data</strong> from the GR Cup race cars.</p>
<p>At first glance, it was overwhelming. Millions of rows, hundreds of fields with names like <code>accx_can</code>, <code>pbrake_f</code>, <code>Laptrigger_lapdist_dls</code>. I didn’t even know what half the abbreviations meant.</p>
<p>I started simple — using tools like <code>less</code>, <code>grep</code>, and Excel to poke around. Then it clicked: Each row wasn’t just a data point — it was <strong>a heartbeat from the car</strong>.</p>
<p>Telemetry captures everything happening inside a racecar every fraction of a second: speed, throttle, brake pressure, gear changes, acceleration, GPS position.</p>
<p>And if I could take those heartbeats and <strong>replay them live</strong>, I could recreate the rhythm of a race — not a static CSV, but a <em>living timeline of events</em>.</p>
<p>That idea became the foundation of what I came to call <em>GRapple</em> (because it grapples with lots of data 🙂):</p>
<blockquote>
<p>a real-time race replay, analytics, and strategy system powered by Elixir and OTP.</p>
</blockquote>
<h2><a href="#️-turning-data-into-motion" aria-hidden="true" class="anchor" id="️-turning-data-into-motion"></a>⚙️ Turning Data into Motion</h2>
<p>My first task was to sort the telemetry chronologically. That sounds simple, but the dataset wasn’t ordered, contained duplicates, and was over 11 million lines long, so I used Unix <code>sort</code> and <code>uniq</code> and wired those tools into Elixir for performance.</p>
<p>Once sorted, I used <a href="https://github.com/dashbitco/nimble_csv"><code>NimbleCSV</code></a> to parse each lazily streamed event into well-defined structs. The core of it looks like this:</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;">@</span><span style="color: #928374;">doc</span> <span style="color: #928374;">&quot;&quot;&quot;
</div><div class="line" data-line="2">  Parses the given Telemetry Data CSV file into a lazy Stream of chronological TelemetryPoints.
</div><div class="line" data-line="3">  WARNING: this function can take a few minutes if the CSV file is large (GBs)
</div><div class="line" data-line="4">  &quot;&quot;&quot;</span>
</div><div class="line" data-line="5">  <span style="color: #d3869b;">@</span><span style="color: #d3869b;">spec </span><span style="color: #b8bb26; font-weight: bold;">parse_stream!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">filename</span> <span style="color: #fe8019;">::</span> <span style="color: #ebdbb2;">String</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">)</span> <span style="color: #fe8019;">::</span> <span style="color: #fe8019;">&lbrace;</span><span style="color: #b8bb26; font-weight: bold;">integer</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">Enumerable</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="6">  <span style="color: #fb4934;">def</span> <span style="color: #b8bb26; font-weight: bold;">parse_stream!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">filename</span><span style="color: #fe8019;">)</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="7">    <span style="color: #ebdbb2;">cleaned_telemetry_file</span> <span style="color: #fe8019;">=</span> <span style="color: #b8bb26; font-weight: bold;">clean_file!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">filename</span><span style="color: #fe8019;">)</span> <span style="color: #928374;"># remove dups</span>
</div><div class="line" data-line="8">    <span style="color: #ebdbb2;">sorted_telemetry_file</span> <span style="color: #fe8019;">=</span> <span style="color: #b8bb26; font-weight: bold;">sort_file_by_timestamp!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">cleaned_telemetry_file</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="9">    <span style="color: #ebdbb2;">telemetry_count</span> <span style="color: #fe8019;">=</span> <span style="color: #b8bb26; font-weight: bold;">num_data_points</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">sorted_telemetry_file</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="10">
</div><div class="line" data-line="11">    <span style="color: #ebdbb2;">stream</span> <span style="color: #fe8019;">=</span>
</div><div class="line" data-line="12">      <span style="color: #ebdbb2;">File</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">stream!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">sorted_telemetry_file</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="13">      <span style="color: #fe8019;">|&gt;</span> <span style="color: #ebdbb2;">TelemetryParser</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">parse_stream</span><span style="color: #fe8019;">(</span><span style="color: #83a598;">skip_headers: </span><span style="color: #d3869b;">false</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="14">      <span style="color: #fe8019;">|&gt;</span> <span style="color: #ebdbb2;">Stream</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">map</span><span style="color: #fe8019;">(</span><span style="color: #fb4934;">fn</span> <span style="color: #ebdbb2;">row</span> <span style="color: #fe8019;">-&gt;</span>
</div><div class="line" data-line="15">        <span style="color: #fb4934;">try</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="16">          <span style="color: #ebdbb2;">TelemetryPoint</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">from_row!</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">row</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="17">        <span style="color: #fb4934;">rescue</span>
</div><div class="line" data-line="18">          <span style="color: #ebdbb2;">e</span> <span style="color: #fb4934;">in</span> <span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">MatchError</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">ArgumentError</span><span style="color: #fe8019;">]</span> <span style="color: #fe8019;">-&gt;</span>
</div><div class="line" data-line="19">            <span style="color: #ebdbb2;">Logger</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">debug</span><span style="color: #fe8019;">(</span><span style="color: #b8bb26;">&quot;Skipping unparsable row: </span><span style="color: #fe8019;">#&lbrace;</span><span style="color: #b8bb26; font-weight: bold;">inspect</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">row</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #b8bb26;"> (</span><span style="color: #fe8019;">#&lbrace;</span><span style="color: #ebdbb2;">Exception</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">message</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">e</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #b8bb26;">)&quot;</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="20">            <span style="color: #fe8019;">nil</span>
</div><div class="line" data-line="21">        <span style="color: #fb4934;">end</span>
</div><div class="line" data-line="22">      <span style="color: #fb4934;">end</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="23">      <span style="color: #928374;"># reject any rows that failed to be parsed</span>
</div><div class="line" data-line="24">      <span style="color: #fe8019;">|&gt;</span> <span style="color: #ebdbb2;">Stream</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">reject</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">&amp;</span><span style="color: #b8bb26; font-weight: bold;">is_nil</span><span style="color: #fe8019;">/</span><span style="color: #fe8019;">1</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="25">      <span style="color: #fe8019;">|&gt;</span> <span style="color: #b8bb26; font-weight: bold;">compute_deltas</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="26">
</div><div class="line" data-line="27">    <span style="color: #fe8019;">&lbrace;</span><span style="color: #ebdbb2;">telemetry_count</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">stream</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="28">  <span style="color: #fb4934;">end</span>
</div></code></pre>
<p><code>compute_deltas/1</code> does something interesting. It looks at the timestamps of two successive telemetry events, computes the delta of the next event, and stores this inside the struct for the current event. This way, when we replay the telemetry event live, we know exactly how long to wait between any two events and can simulate the race.</p>
<p>Then came the fun part: I needed a process that would emit those events live at the same timing intervals they originally happened (using our deltas from before). Enter the <code>TelemetryReplayer</code>.</p>
<p>At first, I was thinking to broadcast every telemetry row as a message on a Phoenix PubSub topic so other processes could subscribe, react, and compute metrics in real time. This would work great when I incorporate LiveView later on. But for now since I'm still building the backend, I decided I would keep it simple and just have the <code>TelemetryReplayer</code> cast each telemetry event to its associated car GenServer (which I would implement next).</p>
<p>At last, everything was starting to come together. I had gone from millions of rows of CSV files to well-defined data streaming in real time using the power of OTP. Seeing that first replay run felt surreal — after days of CSV wrangling, a virtual race had come to life in my terminal.</p>
<p><img src="/media/grapple-telemetry-replay.png" alt="Terminal output from GRapple replaying race telemetry events in real time" /></p>
<p>This was a truly motivating moment and I was ready to keep moving forward. It's not enough to replay the race live. If I could have one GenServer process per car consuming its telemetry events and keeping rolling metrics as its state, I could unlock a whole new world of possibilities when it comes to analytics and insight.</p>
<h2><a href="#-teaching-grapple-to-think" aria-hidden="true" class="anchor" id="-teaching-grapple-to-think"></a>📊 Teaching GRapple to Think</h2>
<p>Once the race replay worked, the next challenge was to give GRapple some <em>intuition</em>.</p>
<p>I built a <code>MetricsServer</code>, one GenServer process per car, that tracks real-time statistics — speed, throttle, brake pressure, soon acceleration, steering, and much more.</p>
<p>Each process consumes its car's telemetry events, and maintains rolling per-lap stats, smoothing raw signals with exponential moving averages (EMA) to highlight trends in driver behavior. Once a new lap starts, it stores the past lap in its own history. Each process is supervised by a <code>MetricsServerSupervisor</code> for fault-tolerance.</p>
<p>This way, I could get the metrics for any car in the race at any given time by looking at the state of its associated running process. It also helps tremendously in reasoning about the system and keeps things simple: each process knows its car's performance best. Using hand-crafted test CSV files, I started to see the system in action and it was mesmerizing. Data was flowing from a CSV file to the <code>TelemetryReplayer</code> to a car's <code>MetricsServer</code>, which would perform rolling metrics calculations in real time.</p>
<p>And this was just the beginning. Eventually, I am hoping to implement another dedicated Analytics process that will query each <code>MetricsServer</code> and derive higher-level race insights.</p>
<h2><a href="#-what-ive-learned-so-far" aria-hidden="true" class="anchor" id="-what-ive-learned-so-far"></a>🧠 What I've Learned So Far</h2>
<p>This project reminded me why I love Elixir and the BEAM.</p>
<ul>
<li>The <a href="https://elixirschool.com/en/lessons/intermediate/concurrency">Actor Model</a> is perfect for modeling a race with multiple cars.</li>
<li>OTP made concurrency <em>predictable</em>.</li>
<li>Streams let me handle millions of rows lazily and efficiently.</li>
<li>Elixir is the right tool for this job.</li>
</ul>
<p>There's quite a bit until I get to the finish line with this project, but it's been refreshing stepping into the world of motorsports and a joy using Elixir so far.</p>
<h2><a href="#-the-road-ahead" aria-hidden="true" class="anchor" id="-the-road-ahead"></a>🚀 The Road Ahead</h2>
<p>In the next stage, I’ll start extending the <code>MetricsServer</code> to compute more metrics per car using the telemetry events. So far, I've implemented speed, throttle behavior, and brake pressure rolling metrics, but will need to add acceleration, lateral G (for cornering insight), steering, lap timing, and (if time permits) lap section comparison for more insight. Eventually, I will connect it to <strong>AI models via</strong> <a href="https://replicate.com/"><strong>Replicate</strong></a> to simulate a race engineer that can answer live questions about performance and even strategize race decisions.</p>
<p>I'll be writing some more blog posts as I continue building <em>GRapple</em> for the Toyota Hack the Track 2025 challenge. Stay tuned.</p> ]]></description>
    </item>
    <item>
       <title>My Resume Is a LiveView App</title>
       <link>https://rabdulwahhab.com/blog/my-resume-is-a-liveview-app/</link>
       <pubDate>Wed, 14 May 2025 12:48:52 -04</pubDate>
       <guid>https://rabdulwahhab.com/blog/my-resume-is-a-liveview-app/</guid>
       <description><![CDATA[ <p>Resumes can be a pain to maintain. They’re either locked away in Word documents, fragile LaTeX templates, or scattered across PDFs with no version control. As a developer who values clean structure, live updates, modularity, and good design, I wanted something better—so I built my resume as a fully live, styled, and printable Phoenix LiveView app. No database. No external CMS. Just Elixir, TailwindCSS (via DaisyUI), and the power of functional code. Here’s how I did it—and why you might want to consider the same approach.</p>
<h2><a href="#why-i-chose-to-build-my-resume-with-liveview" aria-hidden="true" class="anchor" id="why-i-chose-to-build-my-resume-with-liveview"></a>Why I Chose to Build My Resume with LiveView</h2>
<p>Before writing my resume in Elixir and Phoenix, I searched the web for existing solutions to the extensible, modular, versioned resume problem. I found a few options, the most prominent being <a href="https://rxresu.me/">RxResume</a> and <a href="https://jsonresume.org/">JSON Resume</a>. I actually created an account on RxResume and tried creating my resume. I found it to be a very polished experience and excellent for most use cases. However, I wanted to own my data and version my resume content rather than the finished product. This led me to stumble upon JSON Resume. I liked the well-defined format, but didn't want to use JSON. I wanted to build something in pure Elixir that would</p>
<ul>
<li>Emphasize modularity and flexibility</li>
<li>Support Git-based versioning</li>
<li>Cleanly separate resume content and presentation</li>
<li>Provide a streamlined development experience</li>
<li>Make resume writing and editing fun</li>
</ul>
<p>Eventually I decided I would build my own solution for myself. I might open-source the code in the near future, but for now, I am keeping my resume content along with the app code for easier versioning.</p>
<h2><a href="#designing-resume-data" aria-hidden="true" class="anchor" id="designing-resume-data"></a>Designing Resume Data</h2>
<p>I bootstrapped this project with <code>phx_new</code> version 1.8.0-rc3, which at the time of writing, had the latest and greatest of Phoenix features and updates. I really liked that it had <a href="https://daisyui.com/">DaisyUI</a> and a theme-changer component out-of-the-box. It's nice to get dark-mode support free for easier nighttime resume writing.</p>
<p>The first step was to design my data. A good data definition goes a long way in thinking about and solving problems, so I opted for creating structs for all relevant parts of a <code>Resume</code>. This way, I can expect certain keys in my resume data and also receive compile-time checks.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #fb4934;">defmodule</span> <span style="color: #ebdbb2;">LiveResume.Resumes.Resume</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="2">  <span style="color: #928374;">@</span><span style="color: #928374;">moduledoc</span> <span style="color: #928374;">&quot;&quot;&quot;
</div><div class="line" data-line="3">  Represents a Resume with basic personal and professional information.
</div><div class="line" data-line="4">  &quot;&quot;&quot;</span>
</div><div class="line" data-line="5">  <span style="color: #fb4934;">alias</span> <span style="color: #ebdbb2;">LiveResume.Resumes</span><span style="color: #fe8019;">.</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="6">    <span style="color: #ebdbb2;">Bio</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="7">    <span style="color: #ebdbb2;">EducationalExperience</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="8">    <span style="color: #ebdbb2;">Certification</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="9">    <span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="10">    <span style="color: #ebdbb2;">Project</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="11">    <span style="color: #ebdbb2;">WorkExperience</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="12">    <span style="color: #ebdbb2;">Link</span>
</div><div class="line" data-line="13">  <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="14">
</div><div class="line" data-line="15">  <span style="color: #d3869b;">@</span><span style="color: #d3869b;">type </span><span style="color: #ebdbb2;">t</span> <span style="color: #fe8019;">::</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">__MODULE__</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="16">          <span style="color: #83a598;">bio: </span><span style="color: #ebdbb2;">Bio</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="17">          <span style="color: #83a598;">education: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">EducationalExperience</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="18">          <span style="color: #83a598;">certifications: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">Certification</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="19">          <span style="color: #83a598;">skills: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="20">          <span style="color: #83a598;">projects: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">Project</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="21">          <span style="color: #83a598;">work_experiences: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">WorkExperience</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="22">          <span style="color: #83a598;">social_links: </span><span style="color: #fe8019;">[</span><span style="color: #ebdbb2;">Link</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">t</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">]</span>
</div><div class="line" data-line="23">        <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="24">
</div><div class="line" data-line="25">  <span style="color: #fb4934;">defstruct</span> <span style="color: #83a598;">bio: </span><span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Bio</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="26">            <span style="color: #83a598;">education: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="27">            <span style="color: #83a598;">skills: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="28">            <span style="color: #83a598;">projects: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="29">            <span style="color: #83a598;">certifications: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="30">            <span style="color: #83a598;">work_experiences: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="31">            <span style="color: #83a598;">social_links: </span><span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span>
</div><div class="line" data-line="32"><span style="color: #fb4934;">end</span>
</div></code></pre>
<p>Here is my definition of a Resume. I won't show all struct code here for the sake of brevity.</p>
<h2><a href="#loading-resume-data-at-runtime" aria-hidden="true" class="anchor" id="loading-resume-data-at-runtime"></a>Loading Resume Data at Runtime</h2>
<p>Now that I had an outline of what my resume content would look like, I needed a way to store it in a file and load it at runtime for use in my app. Elixir has a useful function called <a href="https://hexdocs.pm/elixir/main/Code.html#eval_file/2"><code>Code.eval_file/2</code></a>, which evaluates the expressions in a file and returns the result. Now all I needed to do was write a <code>Resume</code> inside a <code>my_resume.exs</code> file. But where would I store this file? As I mentioned before, I wanted my resume files to live in the codebase along with my app code for maximum simplicity and convenience. My main priority was to create an app that was useful to me and solved a real problem I had been having, not to share my code. In Phoenix projects, static files typically live inside the project <code>priv/</code> directory, so I created <code>priv/resumes</code> to hold my resume <code>.exs</code> files. Here is an example of what my resume files look like:</p>
<p>Because this evaluates Elixir code, the application loads only version-controlled resume files that I control. It is not appropriate for uploaded or otherwise untrusted files.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;"># priv/resumes/test_resume.exs</span>
</div><div class="line" data-line="2"><span style="color: #fb4934;">alias</span> <span style="color: #ebdbb2;">LiveResume.Resumes</span><span style="color: #fe8019;">.</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="3">  <span style="color: #ebdbb2;">Resume</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="4">  <span style="color: #ebdbb2;">Bio</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="5">  <span style="color: #ebdbb2;">Certification</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="6">  <span style="color: #ebdbb2;">EducationalExperience</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="7">  <span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="8">  <span style="color: #ebdbb2;">Project</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="9">  <span style="color: #ebdbb2;">WorkExperience</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="10">  <span style="color: #ebdbb2;">Link</span>
</div><div class="line" data-line="11"><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="12">
</div><div class="line" data-line="13"><span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Resume</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="14">  <span style="color: #83a598;">bio: </span><span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Bio</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="15">    <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Your Name Here&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="16">    <span style="color: #83a598;">headline: </span><span style="color: #b8bb26;">&quot;Full Stack Developer&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="17">    <span style="color: #83a598;">address: </span><span style="color: #b8bb26;">&quot;100 Tawheed Way, Sometown, TN&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="18">    <span style="color: #83a598;">phone: </span><span style="color: #b8bb26;">&quot;(555)-555-5555&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="19">    <span style="color: #83a598;">email: </span><span style="color: #b8bb26;">&quot;me@me.com&quot;</span>
</div><div class="line" data-line="20">  <span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="21">  <span style="color: #83a598;">education: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="22">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">EducationalExperience</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="23">      <span style="color: #83a598;">school: </span><span style="color: #b8bb26;">&quot;Northeastern University&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="24">      <span style="color: #83a598;">location: </span><span style="color: #b8bb26;">&quot;Boston, Massachusetts&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="25">      <span style="color: #83a598;">degree: </span><span style="color: #b8bb26;">&quot;Bachelor of Arts&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="26">      <span style="color: #83a598;">field: </span><span style="color: #b8bb26;">&quot;Computer Science&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="27">      <span style="color: #83a598;">start_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2017-09-05<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="28">      <span style="color: #83a598;">end_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2021-04-16<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="29">      <span style="color: #83a598;">grade: </span><span style="color: #b8bb26;">&quot;cum laude&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="30">      <span style="color: #83a598;">note: </span><span style="color: #b8bb26;">&quot;Member of the Honors Program&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="31">    <span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="32">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">EducationalExperience</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="33">      <span style="color: #83a598;">school: </span><span style="color: #b8bb26;">&quot;Madison Academic Magnet High School&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="34">      <span style="color: #83a598;">location: </span><span style="color: #b8bb26;">&quot;Jackson, TN&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="35">      <span style="color: #83a598;">degree: </span><span style="color: #b8bb26;">&quot;High School Diploma&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="36">      <span style="color: #83a598;">note: </span><span style="color: #b8bb26;">&quot;Honors&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="37">      <span style="color: #83a598;">start_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2013-08-01<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="38">      <span style="color: #83a598;">end_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2017-05-20<span style="color: #fe8019;">]</span>
</div><div class="line" data-line="39">    <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="40">  <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="41">  <span style="color: #83a598;">certifications: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="42">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Certification</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="43">      <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Certified in Cybersecurity (CC)&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="44">      <span style="color: #83a598;">issuer: </span><span style="color: #b8bb26;">&quot;ISC2&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="45">      <span style="color: #83a598;">issue_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2025-02-16<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="46">      <span style="color: #83a598;">expiration_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2028-02-29<span style="color: #fe8019;">]</span>
</div><div class="line" data-line="47">    <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="48">  <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="49">  <span style="color: #83a598;">skills: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="50">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Elixir&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">8</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="51">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Python&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="52">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;C/C++&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="53">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;JavaScript&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="54">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Bash&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">6</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="55">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;HTML&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="56">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Phoenix&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="57">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;ReactJS&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">4</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="58">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;React Native&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">3</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="59">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;TailwindCSS&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">5</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="60">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Linux (Ubuntu, Debian, Kali)&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">9</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="61">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Docker&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">5</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="62">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Git&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">strength: </span><span style="color: #d3869b;">7</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="63">  <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="64">  <span style="color: #83a598;">projects: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="65">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Project</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="66">      <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;LiveCalculator&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="67">      <span style="color: #83a598;">description: </span><span style="color: #b8bb26;">&quot;&quot;&quot;
</div><div class="line" data-line="68">      LiveCalculator is a LiveView calculator app. It works great!
</div><div class="line" data-line="69">      &quot;&quot;&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="70">      <span style="color: #83a598;">completed_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2025-04-20<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="71">      <span style="color: #83a598;">link: </span><span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Link</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="72">        <span style="color: #83a598;">url: </span><span style="color: #b8bb26;">&quot;https://github.com/my_account/live_calc&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="73">        <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;github.com/my_account/live_calc&quot;</span>
</div><div class="line" data-line="74">      <span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="75">      <span style="color: #83a598;">skills: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="76">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Elixir&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="77">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Phoenix&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="78">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;LiveView&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="79">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;OTP&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="80">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;JavaScript&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="81">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;TailwindCSS&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="82">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Docker&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="83">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Fly.io&quot;</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="84">      <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="85">      <span style="color: #83a598;">details: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="86">        <span style="color: #b8bb26;">&quot;Built a LiveView calculator app using Elixir, Phoenix, and OTP&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="87">        <span style="color: #b8bb26;">&quot;Leveraged Phoenix Channels and GenServers to handle all client interactions&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="88">        <span style="color: #b8bb26;">&quot;Used PicoCSS for UI styling&quot;</span>
</div><div class="line" data-line="89">      <span style="color: #fe8019;">]</span>
</div><div class="line" data-line="90">    <span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="91">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Project</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="92">      <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Bulls&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="93">      <span style="color: #83a598;">description: </span><span style="color: #b8bb26;">&quot;&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="94">      <span style="color: #83a598;">completed_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2025-05-13<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="95">      <span style="color: #83a598;">skills: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="96">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Elixir&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="97">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;Phoenix&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="98">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;LiveView&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="99">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Skill</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;TailwindCSS&quot;</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="100">      <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="101">      <span style="color: #83a598;">details: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="102">        <span style="color: #b8bb26;">&quot;Built an realtime multiplayer number guessing game&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="103">        <span style="color: #b8bb26;">&quot;Emphasizes idiomatic Elixir architecture and clean, component-driven design&quot;</span>
</div><div class="line" data-line="104">      <span style="color: #fe8019;">]</span>
</div><div class="line" data-line="105">    <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="106">  <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="107">  <span style="color: #83a598;">work_experiences: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="108">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">WorkExperience</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="109">      <span style="color: #83a598;">organization: </span><span style="color: #b8bb26;">&quot;Some Company&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="110">      <span style="color: #83a598;">location: </span><span style="color: #b8bb26;">&quot;Sometown, CA&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="111">      <span style="color: #83a598;">position: </span><span style="color: #b8bb26;">&quot;Software Engineer I, II&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="112">      <span style="color: #83a598;">start_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2021-12-01<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="113">      <span style="color: #83a598;">end_date: </span><span style="color: #fe8019;">nil</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="114">      <span style="color: #83a598;">details: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="115">        <span style="color: #b8bb26;">&quot;Held a software engineering position on the Software Team&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="116">        <span style="color: #b8bb26;">&quot;Developed software features, investigated operational issues, and fixed software problem reports&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="117">        <span style="color: #b8bb26;">&quot;Oversaw cybersecurity, ensuring compliance with industry-standard practices and hardening all network access points&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="118">      <span style="color: #fe8019;">]</span>
</div><div class="line" data-line="119">    <span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="120">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">WorkExperience</span><span style="color: #fe8019;">&lbrace;</span>
</div><div class="line" data-line="121">      <span style="color: #83a598;">organization: </span><span style="color: #b8bb26;">&quot;Another Company&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="122">      <span style="color: #83a598;">location: </span><span style="color: #b8bb26;">&quot;Sometown, ID&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="123">      <span style="color: #83a598;">position: </span><span style="color: #b8bb26;">&quot;Full Stack Software Development Intern&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="124">      <span style="color: #83a598;">start_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2020-08-01<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="125">      <span style="color: #83a598;">end_date: </span><span style="color: #fe8019;">~</span>D<span style="color: #fe8019;">[</span>2020-12-01<span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="126">      <span style="color: #83a598;">details: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="127">        <span style="color: #b8bb26;">&quot;Developed core gameplay and UI features for a multiplayer party game built with React Native&quot;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="128">        <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Link</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;App selected as Apple App Store App of the Day in December 2022&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">url: </span><span style="color: #b8bb26;">&quot;https://example.com&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="129">        <span style="color: #b8bb26;">&quot;Practiced Behavior Driven Development and wrote integration tests using Jest&quot;</span>
</div><div class="line" data-line="130">      <span style="color: #fe8019;">]</span>
</div><div class="line" data-line="131">    <span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="132">  <span style="color: #fe8019;">]</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="133">  <span style="color: #83a598;">social_links: </span><span style="color: #fe8019;">[</span>
</div><div class="line" data-line="134">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Link</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">url: </span><span style="color: #b8bb26;">&quot;https://github.com/my_account&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;GitHub&quot;</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="135">    <span style="color: #fe8019;">%</span><span style="color: #ebdbb2;">Link</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">url: </span><span style="color: #b8bb26;">&quot;https://linkedin.com/in/my_account&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">name: </span><span style="color: #b8bb26;">&quot;LinkedIn&quot;</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="136">  <span style="color: #fe8019;">]</span>
</div><div class="line" data-line="137"><span style="color: #fe8019;">&rbrace;</span>
</div></code></pre>
<p>And with that, we've represented a resume in pure Elixir that is ready to be version-controlled and used in our LiveView; no database needed. With the Model done, now we move on to the LiveView.</p>
<h2><a href="#generating-my-liveviews" aria-hidden="true" class="anchor" id="generating-my-liveviews"></a>Generating my LiveViews</h2>
<p>At this point, I knew that I would need two LiveViews: one that would index all of my available resumes, and another that would show a resume. I could bootstrap both quickly with Phoenix's generator <code>mix phx.gen.live Resumes Resume resumes throwaway:string</code> rather than writing all of this myself. I used a generator even though I didn't have a database or Ecto schemas, mainly to get the preliminary context and LiveView starting point. The <code>throwaway</code> field was added because the generator requires it, but it was easy to remove all database-related code afterward. I also did this so that if the app's needs grew or I decided to use a database backend, the necessary scaffolding would already be in place. With that done, I had my routes and initial LiveViews set up. My router looked like this:</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #b8bb26; font-weight: bold;">scope</span> <span style="color: #b8bb26;">&quot;/&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">LiveResumeWeb</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="2">  <span style="color: #b8bb26; font-weight: bold;">pipe_through</span> <span style="color: #83a598;">:browser</span>
</div><div class="line" data-line="3">
</div><div class="line" data-line="4">  <span style="color: #928374;"># get &quot;/&quot;, PageController, :home</span>
</div><div class="line" data-line="5">  <span style="color: #b8bb26; font-weight: bold;">live_session</span> <span style="color: #83a598;">:default</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="6">    <span style="color: #b8bb26; font-weight: bold;">live</span> <span style="color: #b8bb26;">&quot;/&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">ResumeLive.Index</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:index</span>
</div><div class="line" data-line="7">    <span style="color: #b8bb26; font-weight: bold;">live</span> <span style="color: #b8bb26;">&quot;/resumes/:id&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">ResumeLive.Show</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:show</span>
</div><div class="line" data-line="8">    <span style="color: #928374;"># live &quot;/resumes/new&quot;, ResumeLive.Form, :new</span>
</div><div class="line" data-line="9">    <span style="color: #928374;"># live &quot;/resumes/:id/edit&quot;, ResumeLive.Form, :edit</span>
</div><div class="line" data-line="10">  <span style="color: #fb4934;">end</span>
</div><div class="line" data-line="11"><span style="color: #fb4934;">end</span>
</div></code></pre>
<h2><a href="#customizing-the-liveviews" aria-hidden="true" class="anchor" id="customizing-the-liveviews"></a>Customizing the LiveViews</h2>
<p>Phoenix uses 'layouts' to separate common page structure out of different views. This made it really easy to get started designing my pages. In fact, I mostly kept the initial <code>app/1</code> layout in <code>layouts.ex</code> as is with a few edits. All I needed to do on my index page is list all resumes.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #fb4934;">defmodule</span> <span style="color: #ebdbb2;">LiveResumeWeb.ResumeLive.Index</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="2">  <span style="color: #fb4934;">use</span> <span style="color: #ebdbb2;">LiveResumeWeb</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:live_view</span>
</div><div class="line" data-line="3">
</div><div class="line" data-line="4">  <span style="color: #fb4934;">alias</span> <span style="color: #ebdbb2;">LiveResume.Resumes</span>
</div><div class="line" data-line="5">
</div><div class="line" data-line="6">  <span style="color: #d3869b;">@</span><span style="color: #d3869b;">impl </span><span style="color: #d3869b;">true</span>
</div><div class="line" data-line="7">  <span style="color: #fb4934;">def</span> <span style="color: #b8bb26; font-weight: bold;">mount</span><span style="color: #fe8019;">(</span><span style="color: #928374;">_params</span><span style="color: #fe8019;">,</span> <span style="color: #928374;">_session</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">)</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="8">    <span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">:ok</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="9">     <span style="color: #ebdbb2;">socket</span>
</div><div class="line" data-line="10">     <span style="color: #fe8019;">|&gt;</span> <span style="color: #b8bb26; font-weight: bold;">assign</span><span style="color: #fe8019;">(</span><span style="color: #83a598;">:page_title</span><span style="color: #fe8019;">,</span> <span style="color: #b8bb26;">&quot;All Resumes&quot;</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="11">     <span style="color: #fe8019;">|&gt;</span> <span style="color: #b8bb26; font-weight: bold;">assign</span><span style="color: #fe8019;">(</span><span style="color: #83a598;">:resume_files</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">Resumes</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">list_resumes_friendly</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="12">  <span style="color: #fb4934;">end</span>
</div><div class="line" data-line="13">
</div><div class="line" data-line="14"><span style="color: #fb4934;">end</span>
</div></code></pre>
<p>Now that our index LiveView has an assigns of <code>@resume_files</code> , I just need to render them. Here is my thin home page:</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;"># index.html.heex</span>
</div><div class="line" data-line="2"><span style="color: #fe8019;">&lt;</span><span style="color: #ebdbb2;">Layouts</span><span style="color: #fe8019;">.</span><span style="color: #ebdbb2;">app</span> <span style="color: #ebdbb2;">flash</span>=<span style="color: #fe8019;">&lbrace;</span><span style="color: #d3869b;">@</span><span style="color: #d3869b;">flash</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="3">  <span style="color: #fe8019;">&lt;</span>.<span style="color: #ebdbb2;">header</span><span style="color: #fe8019;">&gt;</span><span style="color: #ebdbb2;">All</span> Resumes&lt;<span style="color: #fe8019;">/</span><span style="color: #fe8019;">.</span><span style="color: #ebdbb2;">header</span>&gt;
</div><div class="line" data-line="4">  <span style="color: #fe8019;">&lt;</span><span style="color: #ebdbb2;">ul</span> <span style="color: #ebdbb2;">class</span>=<span style="color: #b8bb26;">&quot;menu menu-lg bg-base-200 rounded-box w-full&quot;</span>&gt;
</div><div class="line" data-line="5">    <span style="color: #fe8019;">&lt;</span><span style="color: #fe8019;">%</span><span style="color: #fe8019;">=</span> <span style="color: #ebdbb2;">for</span> <span style="color: #ebdbb2;">resume_file</span> <span style="color: #fe8019;">&lt;-</span> <span style="color: #d3869b;">@</span><span style="color: #d3869b;">resume_files</span> <span style="color: #fb4934;">do</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="6">      &lt;<span style="color: #ebdbb2;">li</span> <span style="color: #ebdbb2;">class</span>=<span style="color: #b8bb26;">&quot;flex flex-row items-center&quot;</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="7">        <span style="color: #fe8019;">&lt;</span>.<span style="color: #b8bb26; font-weight: bold;">link</span> <span style="color: #ebdbb2;">navigate</span><span style="color: #fe8019;">=</span><span style="color: #fe8019;">&lbrace;</span><span style="color: #fe8019;">~</span>p&quot;/resumes/<span style="color: #fe8019;">#&lbrace;</span><span style="color: #ebdbb2;">resume_file</span><span style="color: #fe8019;">&rbrace;</span>&quot;<span style="color: #fe8019;">&rbrace;</span> class=<span style="color: #b8bb26;">&quot;grow&quot;</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="8">          <span style="color: #fe8019;">&lt;</span>.<span style="color: #b8bb26; font-weight: bold;">icon</span> <span style="color: #ebdbb2;">name</span><span style="color: #fe8019;">=</span><span style="color: #b8bb26;">&quot;hero-document-text&quot;</span> class=<span style="color: #b8bb26;">&quot;size-6&quot;</span> <span style="color: #fe8019;">/</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="9">          <span style="color: #fe8019;">&lbrace;</span><span style="color: #ebdbb2;">resume_file</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="10">        <span style="color: #fe8019;">&lt;</span><span style="color: #fe8019;">/</span>.<span style="color: #ebdbb2;">link</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="11">      <span style="color: #fe8019;">&lt;</span>/<span style="color: #ebdbb2;">li</span>&gt;
</div><div class="line" data-line="12">    <span style="color: #fe8019;">&lt;</span><span style="color: #fe8019;">%</span> <span style="color: #ebdbb2;">end</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="13">  <span style="color: #fe8019;">&lt;</span>/<span style="color: #ebdbb2;">ul</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="14"><span style="color: #fe8019;">&lt;</span>/<span style="color: #ebdbb2;">Layouts</span><span style="color: #fe8019;">.</span><span style="color: #ebdbb2;">app</span>&gt;
</div></code></pre>
<p>It looks like this. Simple and clean, just what I was going for.</p>
<p><img src="/media/live-resume-index.png" alt="LiveResume index listing available resume files" /></p>
<p><em>My LiveResume home page.</em></p>
<h2><a href="#building-the-main-resume-liveview" aria-hidden="true" class="anchor" id="building-the-main-resume-liveview"></a>Building the Main Resume LiveView</h2>
<p>Now it's time for the core of the entire app: the Resume LiveView. My vision was to enable myself to design my resume as a HEEx template. I've grown to really enjoy the Phoenix workflow of making template edits and seeing the changes refresh live. It's fast, effective, and fun. What I've learned over the years writing and editing my resume in documents and word editors is that they can be clunky at times and it's hard to get everything the way you want without researching each editor's specific way of doing things. As a Full Stack developer, the design language I know and use frequently is HTML and CSS. I don't want to learn how to format a document in MS Word, Google Docs, or LibreOffice individually. With DaisyUI and TailwindCSS, I could control every aspect of the display in a familiar way.</p>
<p>The one issue I kept turning over in my mind was how I would export the resume when finished. Ideally, I would like to export to PDF, but after doing some searching, I found that I would need some external dependencies for that and I did not want to go that route at this time. My solution to this is that when finished, I would just print the webpage and use the print controls to save the final resume as a PDF. The option to 'Save to PDF' when printing exists on all systems and the printing options are decently sufficient for controlling margins and other paging idiosyncrasies.</p>
<p>With my user-story in mind, now I could start implementing my dream resume builder. I wanted to take advantage of Phoenix's layout system to create swappable templates for my resume content. I wanted to use separate HEEx templates per resume layout, but how could I render different HEEx templates dynamically when the <code>:show</code> Live action can only render one template? My solution to this issue was inspired by how Phoenix itself handles routes in the application.</p>
<h2><a href="#dynamically-rendering-templates-within-a-live-action" aria-hidden="true" class="anchor" id="dynamically-rendering-templates-within-a-live-action"></a>Dynamically Rendering Templates within a Live Action</h2>
<p>If you've ever noticed at the very bottom of your web context, the one named <code>&lt;MyApp&gt;Web</code>, there is a small but powerful function that sits at the heart of routing in our application.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;">@</span><span style="color: #928374;">doc</span> <span style="color: #928374;">&quot;&quot;&quot;
</div><div class="line" data-line="2">When used, dispatch to the appropriate controller/live_view/etc.
</div><div class="line" data-line="3">&quot;&quot;&quot;</span>
</div><div class="line" data-line="4"><span style="color: #fb4934;">defmacro</span> <span style="color: #b8bb26; font-weight: bold;">__using__</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">which</span><span style="color: #fe8019;">)</span> <span style="color: #fb4934;">when</span> <span style="color: #b8bb26; font-weight: bold;">is_atom</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">which</span><span style="color: #fe8019;">)</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="5">  <span style="color: #b8bb26; font-weight: bold;">apply</span><span style="color: #fe8019;">(</span><span style="color: #fe8019;">__MODULE__</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">which</span><span style="color: #fe8019;">,</span> <span style="color: #fe8019;">[</span><span style="color: #fe8019;">]</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="6"><span style="color: #fb4934;">end</span>
</div></code></pre>
<p><a href="https://hexdocs.pm/elixir/main/Kernel.html#apply/2"><code>Kernel.apply/2</code></a> calls a function from a module with arguments. Phoenix uses this to figure out which template to render based on the actions defined in the router. We can reuse this idea for our resume templates.</p>
<p>First, I added a directory specifically for resume layouts in <code>components/layouts/resumes</code> which would hold each resume template.</p>
<p><img src="/media/live-resume-templates.png" alt="LiveResume source tree showing embedded resume layout templates" /></p>
<p>I then created a small wrapper <code>ResumeLayouts</code> module for interfacing with it.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #fb4934;">defmodule</span> <span style="color: #ebdbb2;">LiveResumeWeb.ResumeLayouts</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="2">  <span style="color: #928374;">@</span><span style="color: #928374;">moduledoc</span> <span style="color: #928374;">&quot;&quot;&quot;
</div><div class="line" data-line="3">  This module holds the Resume layouts.
</div><div class="line" data-line="4">
</div><div class="line" data-line="5">  See the `layouts/resumes` directory for all templates available.
</div><div class="line" data-line="6">  &quot;&quot;&quot;</span>
</div><div class="line" data-line="7">  <span style="color: #fb4934;">use</span> <span style="color: #ebdbb2;">LiveResumeWeb</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:html</span>
</div><div class="line" data-line="8">
</div><div class="line" data-line="9">  <span style="color: #b8bb26; font-weight: bold;">embed_templates</span> <span style="color: #b8bb26;">&quot;layouts/resumes/*&quot;</span>
</div><div class="line" data-line="10"><span style="color: #fb4934;">end</span>
</div></code></pre>
<p>Now, each template defined in <code>components/layouts/resumes</code> is a function inside the <code>LiveResumeWeb.ResumeLayouts</code> module. Since the template name comes from a URL parameter, I use an explicit function allowlist instead of converting arbitrary input to an atom.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;"># show.html.heex</span>
</div><div class="line" data-line="2"><span style="color: #fe8019;">&lt;</span><span style="color: #fe8019;">%</span><span style="color: #fe8019;">=</span> <span style="color: #ebdbb2;">if</span> <span style="color: #fe8019;">!</span><span style="color: #d3869b;">@</span><span style="color: #d3869b;">resume</span> <span style="color: #fb4934;">do</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="3">  &lt;L<span style="color: #ebdbb2;">ayouts</span><span style="color: #fe8019;">.</span><span style="color: #ebdbb2;">app</span> <span style="color: #83a598;">:if</span>=<span style="color: #fe8019;">&lbrace;</span><span style="color: #fe8019;">!</span><span style="color: #d3869b;">@</span><span style="color: #d3869b;">resume</span><span style="color: #fe8019;">&rbrace;</span> flash=<span style="color: #fe8019;">&lbrace;</span><span style="color: #d3869b;">@</span><span style="color: #d3869b;">flash</span><span style="color: #fe8019;">&rbrace;</span><span style="color: #fe8019;">&gt;</span><span style="color: #fe8019;">&lt;</span>/<span style="color: #ebdbb2;">Layouts</span><span style="color: #fe8019;">.</span><span style="color: #ebdbb2;">app</span>&gt;
</div><div class="line" data-line="4"><span style="color: #fe8019;">&lt;</span><span style="color: #fe8019;">%</span> <span style="color: #ebdbb2;">else</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">&gt;</span>
</div><div class="line" data-line="5">  <span style="color: #fe8019;">&lbrace;</span><span style="color: #b8bb26; font-weight: bold;">render_template</span><span style="color: #fe8019;">(</span><span style="color: #d3869b;">@</span><span style="color: #d3869b;">template</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">assigns</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="6">&lt;<span style="color: #fe8019;">%</span> <span style="color: #ebdbb2;">end</span> <span style="color: #fe8019;">%</span><span style="color: #fe8019;">&gt;</span>
</div></code></pre>
<p>But where did the <code>@template</code> assign come from? I parse it out from the URL as a query parameter in every request to the LiveView and store it in the socket.</p>
<pre class="lumis" style="color: #ebdbb2; background-color: #1d2021;"><code class="language-elixir" translate="no" tabindex="0"><div class="line" data-line="1"><span style="color: #928374;"># show.ex</span>
</div><div class="line" data-line="2"><span style="color: #d3869b;">@</span><span style="color: #d3869b;">impl </span><span style="color: #d3869b;">true</span>
</div><div class="line" data-line="3"><span style="color: #fb4934;">def</span> <span style="color: #b8bb26; font-weight: bold;">handle_params</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">params</span><span style="color: #fe8019;">,</span> <span style="color: #928374;">_uri</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">)</span> <span style="color: #fb4934;">do</span>
</div><div class="line" data-line="4">  <span style="color: #ebdbb2;">template</span> <span style="color: #fe8019;">=</span> <span style="color: #ebdbb2;">Map</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">get</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">params</span><span style="color: #fe8019;">,</span> <span style="color: #b8bb26;">&quot;template&quot;</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="5">
</div><div class="line" data-line="6">  <span style="color: #fe8019;">&lbrace;</span><span style="color: #83a598;">:noreply</span><span style="color: #fe8019;">,</span>
</div><div class="line" data-line="7">    <span style="color: #ebdbb2;">socket</span>
</div><div class="line" data-line="8">    <span style="color: #fe8019;">|&gt;</span> <span style="color: #b8bb26; font-weight: bold;">set_template</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">template</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">&rbrace;</span>
</div><div class="line" data-line="9"><span style="color: #fb4934;">end</span>
</div><div class="line" data-line="10">
</div><div class="line" data-line="11"><span style="color: #fb4934;">defp</span> <span style="color: #b8bb26; font-weight: bold;">set_template</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">,</span> <span style="color: #fe8019;">nil</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">do: </span><span style="color: #b8bb26; font-weight: bold;">assign</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:template</span><span style="color: #fe8019;">,</span> <span style="color: #b8bb26;">&quot;default&quot;</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="12"><span style="color: #fb4934;">defp</span> <span style="color: #b8bb26; font-weight: bold;">set_template</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">template</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">do: </span><span style="color: #b8bb26; font-weight: bold;">assign</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">socket</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">:template</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">template</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="13">
</div><div class="line" data-line="14"><span style="color: #fb4934;">defp</span> <span style="color: #b8bb26; font-weight: bold;">render_template</span><span style="color: #fe8019;">(</span><span style="color: #b8bb26;">&quot;test&quot;</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">assigns</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">do: </span><span style="color: #ebdbb2;">LiveResumeWeb.ResumeLayouts</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">test</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">assigns</span><span style="color: #fe8019;">)</span>
</div><div class="line" data-line="15"><span style="color: #fb4934;">defp</span> <span style="color: #b8bb26; font-weight: bold;">render_template</span><span style="color: #fe8019;">(</span><span style="color: #928374;">_template</span><span style="color: #fe8019;">,</span> <span style="color: #ebdbb2;">assigns</span><span style="color: #fe8019;">)</span><span style="color: #fe8019;">,</span> <span style="color: #83a598;">do: </span><span style="color: #ebdbb2;">LiveResumeWeb.ResumeLayouts</span><span style="color: #fe8019;">.</span><span style="color: #b8bb26; font-weight: bold;">default</span><span style="color: #fe8019;">(</span><span style="color: #ebdbb2;">assigns</span><span style="color: #fe8019;">)</span>
</div></code></pre>
<p>The fallback clause handles unknown template names without creating atoms from user-controlled input. A simple test in the browser showed that dynamic resume template rendering was working, so I could continue building the templates and editor UI.</p>
<h2><a href="#building-out-the-ui" aria-hidden="true" class="anchor" id="building-out-the-ui"></a>Building Out the UI</h2>
<p>Rendering the resume in the template is where the real work takes place, so I took some time to carefully design a resume that I liked and worked for me. There's not much to say about it since it is just styling with DaisyUI and TailwindCSS. Here's how our test resume from above looks like using the default template I designed:</p>
<p><img src="/media/live-resume-light.png" alt="LiveResume sample resume in the light theme" /></p>
<p>Here's what it looks like in dark mode:</p>
<p><img src="/media/live-resume-dark.png" alt="LiveResume sample resume in the dark theme" /></p>
<p>It's nice to be able to build out your resume using the stack you know and love and seeing updates in real-time as the whole thing comes together as each component gets built.</p>
<p>I added a drawer UI component from DaisyUI to hold the theme-changer, template selection, and other settings. This way, the main resume design experience is kept clean with nothing in the way so I can focus on designing my resume.</p>
<p><img src="/media/live-resume-settings-button.png" alt="LiveResume interface with the settings button highlighted" /></p>
<p><img src="/media/live-resume-settings-drawer.png" alt="LiveResume settings drawer with template and theme controls" /></p>
<p>The 'Reload Resume' button re-evaluates our resume <code>.exs</code> file in case we made any content changes to it.</p>
<h2><a href="#exporting" aria-hidden="true" class="anchor" id="exporting"></a>Exporting</h2>
<p>With my resume done, I'm ready to export to PDF or print if I want. Using the browser's print dialog, I can adjust the page settings before saving to PDF. I like to set the margins to 0.5 inches.</p>
<p><img src="/media/live-resume-print-preview.png" alt="Browser print preview of a LiveResume document" /></p>
<p>You may have noticed that the icons are missing in the final exported resume. This is because the hero icons included in Phoenix by default are SVGs that are not inlined but are instead fetched. When the browser tries to print (i.e. export) the page, since the SVGs are not included in the markup inline, they are skipped. There are some workarounds to this which I left for another day.</p>
<h2><a href="#challenges-and-reflections" aria-hidden="true" class="anchor" id="challenges-and-reflections"></a>Challenges and Reflections</h2>
<p>Throughout the project, there were a few challenges that needed tailored solutions. They included</p>
<ul>
<li>Navigating LiveView layout overrides</li>
<li>Making the UI printable without compromising design</li>
<li>Building a flexible component-based system for layouts</li>
<li>Balancing developer UX and maintainability</li>
</ul>
<p>In overcoming each of them, I tried to balance simplicity and extensibility. In case I want to take the project further by adding new resume fields, designing new resume templates, or using a database for persistence, I have the ability do so fairly easily without breaking existing features or templates.</p>
<h2><a href="#wrapping-up" aria-hidden="true" class="anchor" id="wrapping-up"></a>Wrapping Up</h2>
<p>Right now, this project is closed source, mainly because I am keeping my resume data with personal information alongside the application code for developer convenience. However, in the future, I might abstract the resume parsing and layout features into a separate library that could be released as open source for other developers. I am also working on solving the inline SVG issue during resume export.</p>
<p>LiveResume solved a real problem for me and it's how I write, design, and version my resumes from now on. I'm really enjoying designing my resumes in it especially with the tight feedback loop of Phoenix live reloading and endless customization options with DaisyUI and TailwindCSS.</p>
<p>I'm working on some other projects right now as well. Stay tuned for more app breakdowns.</p> ]]></description>
    </item>
  </channel>
</rss>
