<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Nick Stalter | Lead SeniorSoftware Engineer</title>
    <description>Nickstalter.com is a blog focused website where I share my insights, frustrations, and anything else I discover in the development world.
</description>
    <link>http://nickstalter.com/</link>
    <atom:link href="http://nickstalter.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Wed, 18 Feb 2026 12:26:17 +0000</pubDate>
    <lastBuildDate>Wed, 18 Feb 2026 12:26:17 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Phoenix LiveView: Offline-ish list with localStorage and a follow-up event</title>
        <description>&lt;p&gt;On mobile, locking the screen often drops the WebSocket or suspends the tab. When the user unlocks and returns to your LiveView, the process may have been torn down and &lt;strong&gt;mount runs again&lt;/strong&gt;. If mount always shows a loading spinner and refetches from the server, the user sees a full refresh every time they check their list—annoying when they’re in the store and opening the app every few minutes.&lt;/p&gt;

&lt;p&gt;This post shows a small pattern to make a list (or any data-heavy view) feel “offline-ish”: &lt;strong&gt;cache a serializable snapshot in localStorage&lt;/strong&gt;, have the client &lt;strong&gt;push it to the server in a follow-up event&lt;/strong&gt; (not in connect params—that can blow the URI length limit), and &lt;strong&gt;show it immediately&lt;/strong&gt; while still refetching in the background. If decode or staleness check fails, you fall back to a normal load.&lt;/p&gt;

&lt;p&gt;I used this for the shopping list in &lt;a href=&quot;https://onrotation.app&quot;&gt;OnRotation&lt;/a&gt;; the same idea applies to any LiveView that remounts often on mobile.&lt;/p&gt;

&lt;h2 id=&quot;1-cache-shape&quot;&gt;1. Cache shape&lt;/h2&gt;

&lt;p&gt;Store a JSON object with whatever the template needs to render, plus a timestamp for staleness:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entries&lt;/code&gt; – array of items (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;display_name&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quantity&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;category&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checked&lt;/code&gt;, …)&lt;/li&gt;
  &lt;li&gt;Any other assigns the template uses (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selected_week_start&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_meal_names&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_key_to_meal_names&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cached_at&lt;/code&gt; – ISO8601 timestamp so the server can ignore cache older than your TTL (e.g. 15 minutes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a single key per origin (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myapp_shopping_list&lt;/code&gt;) if you have one list per user.&lt;/p&gt;

&lt;h2 id=&quot;2-server-push-cache-after-loading-or-updating&quot;&gt;2. Server: push cache after loading or updating&lt;/h2&gt;

&lt;p&gt;Whenever you assign the list (or related) data, push a serializable payload to the client so the hook can write it to localStorage. No structs or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DateTime&lt;/code&gt; in the payload—plain maps and strings.&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;build_cache_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;entries&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entry_to_cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;selected_week_start&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;selected_week_start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_iso8601&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;selected_week_start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;cached_at&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;utc_now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_iso8601&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ... other keys the template needs&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;push_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;push_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cache_shopping_list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;build_cache_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_cache(socket)&lt;/code&gt; at the end of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_info(:load_plan, socket)&lt;/code&gt; and after every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_event&lt;/code&gt; that updates the list (toggle, add, delete, etc.).&lt;/p&gt;

&lt;h2 id=&quot;3-client-hook-to-write-cache&quot;&gt;3. Client: hook to write cache&lt;/h2&gt;

&lt;p&gt;A hook that listens for the event and stores the payload:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CACHE_KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;myapp_shopping_list&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ShoppingListCacheHook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;mounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handleEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;cache_shopping_list&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CACHE_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Mount the hook on an element in your LiveView (e.g. a hidden div so it’s present when the list is rendered).&lt;/p&gt;

&lt;h2 id=&quot;4-client-send-cache-in-a-follow-up-event-not-connect-params&quot;&gt;4. Client: send cache in a follow-up event (not connect params)&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not&lt;/strong&gt; put the cache in LiveSocket’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt;. Those end up in the connection URL and can hit &lt;strong&gt;URI length limits&lt;/strong&gt; (often 4–8KB), causing “request URI too long” errors and even infinite redirect/error loops when the list is large.&lt;/p&gt;

&lt;p&gt;Instead, have the same hook that writes the cache &lt;strong&gt;push an event&lt;/strong&gt; to the server when it mounts. The payload goes in the WebSocket message body, so size is not an issue:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ShoppingListCacheHook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;mounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handleEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;cache_shopping_list&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CACHE_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Send cache to server in a follow-up event to avoid URI length limit&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cached&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CACHE_KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cached&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pushEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;restore_cached_list&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cached&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Keep &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params: { _csrf_token: csrfToken }&lt;/code&gt; (or a simple static object); no cache there.&lt;/p&gt;

&lt;h2 id=&quot;5-server-apply-cache-when-the-event-arrives&quot;&gt;5. Server: apply cache when the event arrives&lt;/h2&gt;

&lt;p&gt;Handle the event: if you’re still loading and the cache is valid, apply it and set your loading flag to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;. You still run the refetch (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;send(self(), :load_plan)&lt;/code&gt; in mount); when it completes, assign fresh data and call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_cache&lt;/code&gt; again.&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;@cache_ttl_seconds&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;900&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 15 minutes&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assign_defaults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:load_plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;restore_cached_list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cache&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plan_loading?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;try_apply_decoded_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:plan_loading?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;:skip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;restore_cached_list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;try_apply_decoded_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Jason&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache_fresh?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply_cached_assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)},&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:skip&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:skip&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache_fresh?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cached_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iso&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iso&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_iso8601&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iso&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;utc_now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;@cache_ttl_seconds&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Decode and apply: build &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entries&lt;/code&gt; (and any derived assigns like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entries_by_section&lt;/code&gt;) from the decoded map. If anything fails (decode, missing keys, bad types), leave the socket as-is and let the refetch show the real data. No need for elaborate validation—fail fast and fall back.&lt;/p&gt;

&lt;h2 id=&quot;edge-cases&quot;&gt;Edge cases&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;No cache&lt;/strong&gt; – Behave as today: loading spinner, then load.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stale cache&lt;/strong&gt; – Ignore if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cached_at&lt;/code&gt; is older than your TTL; load as normal.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Decode / structure error&lt;/strong&gt; – Ignore cache and refetch. Defensive decode keeps bad or tampered data from breaking the view; the server remains the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;security-note&quot;&gt;Security note&lt;/h2&gt;

&lt;p&gt;The cache is only for UX: show something immediately. The server always re-fetches and scopes data by the authenticated session. Don’t trust the cache for authorization. For a shopping list or similar, storing it in localStorage is fine; for more sensitive data, consider whether you want it on the client at all.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;After each load or mutation, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_event(..., &quot;cache_...&quot;, payload)&lt;/code&gt; with a serializable snapshot.&lt;/li&gt;
  &lt;li&gt;A JS hook writes that payload to localStorage and, on mount, reads it and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pushEvent(&quot;restore_cached_list&quot;, { cache })&lt;/code&gt; so the server gets it in a &lt;strong&gt;WebSocket message&lt;/strong&gt; (not connect params—avoids URI length limits).&lt;/li&gt;
  &lt;li&gt;Server handles &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;restore_cached_list&quot;&lt;/code&gt;: if still loading and cache is valid (decode + TTL), apply it and set loading to false.&lt;/li&gt;
  &lt;li&gt;Mount always triggers a refetch; when it completes, assign fresh data and push the new cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a short TTL (e.g. 15 minutes), users get an instant list when they reopen the app after a brief lock, and the list still refreshes in the background so it stays accurate.&lt;/p&gt;

&lt;p&gt;For more on hooks and storage, see the earlier post &lt;a href=&quot;https://distortia.github.io/2022/04/14/phoenix-liveview-with-localstorage-or-sessionstorage.html&quot;&gt;Phoenix LiveView with localStorage or sessionStorage&lt;/a&gt;; this one focuses on &lt;strong&gt;reconnect/mount&lt;/strong&gt; and sending the cache in a &lt;strong&gt;follow-up pushEvent&lt;/strong&gt; (not connect params) to avoid both the full refetch and URI length limits.&lt;/p&gt;
</description>
        <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/phoenix-liveview-offline-ish-reconnect-cache</link>
        <guid isPermaLink="true">http://nickstalter.com/phoenix-liveview-offline-ish-reconnect-cache</guid>
        
        <category>phoenix</category>
        
        <category>liveview</category>
        
        <category>elixir</category>
        
        <category>localStorage</category>
        
        <category>reconnect</category>
        
        <category>mobile</category>
        
        
        <category>engineering</category>
        
        <category>elixir</category>
        
        <category>phoenix</category>
        
      </item>
    
      <item>
        <title>OnRotation: Technical Architecture of a Phoenix LiveView Meal Planning App</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://onrotation.app&quot;&gt;OnRotation&lt;/a&gt; is a weekly meal planning app built with Phoenix and LiveView. I’ve written about the product story before; this post focuses on the technical architecture: how the app is structured, how data flows, and the patterns we rely on to keep it simple and maintainable.&lt;/p&gt;

&lt;h2 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Phoenix 1.8&lt;/strong&gt; with &lt;strong&gt;LiveView&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ecto + PostgreSQL&lt;/strong&gt; for persistence&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; for styling (CSS variables, no config file)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Req&lt;/strong&gt; for HTTP (no HTTPoison/Tesla)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Resend&lt;/strong&gt; for transactional email (magic links, invites)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sentry&lt;/strong&gt; for error tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quality tooling: &lt;strong&gt;Credo&lt;/strong&gt;, &lt;strong&gt;Dialyzer&lt;/strong&gt;, &lt;strong&gt;SoBelow&lt;/strong&gt; (security), all wired into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix precommit&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h2&gt;

&lt;p&gt;The app follows a context-based structure. Core domains:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Accounts&lt;/strong&gt; – users, households, invites, magic-link auth&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Meals&lt;/strong&gt; – meal library, curated meals, meal packs&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plans&lt;/strong&gt; – weekly plans and plan entries&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ShoppingLists&lt;/strong&gt; – per-household shopping list with plan-derived items&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blog&lt;/strong&gt; – public blog with Earmark-based body processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost all domain data is scoped by &lt;strong&gt;household&lt;/strong&gt;. A user belongs to one household; meals, plans, and shopping lists are all keyed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;household_id&lt;/code&gt;. That keeps queries straightforward and authorization simple.&lt;/p&gt;

&lt;h2 id=&quot;the-scope-pattern&quot;&gt;The Scope Pattern&lt;/h2&gt;

&lt;p&gt;We use an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OnRotation.Accounts.Scope&lt;/code&gt; struct to represent the caller’s context:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;user:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;household:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;phx.gen.auth&lt;/code&gt; gives us &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_scope&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt;. The scope carries both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;household&lt;/code&gt;, so controllers and LiveViews can consistently pass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_scope&lt;/code&gt; into contexts. Context functions take &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scope&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;household_id&lt;/code&gt; as the first argument and scope all queries by household.&lt;/p&gt;

&lt;p&gt;This avoids leaking user-only logic into contexts and keeps “who is acting” and “on which household” explicit.&lt;/p&gt;

&lt;h2 id=&quot;routing-and-pipelines&quot;&gt;Routing and Pipelines&lt;/h2&gt;

&lt;p&gt;We use separate pipelines and live sessions for different auth levels:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Public&lt;/strong&gt; – marketing, blog, sitemap, robots (browser_and_seo)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Auth routes&lt;/strong&gt; – log-in (magic link), registration&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Authenticated&lt;/strong&gt; – plan, meals, shopping list, feedback (live_session :authenticated)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blog admin&lt;/strong&gt; – create/edit blog posts (require_blog_author)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Meal pack admin&lt;/strong&gt; – manage curated meal packs (require_meal_pack_admin)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Admin&lt;/strong&gt; – metrics dashboard (require_admin)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;API pipeline&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/v1&lt;/code&gt;) accepts JSON and uses Bearer token auth. It’s used by the Android app; web users stay in LiveView.&lt;/p&gt;

&lt;h2 id=&quot;authentication&quot;&gt;Authentication&lt;/h2&gt;

&lt;p&gt;We’re privacy-focused: no passwords, magic links only, and Simple Analytics for basic analytics instead of tracking-heavy alternatives. No cross-site tracking, no ad networks.&lt;/p&gt;

&lt;p&gt;Web users sign in via magic links. The flow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;User enters email.&lt;/li&gt;
  &lt;li&gt;We create or find the user and send a tokenized link (Resend).&lt;/li&gt;
  &lt;li&gt;User clicks the link; we validate the token and establish a session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the mobile API:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Client calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /api/v1/auth/magic-link&lt;/code&gt; with email.&lt;/li&gt;
  &lt;li&gt;User receives the link and opens it in a browser; we redirect to a page that calls the API to exchange the token for a Bearer token.&lt;/li&gt;
  &lt;li&gt;Client stores the Bearer token and sends it on subsequent requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both flows end up with a session token; web uses cookies, API uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;data-model&quot;&gt;Data Model&lt;/h2&gt;

&lt;p&gt;The main entities:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Household&lt;/strong&gt; – one per account; owns meals, plans, shopping list&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Meal&lt;/strong&gt; – name, recipe_url, main_ingredients, extras, tags, complexity (1–3)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt; – one per week per household (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;week_start_date&lt;/code&gt; = Monday)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PlanEntry&lt;/strong&gt; – links plan to meal, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;day_index&lt;/code&gt; (0=Mon … 6=Sun) and position for multiple meals per day&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ShoppingListEntry&lt;/strong&gt; – per-household; entries come from plan merge or manual “extras”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meals, plans, and shopping lists are always queried with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;household_id&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;h2 id=&quot;shopping-list-plan-merge-and-categorization&quot;&gt;Shopping List: Plan Merge and Categorization&lt;/h2&gt;

&lt;p&gt;The shopping list has two sources of items:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Plan-derived&lt;/strong&gt; – parsed from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main_ingredients&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extras&lt;/code&gt; of meals in the current week’s plan&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Manual extras&lt;/strong&gt; – user-added items (e.g. paper towels, milk)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge_plan_into_list/2&lt;/code&gt; takes the current week’s plan, parses ingredients into segments (comma/semicolon), normalizes them into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_key&lt;/code&gt;s for grouping, and upserts into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShoppingListEntry&lt;/code&gt; by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(household_id, item_key)&lt;/code&gt;. Existing rows are never deleted by the merge—only added or updated.&lt;/p&gt;

&lt;p&gt;Categorization is done by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShoppingLists.Categorizer&lt;/code&gt;: whole-word keyword matching against produce, meat/seafood, cheese/dairy, pantry, and a default “items” bucket. Priority order matters (e.g. “egg noodles” matches pantry before cheese) so we can sort the list in a sensible shopping order.&lt;/p&gt;

&lt;h2 id=&quot;liveview-and-ui&quot;&gt;LiveView and UI&lt;/h2&gt;

&lt;p&gt;Core features (plan, meals, shopping list) are LiveView pages. We use:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Streams&lt;/strong&gt; for large, appendable lists to avoid memory growth&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Functional components&lt;/strong&gt; (CoreComponents) for forms, buttons, layout&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Colocated JS hooks&lt;/strong&gt; when we need client behavior (e.g. theme toggle in localStorage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All LiveView templates start with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;Layouts.app flash={@flash} ...&amp;gt;&lt;/code&gt; and receive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_scope&lt;/code&gt; from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;live_session&lt;/code&gt; so they can pass it into context calls.&lt;/p&gt;

&lt;h2 id=&quot;design-system&quot;&gt;Design System&lt;/h2&gt;

&lt;p&gt;Colors and typography live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DESIGN.md&lt;/code&gt; and CSS variables (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;var(--onrotation-primary)&lt;/code&gt;, etc.). Light/dark themes use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-theme=&quot;light&quot;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-theme=&quot;dark&quot;&lt;/code&gt;; we also support “system” via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prefers-color-scheme&lt;/code&gt;. Theme choice is persisted in localStorage and applied before paint when possible to avoid flash.&lt;/p&gt;

&lt;p&gt;Transactional emails share the same layout and brand colors, with inline styles for email client compatibility.&lt;/p&gt;

&lt;h2 id=&quot;blog-and-content&quot;&gt;Blog and Content&lt;/h2&gt;

&lt;p&gt;The blog is a separate context (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OnRotation.Blog&lt;/code&gt;) with published posts, tags, and an author-scoped admin. Body content is stored as Markdown in the database and rendered with Earmark at request time. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BodyProcessor&lt;/code&gt; runs after Earmark to replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{meal:N}}&lt;/code&gt; shortcodes with rendered meal cards—so authors can embed curated meals in posts, and logged-in users can add them to their library in place.&lt;/p&gt;

&lt;p&gt;The blog admin is a LiveView (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BlogAdminLive&lt;/code&gt;) behind a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require_blog_author&lt;/code&gt; plug. Authors get a list view, create/edit with autosave draft, tag management, and a preview that matches the public layout. Public routes are controller-based: index (optionally filtered by tag), show by slug, and an RSS feed at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/blog/feed.xml&lt;/code&gt;. The sitemap includes blog posts for SEO. Slugs are derived from titles; tags are many-to-many and filterable on the index.&lt;/p&gt;

&lt;h2 id=&quot;api-for-mobile&quot;&gt;API for Mobile&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/v1&lt;/code&gt; exposes resources for meals, plans, and plan entries. The API uses the same context functions as the web app; the only difference is auth (Bearer tokens) and response format (JSON). A plug assigns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;household_id&lt;/code&gt; from the authenticated user so controllers can call contexts like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Meals.list_meals(household_id, opts)&lt;/code&gt; without extra logic.&lt;/p&gt;

&lt;h2 id=&quot;quality-and-consistency&quot;&gt;Quality and Consistency&lt;/h2&gt;

&lt;p&gt;We enforce consistency with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;One &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alias&lt;/code&gt; per line&lt;/strong&gt; – no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alias Foo.{Bar, Baz}&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@moduledoc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@spec&lt;/code&gt; on public functions&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maybe_&lt;/code&gt; prefix&lt;/strong&gt; – use descriptive names instead&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix precommit&lt;/code&gt;&lt;/strong&gt; – format, Credo, SoBelow, Dialyzer, tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/rules&lt;/code&gt; files document these conventions for humans and AI-assisted workflows.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;OnRotation is a Phoenix app where:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Household-scoped contexts and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scope&lt;/code&gt; struct keep authorization and data access clear.&lt;/li&gt;
  &lt;li&gt;LiveView drives the web UX with streams and functional components.&lt;/li&gt;
  &lt;li&gt;A small JSON API under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/v1&lt;/code&gt; serves mobile with the same business logic.&lt;/li&gt;
  &lt;li&gt;Magic links power both web and mobile auth.&lt;/li&gt;
  &lt;li&gt;A token-based design system and precommit checks keep the codebase consistent and maintainable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building something similar—household/multi-tenant, LiveView-heavy, with a mobile API—this architecture is a solid starting point. The main lesson: invest in scoping and context boundaries early; they make everything else easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://onrotation.app&quot;&gt;Try OnRotation at onrotation.app&lt;/a&gt;&lt;/strong&gt; — start with our Starter Pack and plan your week in minutes.&lt;/p&gt;
</description>
        <pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/onrotation-technical-architecture</link>
        <guid isPermaLink="true">http://nickstalter.com/onrotation-technical-architecture</guid>
        
        <category>phoenix</category>
        
        <category>liveview</category>
        
        <category>elixir</category>
        
        <category>ecto</category>
        
        <category>architecture</category>
        
        
        <category>engineering</category>
        
        <category>elixir</category>
        
        <category>phoenix</category>
        
      </item>
    
      <item>
        <title>Beyond the Whiteboard: Why We Built OnRotation</title>
        <description>&lt;p&gt;We built OnRotation out of something simple: our love of cooking and the desire to make everyday life run a little smoother.&lt;/p&gt;

&lt;p&gt;Like many home cooks, we already had a rotation of go-to meals we loved. We enjoyed experimenting with them, tweaking flavors, and trying new ideas. But weeknights still brought the same questions: &lt;em&gt;What are we making? Do we have everything? Did we remember to thaw the meat?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Too often, that led to wandering the grocery store aisles without a plan, buying things we didn’t need, or giving in to last-minute takeout.&lt;/p&gt;

&lt;h2 id=&quot;a-little-about-us&quot;&gt;A Little About Us&lt;/h2&gt;

&lt;p&gt;We’re a couple based in Columbus, Ohio, sharing our home with two cats and two dogs who are always very interested in what’s for dinner. I’m a software engineer, and my wife manages a special diet for her epilepsy—which means meal planning isn’t just about convenience for us, it’s essential.&lt;/p&gt;

&lt;p&gt;We both love cooking. It’s one of our favorite ways to spend time together, experimenting with flavors and finding new ways to make our go-to meals even better. But managing a specific dietary plan while juggling busy schedules? That’s where things got complicated.&lt;/p&gt;

&lt;p&gt;One of the biggest challenges was finding alternatives to our favorite meals that fit the dietary requirements. We’d discover a great substitute, make it once, then completely forget about it weeks later.&lt;/p&gt;

&lt;h2 id=&quot;the-whiteboard-era&quot;&gt;The Whiteboard Era&lt;/h2&gt;

&lt;p&gt;So we started keeping track—first in a spreadsheet, listing every meal we’d successfully made, what worked, what didn’t, and when we last had it. Then we’d reference that spreadsheet when filling out our weekly whiteboard.&lt;/p&gt;

&lt;p&gt;Yes, a literal whiteboard on our fridge. We had the days of the week written out, and every Sunday we’d plan our meals and fill it in. We’d jot notes like “pull out chicken Tuesday” to remind ourselves to thaw meat ahead of time.&lt;/p&gt;

&lt;p&gt;It worked… but it was clunky. We were juggling two systems just to answer one question: &lt;em&gt;What are we eating this week?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We found ourselves thinking, &lt;em&gt;There has to be a better, easier, and more fun way to do this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So we created one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-onrotation-does&quot;&gt;What OnRotation Does&lt;/h2&gt;

&lt;p&gt;OnRotation takes years of using that trusty whiteboard and combines it with our love of cooking, experimenting, and prepping. It’s designed to help you:&lt;/p&gt;

&lt;h3 id=&quot;plan-your-week&quot;&gt;Plan Your Week&lt;/h3&gt;
&lt;p&gt;Build your own library of meals—your actual go-to dishes, not someone else’s recipes. Then drag them into a 7-day calendar. See your entire week at a glance, know exactly what’s for dinner each night, and eliminate the daily “what should we make?” debate.&lt;/p&gt;

&lt;h3 id=&quot;track-your-rotation&quot;&gt;Track Your Rotation&lt;/h3&gt;
&lt;p&gt;See when you last made each meal and how often you cook it. This helps you keep things fresh and varied—no more accidentally making the same thing three weeks in a row, or forgetting about that great recipe you tried last month.&lt;/p&gt;

&lt;h3 id=&quot;shop-with-intention&quot;&gt;Shop with Intention&lt;/h3&gt;
&lt;p&gt;Your shopping list is automatically generated from your weekly plan. No more wandering the store wondering what you need. Just open the app, check off items as you shop, and know you have everything for the week.&lt;/p&gt;

&lt;h3 id=&quot;manage-with-your-household&quot;&gt;Manage with Your Household&lt;/h3&gt;
&lt;p&gt;You can invite members to your household to share in meal planning, shopping lists, and maintaining your collection of meals.&lt;/p&gt;

&lt;h3 id=&quot;add-what-you-actually-need&quot;&gt;Add What You Actually Need&lt;/h3&gt;
&lt;p&gt;Because meal planning isn’t the only reason you go to the store, OnRotation lets you add extra items—paper towels, milk, dog food—right alongside your meal ingredients. One list for everything.&lt;/p&gt;

&lt;h2 id=&quot;who-its-for&quot;&gt;Who It’s For&lt;/h2&gt;

&lt;p&gt;While we built this for ourselves, we quickly realized how powerful it could be for anyone who:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Manages dietary restrictions&lt;/strong&gt; - Whether it’s epilepsy, celiac, diabetes, or allergies, keeping track of what works is crucial&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cooks the same meals regularly&lt;/strong&gt; - You’re not looking for new recipes every week, you just want to organize the ones you already love
– &lt;strong&gt;Those looking for new ideas&lt;/strong&gt; - You can browse Meal Packs to get ideas for dinners that you may have not had in a while or something completely new&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Wants to reduce food waste&lt;/strong&gt; - Buying only what you need for planned meals means less throwing away spoiled food&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Needs to eliminate decision fatigue&lt;/strong&gt; - The daily “what’s for dinner” question is exhausting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Families especially benefit from this. Kids can see what’s coming up for the week (no more surprise complaints at dinner time), and everyone can prepare mentally for what’s ahead.&lt;/p&gt;

&lt;h2 id=&quot;getting-started-is-easy&quot;&gt;Getting Started is Easy&lt;/h2&gt;

&lt;p&gt;We created &lt;strong&gt;Meal Packs&lt;/strong&gt;—curated collections of common meals like Tacos, Spaghetti, Grilled Chicken, and Burgers. Add our Starter Pack of 25 meals with one click, then customize every single one to match how &lt;em&gt;your&lt;/em&gt; family actually makes it.&lt;/p&gt;

&lt;p&gt;Swap store-bought pasta for homemade. Add your secret taco seasoning blend. Change “grilled chicken” to “baked chicken thighs” because that’s what you actually cook. The meal packs give you a foundation to build on, so you’re not staring at a blank screen wondering where to start.&lt;/p&gt;

&lt;p&gt;Within minutes, you’ll have a library that reflects your family’s actual cooking style—not someone else’s idea of what you should eat.&lt;/p&gt;

&lt;h2 id=&quot;why-it-matters&quot;&gt;Why It Matters&lt;/h2&gt;

&lt;p&gt;When we started planning our meals for the week, everything changed. Meal planning gave us peace of mind. It saved time at the store, cut down on food waste, and helped us stick to the way of eating that works best for us.&lt;/p&gt;

&lt;p&gt;Instead of guessing what sounded good each night, we knew exactly what we were making and what we needed. We shopped with purpose, used what we already had in the pantry or freezer, and avoided those “cheating” or “ordering in” days that can sneak up when life gets busy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because meal planning shouldn’t feel like a chore—it should feel like a tool that gives you back your time, your energy, and your peace of mind.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;try-onrotation&quot;&gt;Try OnRotation&lt;/h2&gt;

&lt;p&gt;We’re excited to finally share what started as a whiteboard on our fridge and turned into something we use every single day.&lt;/p&gt;

&lt;p&gt;OnRotation is live at &lt;a href=&quot;https://onrotation.app&quot;&gt;onrotation.app&lt;/a&gt;. Start with our Starter Pack, customize it to your style, and take control of your weekly cooking.&lt;/p&gt;

&lt;p&gt;We’d love to hear what you think. What features would make meal planning easier for you? What’s your biggest challenge with planning meals? Let us know—we’re just getting started, and your feedback shapes where we go next.&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/onrotation</link>
        <guid isPermaLink="true">http://nickstalter.com/onrotation</guid>
        
        <category>meal-planning</category>
        
        <category>onrotation</category>
        
        <category>launch-story</category>
        
        
        <category>product</category>
        
        <category>launch</category>
        
      </item>
    
      <item>
        <title>Creating an Accordion in Phoenix Liveview</title>
        <description>&lt;h1 id=&quot;creating-an-accordion-in-phoenix-liveview&quot;&gt;Creating an Accordion in Phoenix Liveview&lt;/h1&gt;
&lt;p&gt;Hello friends, I wanted to spend some time and write about converting an HTML/JS accordion to Phoenix’s functional components in LiveView. We will go through what functional components are, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slots&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrs&lt;/code&gt; and how to open/close without really writing any Javascript.&lt;/p&gt;

&lt;h3 id=&quot;background&quot;&gt;Background&lt;/h3&gt;
&lt;p&gt;I wanted to start with an HTML/JS based accordion so we can go through just how powerful LiveView is&lt;/p&gt;

&lt;p&gt;A basic HTML/Javascript accordion can look something like this:&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-w-md mx-auto&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;border rounded overflow-hidden&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Accordion Item 1 --&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;border-b&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;onclick=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggleAccordion(&apos;content1&apos;)&quot;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Section 1
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden p-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Your content for Section 1 goes here --&amp;gt;&lt;/span&gt;
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Accordion Item 2 --&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;border-b&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;onclick=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggleAccordion(&apos;content2&apos;)&quot;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Section 2
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content2&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden p-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Your content for Section 2 goes here --&amp;gt;&lt;/span&gt;
          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Add more accordion items as needed --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the ability to toggle open/close:&lt;/p&gt;
&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
  &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toggleAccordion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/script&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;That is all fine and dandy, but what if we want to reuse the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; someplace else? We can convert this into a functional component!&lt;/p&gt;

&lt;h3 id=&quot;functional-components&quot;&gt;Functional Components&lt;/h3&gt;
&lt;p&gt;Functonal components allow us to reuse components across various facets of our application, including LiveView and non-LiveViews(some people call them dead views).&lt;/p&gt;

&lt;p&gt;You can learn more about the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CoreComponents&lt;/code&gt; in the &lt;a href=&quot;https://hexdocs.pm/phoenix/components.html#corecomponents&quot;&gt;Phoenix Docs&lt;/a&gt;
We will be using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core_components.ex&lt;/code&gt; for our example.&lt;/p&gt;

&lt;p&gt;We will then expand on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; to take in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slots&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attributes&lt;/code&gt;. &lt;a href=&quot;https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-attributes&quot;&gt;Attributes&lt;/a&gt; are, as their name implies, attributes to pass to our component. These attrs are &lt;a href=&quot;https://hexdocs.pm/phoenix/components.html#corecomponents&quot;&gt;Slots&lt;/a&gt; are markup that we want our component to render in some way.&lt;/p&gt;

&lt;p&gt;Hopefully this all makes more sense later on.&lt;/p&gt;

&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;

&lt;p&gt;We can generate a new Phoenix app: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix phx.new --no-ecto my_app&lt;/code&gt;. Once finished, there is a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core_components.ex&lt;/code&gt; that was generated.
These core components are all functional components and LiveView provides a few already(buttons, forms, etc.)&lt;/p&gt;

&lt;p&gt;Let’s start up the app with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iex -S mix phx.server&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We need a canvas to paint our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; onto, so we should create a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;liveview&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;router.ex&lt;/code&gt; add a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;live&lt;/code&gt; route:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;live &quot;/hello&quot;, MyAppLive&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now create the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LiveView&lt;/code&gt; file at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./my_app/lib/my_app_web/live/my_app_live.ex&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And paste the following in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_app_live.ex&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MyAppLive&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Phoenix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;LiveView&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;CoreComponents&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;sx&quot;&gt;~H&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
      &amp;lt;h1&amp;gt;Hello there&amp;lt;/h1&amp;gt;
    &quot;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we visit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://localhost:4000/hello&lt;/code&gt; we should see our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hello there&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;converting-htmljs-accordion-to-functional-component&quot;&gt;Converting HTML/JS Accordion to Functional Component&lt;/h3&gt;

&lt;p&gt;Conversion is pretty straightforward in our case, as HEEX templates are already HTML.&lt;/p&gt;

&lt;p&gt;First we need to define our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; component inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/my_app/components/core_components.ex&lt;/code&gt; and paste our original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; without the JavaScript and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onclick&lt;/code&gt;. We will re-add the click functionality later.&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;accordion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;sx&quot;&gt;~H&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
      &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;md&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auto&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
        &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;border&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rounded&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
          &amp;lt;!-- Accordion Item 1 --&amp;gt;
          &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
            &amp;lt;button
              class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;semibold&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;hover:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;focus:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;outline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
              onclick=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;toggleAccordion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;content1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
            &amp;gt;
              Section 1
            &amp;lt;/button&amp;gt;
            &amp;lt;div id=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
              &amp;lt;!-- Your content for Section 1 goes here --&amp;gt;
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;

          &amp;lt;!-- Accordion Item 2 --&amp;gt;
          &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
            &amp;lt;button
              class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;semibold&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;hover:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;focus:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;outline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
              onclick=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;toggleAccordion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;content2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
            &amp;gt;
              Section 2
            &amp;lt;/button&amp;gt;
            &amp;lt;div id=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
              &amp;lt;!-- Your content for Section 2 goes here --&amp;gt;
              Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;

          &amp;lt;!-- Add more accordion items as needed --&amp;gt;

        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &quot;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can now use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; in our LiveView! All we need to do is replace our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;h1&amp;gt;Hello there&amp;lt;/h1&amp;gt;&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;.accordion&amp;gt;&amp;lt;/.accordion&amp;gt;&lt;/code&gt;. We should now see our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;adding-slots-and-attributes-to-our-functional-component&quot;&gt;Adding Slots and Attributes to our Functional Component&lt;/h3&gt;
&lt;p&gt;Having hard coded markup specific to this accordion is undesired if we want to reuse it in other places. We can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slots&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrs&lt;/code&gt; to render mark up being passed into the component.&lt;/p&gt;

&lt;p&gt;Slots and attrs need to be defined above our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; definition and will look like the following:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;accordion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;sx&quot;&gt;~H&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
    &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;md&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auto&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
      &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;border&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rounded&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
        &amp;lt;%= for content &amp;lt;- @content do %&amp;gt;
          &amp;lt;div class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
            &amp;lt;button
              class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;semibold&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;hover:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;focus:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;outline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;
            &amp;gt;
              &amp;lt;%= content.title %&amp;gt;
            &amp;lt;/button&amp;gt;
            &amp;lt;div id={content.id} class=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;
              &amp;lt;%= render_slot(content) %&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;% end %&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &quot;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Here, we are using a named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slot&lt;/code&gt; called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt; that has a few attributes that will help us when we add the open/close functionality back.&lt;/p&gt;

&lt;p&gt;We need to update our LiveView to use the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accordion&lt;/code&gt; functionality:&lt;/p&gt;
&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;o&quot;&gt;&amp;lt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;accordion&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-title&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Hello&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;there&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my second title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-second-title&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Hello&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;there&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my third title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-third-title&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Hello&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;there&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;/.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;accordion&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Currently, we cannot open/close it, as we are not using the JavaScript that came with it. So let’s do that now!&lt;/p&gt;

&lt;h3 id=&quot;replacing-javascript-with-liveview-js&quot;&gt;Replacing JavaScript with LiveView JS&lt;/h3&gt;
&lt;p&gt;Okay, so, this is technically cheating as we are still using JavaScript. LiveView has a module called &lt;a href=&quot;https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JS&lt;/code&gt;&lt;/a&gt;. This module is full of utility functions that will call the JavaScript for us without us having to write a script for it.&lt;/p&gt;

&lt;p&gt;We need to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;phx-click&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button&lt;/code&gt; in the accordion:&lt;/p&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;phx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;##{content.id}&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JS.toggle()&lt;/code&gt; will&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Show or hide elements based on visibility, with optional transitions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;condiontally-showhide-by-default&quot;&gt;Condiontally Show/Hide by default&lt;/h3&gt;
&lt;p&gt;If we wanted to start with the accordion panels closed by default we can use an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attr&lt;/code&gt; called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_closed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attr&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt; slot:&lt;/p&gt;
&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_closed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:boolean&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And add a condtional style to the accordion panel:&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{content.id}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;p-4&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{if&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;:start_closed&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;do:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;display:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;;&quot;}&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note how we have to access the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_closed&lt;/code&gt;, currently we cannot set default values for slot attributes&lt;/p&gt;

&lt;p&gt;Finally, add the attribute to some of the accordion’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt; slots:&lt;/p&gt;
&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my second title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-second-title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_closed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my third title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-third-title&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_closed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have parts of the accordion that are hidden by default while retaining the ability to toggle as needed!&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Functional components are super powerful and allow us to reuse components across our applcation. Slots and attributes can be somewhat intimidating at first but hopefully this helped to bring you the confidence you need.&lt;/p&gt;
</description>
        <pubDate>Mon, 04 Dec 2023 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/phoenix-liveview-accordion</link>
        <guid isPermaLink="true">http://nickstalter.com/phoenix-liveview-accordion</guid>
        
        
      </item>
    
      <item>
        <title>Phoenix Liveview using LocalStorage or SessionStorage</title>
        <description>&lt;h1 id=&quot;phoenix-liveview-using-localstorage-or-sessionstorage&quot;&gt;Phoenix Liveview using LocalStorage or SessionStorage&lt;/h1&gt;

&lt;p&gt;Sometimes we want to fetch or store data in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;, we can fetch/store this data using &lt;a href=&quot;https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook&quot;&gt;LiveView hooks&lt;/a&gt;.
These hooks allow for interoperability with JavaScript and grant us the ability to interact with a liveview during its lifecycle.&lt;/p&gt;

&lt;p&gt;For storing the data, we will call a JavaScript hook inside a liveview function to store the value of a counter.&lt;/p&gt;

&lt;p&gt;For fetching the data, we will be using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reconnected&lt;/code&gt; hook to fetch data from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;. In this post we will be using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt; as the data is wiped when we close the tab. If you wanted to persist the data, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; is what you are looking for. Both storages use the same function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getItem(key)&lt;/code&gt; where we provide a key which &lt;em&gt;hopefully&lt;/em&gt; has the values we are looking for.&lt;/p&gt;

&lt;p&gt;For more information regarding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; see the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage&quot;&gt;Mozilla Docs&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;cloning-the-demo-project&quot;&gt;Cloning the Demo Project&lt;/h2&gt;

&lt;p&gt;Let’s clone the demo Phoenix LiveView app. The main branch will only contain the minimal amount of setup without implementing any hooks:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone git@github.com:distortia/counter_example.git&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For the complete solution check out the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complete&lt;/code&gt; branch.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-the-hooks&quot;&gt;Setting up the Hooks&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: All hooks need to be defined above the following line &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;let liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {params: {_csrf_token: csrfToken}})&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Initializing our empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hooks&lt;/code&gt; object to be populated later:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;let hooks = {}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We need to add our hooks to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;liveSocket&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;let liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {
  params: {_csrf_token: csrfToken},
  hooks: hooks,
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;saving-state-on-button-click&quot;&gt;Saving State on Button Click&lt;/h2&gt;

&lt;p&gt;To save the state of our counter on button click we need to write a custom hook.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.js&lt;/code&gt;, we are going to create a hook called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;saveCount&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
hooks.saveCount = {
  mounted() {
    this.handleEvent(&quot;saveCount&quot;, ({ count }) =&amp;gt;
      sessionStorage.setItem(&quot;count&quot;, count)
    )
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;saveCount&lt;/code&gt; uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mounted&lt;/code&gt; hook so it gets attached as the component is mounted.&lt;/p&gt;

&lt;p&gt;For the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_event&lt;/code&gt; function we use a new function called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_event&lt;/code&gt; which will invoke the JavaScript hook we called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;saveCount&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def handle_event(&quot;inc&quot;, _session, socket) do
  count = socket.assigns.count + 1
  socket = socket |&amp;gt; assign(count: count)
  {:noreply, push_event(socket, &quot;saveCount&quot;, %{count: count})}
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To wire up our button we need to add a unique ID as well as the hook:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;button id=&quot;inc-button&quot; phx-click=&quot;inc&quot; phx-hook=&quot;saveCount&quot;&amp;gt;Increment&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We should now see the value of count being incremented in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt; as well as on the page&lt;/p&gt;

&lt;h2 id=&quot;restoring-state-on-reconnection&quot;&gt;Restoring state on Reconnection&lt;/h2&gt;

&lt;p&gt;To restore the state from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt; we need to create a new hook that I will call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;hooks.restore = {
  mounted() {
    count = sessionStorage.getItem(&quot;count&quot;)
    this.pushEvent(&quot;restore&quot;, {count: count})
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This hook will call our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt; event in the liveview which looks like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def handle_event(&quot;restore&quot;, %{&quot;count&quot; =&amp;gt; count}, socket) do
  count = String.to_integer(count)
  socket = socket |&amp;gt; assign(count: count)
  {:noreply, socket}
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Values in sessionStorage are strings and we need to properly convert to the correct type&lt;/p&gt;

&lt;p&gt;We do need to handle cases where we have no value in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;, we can do that by explcitly handling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nil&lt;/code&gt; values above the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handle_event&lt;/code&gt; call:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def handle_event(&quot;restore&quot;, %{&quot;count&quot; =&amp;gt; nil}, socket), do: {:noreply, socket}

def handle_event(&quot;restore&quot;, %{&quot;count&quot; =&amp;gt; count}, socket) do
  count = String.to_integer(count)
  socket = socket |&amp;gt; assign(count: count)
  {:noreply, socket}
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In order to actually call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt; hook we need to add a unique ID to the element and attach our hook to it:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;section class=&quot;phx-hero&quot; id=&quot;page-live&quot; phx-hook=&quot;restore&quot; &amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We now have a way to restore data from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localStorage&lt;/code&gt; into our LiveView!&lt;/p&gt;

&lt;p&gt;To test this out, we can go into the developer settings and manually add key-value pair to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;example: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count: 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When we refresh the page, the counter should reflect the value in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;This is how to store and restore data in a LiveView using Hooks and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionStorage&lt;/code&gt;. If you found it helpful, reach out on twitter or share with your friends!&lt;/p&gt;

&lt;p&gt;If you are interested in working with elixir every day, come join my remote team at &lt;a href=&quot;https://www.influxdata.com/careers/?gh_jid=3985743&quot;&gt;InfluxData&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;

&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/distortia/counter_example&quot;&gt;Repo&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook&quot;&gt;LiveView Hooks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage&quot;&gt;MDN SessionStorage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 14 Apr 2022 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/phoenix-liveview-with-localstorage-or-sessionstorage</link>
        <guid isPermaLink="true">http://nickstalter.com/phoenix-liveview-with-localstorage-or-sessionstorage</guid>
        
        
      </item>
    
      <item>
        <title>Custom Honeybadger filters for Tokens</title>
        <description>&lt;p&gt;Hey All, it has been quite a long time since my last post. I apologize for that. Today, I will be keeping things short and sweet.&lt;/p&gt;

&lt;h1 id=&quot;custom-honeybadger-filters-for-tokens&quot;&gt;Custom Honeybadger filters for Tokens&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://www.honeybadger.io/&quot;&gt;Honeybadger&lt;/a&gt; is a service for “Exception, uptime, and cron monitoring, all in one place”. At &lt;a href=&quot;https://influxdata.com&quot;&gt;InfluxData&lt;/a&gt;, we use this service to monitor our application and recieve notifications when things go wrong. Sometimes, we receive a &lt;em&gt;bit&lt;/em&gt; too much information, such as Bearer Tokens.&lt;/p&gt;

&lt;p&gt;Leaking tokens is a serious security concern. Bad faith actors can use that token to manipulate your systems or systems you are connected to. Once a token has been exposed, you need to fix the issue and rotate your tokens.&lt;/p&gt;

&lt;p&gt;In this case, we want to redact any token going to Honeybadger.&lt;/p&gt;

&lt;p&gt;Fortunately, the &lt;a href=&quot;https://github.com/honeybadger-io/honeybadger-elixir&quot;&gt;Honeybadger Elixir Repo&lt;/a&gt; has an easy way for us to create a &lt;a href=&quot;https://hexdocs.pm/honeybadger/Honeybadger.Filter.Mixin.html&quot;&gt;custom filters&lt;/a&gt;. Let’s do that now.&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;defmoudle&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyApp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MyFilter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Honeybadger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Filter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Mixin&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filter_error_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Regex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;~r/(Token|Bearer) [^&quot;]+/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;1 redacted&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will filter error messages before being sent to Honeybadger and check for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Token &amp;lt;token&amp;gt;&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt; and replace them with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Token redacted&lt;/code&gt;. What is nice is that this covers &lt;a href=&quot;https://github.com/teamon/tesla&quot;&gt;Tesla&lt;/a&gt;, &lt;a href=&quot;https://hexdocs.pm/httpoison/readme.html&quot;&gt;HTTPoison&lt;/a&gt; and probably any other HTTP library you pick. All the error messages are just strings. That means we can expect escaped strings and can capture the values up until we hit the closing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, let’s write a few tests for these scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: We are looking for typical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Token &amp;lt;token&amp;gt;&lt;/code&gt;. Error messages are truncated as they are quite large.&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyApp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HoneybadgerFilterTest&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ExUnit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Case&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;async:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyApp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HoneybadgerFilter&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;filter_error_message&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;replaces general tokens&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;some.jwt.abc123&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;[token: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;HoneybadgerFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_error_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;[token: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;redacted&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;refute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;replaces bearer tokens&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;some.jwt.abc123&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;{&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;}&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;HoneybadgerFilter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_error_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;{&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Bearer redacted&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;}&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;refute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The tests will cover and probably any other HTTP library you pick.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Adding custom filters for Honeybadger can be very straightforward. With a little of regex(I am sorry), you can safely export errors without leaking tokens!&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;

&lt;p&gt;Nick&lt;/p&gt;
</description>
        <pubDate>Fri, 24 Sep 2021 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/honeybadger-filters-for-tokens</link>
        <guid isPermaLink="true">http://nickstalter.com/honeybadger-filters-for-tokens</guid>
        
        
      </item>
    
      <item>
        <title>Introducing ProPollr.com</title>
        <description>&lt;p&gt;Hey everyone!&lt;/p&gt;

&lt;p&gt;I have been hacking away on an idea for a week and finished it!&lt;/p&gt;

&lt;p&gt;Introducing &lt;a href=&quot;propollr.com&quot;&gt;ProPollr.com&lt;/a&gt;!&lt;/p&gt;

&lt;h1 id=&quot;what-is-propollr&quot;&gt;What is ProPollr?&lt;/h1&gt;

&lt;p&gt;ProPollr is a realtime polling application that focuses on ease of use, realtime interactivity, and polling anonymity!&lt;/p&gt;

&lt;p&gt;I came up with the idea as I was walking passed a group that was holding a retro. They were writing all of their responses onto sticky notes and walking up to the whiteboard to place them. That’s when it hit me. We know that anonymous feedback is honest feedback, so let’s capitalize on that!&lt;/p&gt;

&lt;h1 id=&quot;whats-the-tech-stack&quot;&gt;What’s the Tech Stack?&lt;/h1&gt;

&lt;p&gt;This should come as no surprise, I am using&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt; - I’ll touch on this in a minute.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bulma.io/&quot;&gt;Bulma&lt;/a&gt; - With Node-Sass for Brunch&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://fontawesome.com/&quot;&gt;Font Awesome 5 Pro&lt;/a&gt; - Awesome Icons!&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/zanderxyz/veil&quot;&gt;Veil&lt;/a&gt; - I’ll touch on this in a minute as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;phoenix&quot;&gt;Phoenix&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt; is a wonderful framework for composing web applications and API’s. Some of the amazing features that comes with Phoenix, are Channels and Presence.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix/channels.html&quot;&gt;Channels&lt;/a&gt; are a means to send and receive messages from sockets. There are a myriad of functionalities built in that allow you to incorporate realtime aspects with minimal code. With Channels, you can perform controller actions and broadcast to the specfied socket, you can listen for specific messages and reply with changes to state. An awesome use of sockets is the &lt;a href=&quot;https://github.com/grych/drab&quot;&gt;Drab Project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I use sockets for realtime question and answers.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix/presence.html&quot;&gt;Presence&lt;/a&gt; is a newer feature of Phoenix that revolves around, well, presences. It’s biggest highlight is the ability to track and register process informatatin across the cluster. Presence has no single point of failure and no single source of truth. This is massive!&lt;/p&gt;

&lt;p&gt;The main reason for me to use it, is to display the current number of Pollrs in a Sesh(a question session).&lt;/p&gt;

&lt;h3 id=&quot;veil&quot;&gt;Veil&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/zanderxyz/veil&quot;&gt;Veil&lt;/a&gt; is a wonderful passwordless authentication library. It is much easier to not have to worry about storing/salting/hashing/protecting passwords if you never check for one. This also makes the user experience that much simpler. The action for logging in is the same as creating an account. You get an email sent to your inbox and you click the link to authenticate. I find is very simple and easy to work with. Authentication is then handled to an encoded cookie that automatically gets checked and its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TTL(Time to Live)&lt;/code&gt; extended.&lt;/p&gt;

&lt;h1 id=&quot;so-what-is-the-plan&quot;&gt;So What is the Plan?&lt;/h1&gt;

&lt;p&gt;Propollr is open for use now.&lt;/p&gt;

&lt;p&gt;My next steps, in no particular order:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Full Launch&lt;/li&gt;
  &lt;li&gt;Add extra features such as graphs and charts&lt;/li&gt;
  &lt;li&gt;Extra question categorization - So you can run things like retros&lt;/li&gt;
  &lt;li&gt;Setup CI/CD using &lt;a href=&quot;https://github.com/bitwalker/distillery&quot;&gt;Distillery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;For me, the biggest win is the ability to take an idea and push something out in a week. It’s a wonderful feeling to release something new and already have a viable beta group to use it.&lt;/p&gt;

&lt;p&gt;Feel free to check it out and submit some feedback!&lt;/p&gt;

&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;propollr.com&quot;&gt;ProPollr.com&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix/channels.html&quot;&gt;Channel&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix/presence.html&quot;&gt;Presence&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bulma.io/&quot;&gt;Bulma&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/grych/drab&quot;&gt;Drab&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/zanderxyz/veil&quot;&gt;Veil&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/bitwalker/distillery&quot;&gt;Distillery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 03 Sep 2018 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/introducing-propollr</link>
        <guid isPermaLink="true">http://nickstalter.com/introducing-propollr</guid>
        
        
      </item>
    
      <item>
        <title>Git Graveyard: Letscode - 2014</title>
        <description>&lt;p&gt;Welcome back to another post about my Git Graveyard. Today I want to look at a CLI(Command-line Interface) utility I worked on.&lt;/p&gt;

&lt;h1 id=&quot;lets-code---cli-utility-for-opening-up-project-specific-programs---2014&quot;&gt;Let’s Code - CLI Utility for Opening Up Project Specific Programs - 2014&lt;/h1&gt;

&lt;p&gt;Let’s Code was intended to be a CLI that would open up specific programs that you need to get working on your project.&lt;/p&gt;

&lt;p&gt;You would use it like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;letscode myproject&lt;/code&gt;. Let’s Code would then open your editor with your project, Trello/Jira, and can be configured to open up another other programs you need, like Slack.&lt;/p&gt;

&lt;h1 id=&quot;motivation&quot;&gt;Motivation&lt;/h1&gt;

&lt;p&gt;I was motivated to make getting into work easier. I was constantly switching between projects and various programs in order to do my work. I wanted to help my context switching out while trying to remain in the terminal as best I could.&lt;/p&gt;

&lt;p&gt;The usage was pretty straightforward:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;thor&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Letscode&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CLI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Thor&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Thor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Actions&lt;/span&gt;
        &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Letscode&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;#main method&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;start [PROJECT]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;To start a project, type &apos;letscode start [PROJECT]&apos;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;get_project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;process_keys&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;config&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;To config a project, please type &apos;letscode config&apos;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get_config&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;write_file&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Shows all projects in the config file.&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
           &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt;  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_projects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_yaml&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;delete [PROJECT]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot; To delete a file type &apos;letscode delete [PROJECT].&apos;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;read_file&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;write_file&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The various methods went out and did what you expect. Just follow up with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;letscode myproject&lt;/code&gt; and you were ready to roll!&lt;/p&gt;

&lt;h1 id=&quot;reason-for-abandoning&quot;&gt;Reason for Abandoning&lt;/h1&gt;

&lt;p&gt;I abandoned this project as I switched jobs and had less context switching over all. As I reread the code I wrote and the direction it was going, I feel this project would have been a fun little utility.&lt;/p&gt;

&lt;h1 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Ruby - I was getting more into rails at the time and wanted something familiar to work in.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://github.com/erikhuda/thor&quot;&gt;Thor&lt;/a&gt; - toolkit for building powerful command-line interfaces in Ruby&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;http://watir.com/&quot;&gt;Watir&lt;/a&gt; - A gem typically used for browser automation(mostly for testing)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;http://chromedriver.chromium.org/&quot;&gt;ChromeDriver&lt;/a&gt; - chromium’s webdriver implementation for browser automation&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;All in all, I may want to revisit this utility. I have quite a bit of use for it as of late. The project I am working on has different repos for it’s umbrella apps and I have to switch between projects fairly regularly. As far as looking at my code, I am pleasantly surpised with past me. I managed to keep things coherent and readible.&lt;/p&gt;

&lt;p&gt;I may end up porting this over to Elixir, it’s one of my favorite languages and has strong built in CLI Utilities with &lt;a href=&quot;https://hexdocs.pm/elixir/OptionParser.html&quot;&gt;OptionParser&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sat, 18 Aug 2018 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/git-graveyard-letscode</link>
        <guid isPermaLink="true">http://nickstalter.com/git-graveyard-letscode</guid>
        
        
      </item>
    
      <item>
        <title>Git Graveyard: Alpaca - 2014</title>
        <description>&lt;p&gt;Hey all, I’ve seen some recent posts online around “Github Graveyard”. The premise of “Github Graveyard” is to reflect on old projects that end up abandoned. I know I have a few of these projects and wanted to start a new series for my projects. I’d also like to extend “Github Graveyard” to be “Git Graveyard” or “Project Graveyard”. I know GitHub is a big deal but we should not be narrowing the scope of these stories.&lt;/p&gt;

&lt;h1 id=&quot;alpaca---fully-encrypted-chat-messaging-system---2014&quot;&gt;Alpaca - Fully Encrypted Chat Messaging System - 2014&lt;/h1&gt;

&lt;p&gt;Alpaca was intended to be a fully ecrypted(end-to-end) messaging application with a focus on security and ease of use. My original thought was to start as a web service to make everything work, then port over the neccessary pieces to mobile.&lt;/p&gt;

&lt;p&gt;One of the key pieces I wanted, was to be completely unable to decrypt the communications and stay as an independent entity. As of late, it appears that no matter what social networks and media do, they always seemed to get sucked into some sort of mess.&lt;/p&gt;

&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;/h2&gt;

&lt;p&gt;I came up with the idea for Alpaca as a way to better utilize chat apps and provide a more secure method for communication. At school, I was using a lot of Skype(gross) and Google Hangouts(Not much better). There wasn’t a huge demand for secured communication but I was trying to be proactive to better protect everyone. It wasn’t super easy to invite someone into a group chat and have real discussions about our project or homework.&lt;/p&gt;

&lt;h2 id=&quot;reason-for-abandoning&quot;&gt;Reason for Abandoning&lt;/h2&gt;

&lt;p&gt;I started Alpaca in college and ended up abandoning due to lack of time and apps like Telegram coming to the forefront. Telegram gets quite a bit of flak for it’s use in shadier activites. Maybe it was best that I never finished this project.&lt;/p&gt;

&lt;p&gt;I fell short before doing the encryption.&lt;/p&gt;

&lt;h2 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h2&gt;

&lt;p&gt;The tech stack was pretty simple back then,&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://socket.io/&quot;&gt;Socket.io&lt;/a&gt; - Real time updating for the chat.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://www.mongodb.com/&quot;&gt;Mongodb&lt;/a&gt; - Easy storage for history, if desired.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://expressjs.com/&quot;&gt;Expressjs&lt;/a&gt; - Web Server&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://sass-lang.com/&quot;&gt;Sass&lt;/a&gt; + &lt;a href=&quot;getskeleton.com/&quot;&gt;SkeletonUI&lt;/a&gt; - Simple and lightweight front end framework and tooling&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Polymer or Angular - Never got around to integrating these into the site. I was experiencing a lot of Angluar 0.x - 1.x back in the day but Polymer offered something interesting with web components.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I feel pretty good about not finishing this project. With all the trouble that Telegram gets into from the shadier parts of the world, it is more trouble that it is worth. But I enjoyed looking back and my motivations and what I was thinking about back then.&lt;/p&gt;
</description>
        <pubDate>Wed, 15 Aug 2018 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/github-graveyard-alpaca</link>
        <guid isPermaLink="true">http://nickstalter.com/github-graveyard-alpaca</guid>
        
        
      </item>
    
      <item>
        <title>Elixir EScripts vs Mix Run</title>
        <description>&lt;p&gt;Continuing on my last post as a write about a new service I am working on at work, I’d like to talk about &lt;a href=&quot;https://hexdocs.pm/mix/master/Mix.Tasks.Escript.Build.html&quot;&gt;EScripts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What are EScripts? They are Elixir scripts in the form of an executable that you compile and run.&lt;/p&gt;

&lt;h1 id=&quot;what-are-escripts-used-for&quot;&gt;What are EScripts used for?&lt;/h1&gt;

&lt;p&gt;This is a great question! EScripts are compiled with all the Elixir code needed to run. Which means it’ll run on any machine that has Erlang installed. This means you can make an portable executable as a CLI(Command Line Interface).&lt;/p&gt;

&lt;p&gt;If you are making a CLI application, such as a custom scaffolding based on inputs from the CLI, then EScripts are wonderful.&lt;/p&gt;

&lt;h1 id=&quot;what-are-escripts-not-used-for&quot;&gt;What are EScripts not used for?&lt;/h1&gt;

&lt;p&gt;While having a CLI can be a great thing, it does not make sense to port service code into EScripts. Service code, such as database seeding, migrating data, etc.&lt;/p&gt;

&lt;p&gt;Phoenix does a great job at demonstrating this with it’s &lt;a href=&quot;https://phoenixframework.org/blog/seeding-data&quot;&gt;seed files&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;EScripts are best suited for creating CLIs as stated in their docs. I have seen many use EScripts for what they should be using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix run my_script.exs&lt;/code&gt; for. Maybe the docs of Elixir is too great and they came across EScripts. Either way, I wanted to shed some light and help educate those that may be tempted to use an EScript.&lt;/p&gt;

&lt;h1 id=&quot;my-experience-in-using-an-escript-instead-of-mix-run&quot;&gt;My experience in using an EScript instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix run&lt;/code&gt;&lt;/h1&gt;

&lt;p&gt;In an attempt to get into the weeds of the situation, I created an EScript to read data from a file, parse/transform, and load it into my database.&lt;/p&gt;

&lt;p&gt;Normally, I would have this as an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.exs&lt;/code&gt; file or Elixir Script inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; folder. I would invoke the file as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MIX_ENV=my_env mix run bin/my_script.exs&lt;/code&gt;. I have different databases for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt;. I know that all my prod/dev environments all have Elixir on them, so I do not have to worry about shipping an all-inclusive erlang executable.&lt;/p&gt;

&lt;p&gt;With each EScript it needs to be compiled for the environment that it needs to run in. This is kind of tedious. Running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MIX_ENV=prod mix escript.build&lt;/code&gt; and not remembering that last time you compile your script can bite you sometimes. I accidentally ran, what I thought was a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt; EScript, in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev&lt;/code&gt;. I was confused for a little bit as to why my data was not in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt; database. Then it hit me, I compiled my EScript for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix run&lt;/code&gt;, I have to explicitly set my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MIX_ENV&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt; in order to run the script on my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod&lt;/code&gt; instance. There is a bit more verbosity with this setup and I feel that the process keeps you from making the mistake of running the right script in the wrong environment.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;For most of your needs you should always pick &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix run&lt;/code&gt; over EScripts. Both can be covered under your code coverage tool of choice. Both can be tested in the same manor. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix run&lt;/code&gt; is more verbose in it’s usage and for me, that’s a huge plus.&lt;/p&gt;
</description>
        <pubDate>Fri, 03 Aug 2018 00:00:00 +0000</pubDate>
        <link>http://nickstalter.com/escript-vs-mix-run</link>
        <guid isPermaLink="true">http://nickstalter.com/escript-vs-mix-run</guid>
        
        
      </item>
    
  </channel>
</rss>
