<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Peng's Learning]]></title><description><![CDATA[Sharing what I learned writing JavaScript and React.]]></description><link>https://blog.peng37.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 23 Apr 2026 02:53:25 GMT</lastBuildDate><atom:link href="https://blog.peng37.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to make your web app accept sharing]]></title><description><![CDATA[I am building a web app that helps people manage frequently used links and notes (called easyy.click), I have added several ways to make collecting links/notes easyy, for example:

People can add multiple links on the creation page;

People can impor...]]></description><link>https://blog.peng37.com/how-to-make-your-web-app-accept-sharing</link><guid isPermaLink="true">https://blog.peng37.com/how-to-make-your-web-app-accept-sharing</guid><category><![CDATA[PWA]]></category><category><![CDATA[Share]]></category><category><![CDATA[Service Workers]]></category><category><![CDATA[Bookmark Management]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Wed, 06 Nov 2024 19:36:07 GMT</pubDate><content:encoded><![CDATA[<p>I am building a web app that helps people manage frequently used links and notes (called <a target="_blank" href="https://easyy.click">easyy.click</a>), I have added several ways to make collecting links/notes easyy, for example:</p>
<ul>
<li><p>People can add multiple links on the creation page;</p>
</li>
<li><p>People can import their browser bookmarks (it still works if you have thousands of links, takes some time though);</p>
</li>
<li><p>Created a browser extension, so people can save the current tab, or any link / selected text by right clicking it, this makes collecting really easyy on laptop;</p>
</li>
</ul>
<p>The next step is, is it possible to accept shared link / text from other apps on the phone? So it’s also easyy to collect things on the phone.</p>
<p>I have the question, GPT has the answer, and the answer is Yes (partially). You may guessed it, it works with Android, but not iOS, details <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target">here</a>.</p>
<p>I will show you how I made it work, to accept sharing for my web app on Android.</p>
<h3 id="heading-1-add-sharetarget-to-manifestjson">1). Add share_target to manifest.json</h3>
<p>Your web app needs to be a PWA, so you have to have a valid manifest.json, and it needs to have a <code>share_target</code> field, like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"share_target"</span>: {
    <span class="hljs-attr">"action"</span>: <span class="hljs-string">"/easyy-share"</span>,
    <span class="hljs-attr">"method"</span>: <span class="hljs-string">"POST"</span>,
    <span class="hljs-attr">"enctype"</span>: <span class="hljs-string">"multipart/form-data"</span>,
    <span class="hljs-attr">"params"</span>: {
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"title"</span>,
      <span class="hljs-attr">"text"</span>: <span class="hljs-string">"text"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"url"</span>
    }
  }
}
</code></pre>
<p>This tell Android, if people share a link/text to your web app, it will make a <code>POST</code> request to <code>${appDomain}/easyy-share</code> (in my case <code>https://app.easyy.click/easyy-share</code>), with the payload defined by <code>enctype</code> and <code>params</code>.</p>
<p>(If your web app is a SPA, you don’t need to prepare a <code>/easyy-share</code> route, this request will handled by service worker, see below)</p>
<p>This is my full manifest.json file: <a target="_blank" href="https://github.com/penghuili/easyy.click/blob/master/public/manifest.json">https://github.com/penghuili/easyy.click/blob/master/public/manifest.json</a></p>
<h3 id="heading-2-register-a-service-worker">2). Register a service worker</h3>
<p>You can process the request with a real backend endpoint (see the MDN doc), or, you can process it within service worker.</p>
<p>My web app is a SPA, and I need to end-to-end encrypt the link/note before sending it to backend, so I process the request in service worker.</p>
<p>When my SPA starts, I register a service worker:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (<span class="hljs-string">'serviceWorker'</span> <span class="hljs-keyword">in</span> navigator) {
  navigator.serviceWorker.register(<span class="hljs-string">'/sw.js'</span>).catch(<span class="hljs-function"><span class="hljs-params">registrationError</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'SW registration failed: '</span>, registrationError);
  });
}
</code></pre>
<p>The sw.js file has the logic to process the request.</p>
<h3 id="heading-3-process-request-in-swjs">3). Process request in sw.js</h3>
<p>The sw.js needs to have a listener for the <code>fetch</code> event, like this</p>
<pre><code class="lang-javascript">self.addEventListener(<span class="hljs-string">'fetch'</span>, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
  <span class="hljs-keyword">if</span> (event.request.method === <span class="hljs-string">'POST'</span> &amp;&amp; event.request.url.includes(<span class="hljs-string">'/easyy-share'</span>)) {
    event.respondWith(handleShare(event.request));
  }
});
</code></pre>
<p>We only process it if the request method is POST and the path is /easyy-share. All other fetch requests will behave normally.</p>
<p>Now let’s see how the <code>handleShare()</code> looks:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleShare</span>(<span class="hljs-params">request</span>) </span>{
  <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">await</span> request.formData();
  <span class="hljs-keyword">const</span> title = formData.get(<span class="hljs-string">'title'</span>);
  <span class="hljs-keyword">const</span> content = formData.get(<span class="hljs-string">'url'</span>) || formData.get(<span class="hljs-string">'text'</span>);

  <span class="hljs-keyword">if</span> (!content) {
    <span class="hljs-keyword">return</span> Response.redirect(<span class="hljs-string">'/shared'</span>, <span class="hljs-number">303</span>);
  }

  saveContent(title, content);

  <span class="hljs-keyword">return</span> Response.redirect(<span class="hljs-string">`/shared?shared=<span class="hljs-subst">${isValidUrl(content) ? <span class="hljs-string">'link'</span> : <span class="hljs-string">'text'</span>}</span>`</span>, <span class="hljs-number">303</span>);
}
</code></pre>
<p>Very straightforward, we get the payload from request, save it, and redirect to a <code>/shared</code> page with some query params. Some notes:</p>
<ol>
<li><p><code>saveContent()</code> is an async function, but here we don’t need to <code>await</code> it, the requests will finish in the background, and users will see the success page quickly;</p>
</li>
<li><p>In 1) we said we don’t need a <code>/easyy-share</code> route, but here we need a <code>/shared</code> route, because <code>Response.redirect('/shared', 303)</code> will open our app, and open the <code>/shared</code> page. This is our good chance to tell users, that the share is done;</p>
</li>
<li><p>service worker doesn’t have access to localStorage, but it has access to indexDB, so if your <code>saveContent()</code> needs to read something from the localStorage (access token maybe?), you need to save that thing also in indexDB in your main app, so service worker can get the value;</p>
</li>
</ol>
<p>Check the full code of my sw.js: <a target="_blank" href="https://github.com/penghuili/easyy.click/blob/master/src/sw.js">https://github.com/penghuili/easyy.click/blob/master/src/sw.js</a></p>
<p>This is the end result:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://x.com/easyydotclick/status/1854192613655560196">https://x.com/easyydotclick/status/1854192613655560196</a></div>
<p> </p>
<p>Let me know if you have any feedback or question.</p>
]]></content:encoded></item><item><title><![CDATA[A react component for Tiktok style swiper]]></title><description><![CDATA[See the end result first:

I am building a note taking app, that is Instagram style, free, encrypted. I have a “Today in history“ page, where you can review what happened on the same day of last week / last month / last 3 months / last year etc.
TikT...]]></description><link>https://blog.peng37.com/a-react-component-for-tiktok-style-swiper</link><guid isPermaLink="true">https://blog.peng37.com/a-react-component-for-tiktok-style-swiper</guid><category><![CDATA[Tiktok]]></category><category><![CDATA[swipe]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Fri, 13 Sep 2024 18:40:25 GMT</pubDate><content:encoded><![CDATA[<p>See the end result first:</p>
<p><img src="https://app.notenote.cc/assets/tiktok-swiper.gif" alt class="image--center mx-auto" /></p>
<p>I am building a note taking app, that is Instagram style, free, encrypted. I have a “Today in history“ page, where you can review what happened on the same day of last week / last month / last 3 months / last year etc.</p>
<p>TikTok is popular and the swipe up there is so addictive, so I decided to make my review page TikTok style.</p>
<p>It’s simple, I will just show the code here.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">'./TikTokCards.css'</span>;

<span class="hljs-keyword">import</span> React, { useRef, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> fastMemo <span class="hljs-keyword">from</span> <span class="hljs-string">'react-fast-memo'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TikTokCards = fastMemo(<span class="hljs-function">(<span class="hljs-params">{ cards, height = <span class="hljs-string">'100vh'</span> }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [currentIndex, setCurrentIndex] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> [startY, setStartY] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> [isDragging, setIsDragging] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [dragOffset, setDragOffset] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> containerRef = useRef(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> handleStart = <span class="hljs-function"><span class="hljs-params">clientY</span> =&gt;</span> {
    setStartY(clientY);
    setIsDragging(<span class="hljs-literal">true</span>);
    setDragOffset(<span class="hljs-number">0</span>);
  };

  <span class="hljs-keyword">const</span> handleMove = <span class="hljs-function"><span class="hljs-params">clientY</span> =&gt;</span> {
    <span class="hljs-keyword">if</span> (!isDragging) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> diff = startY - clientY;
    setDragOffset(diff);
  };

  <span class="hljs-keyword">const</span> handleEnd = <span class="hljs-function">() =&gt;</span> {
    setIsDragging(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Math</span>.abs(dragOffset) &gt; <span class="hljs-number">100</span>) {
      <span class="hljs-keyword">if</span> (dragOffset &gt; <span class="hljs-number">0</span> &amp;&amp; currentIndex &lt; cards.length - <span class="hljs-number">1</span>) {
        setCurrentIndex(currentIndex + <span class="hljs-number">1</span>);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (dragOffset &lt; <span class="hljs-number">0</span> &amp;&amp; currentIndex &gt; <span class="hljs-number">0</span>) {
        setCurrentIndex(currentIndex - <span class="hljs-number">1</span>);
      }
    }
    setDragOffset(<span class="hljs-number">0</span>);
  };

  <span class="hljs-comment">// Touch event handlers</span>
  <span class="hljs-keyword">const</span> handleTouchStart = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> handleStart(e.touches[<span class="hljs-number">0</span>].clientY);
  <span class="hljs-keyword">const</span> handleTouchMove = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> handleMove(e.touches[<span class="hljs-number">0</span>].clientY);
  <span class="hljs-keyword">const</span> handleTouchEnd = <span class="hljs-function">() =&gt;</span> handleEnd();

  <span class="hljs-comment">// Mouse event handlers</span>
  <span class="hljs-keyword">const</span> handleMouseDown = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> handleStart(e.clientY);
  <span class="hljs-keyword">const</span> handleMouseMove = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> handleMove(e.clientY);
  <span class="hljs-keyword">const</span> handleMouseUp = <span class="hljs-function">() =&gt;</span> handleEnd();

  <span class="hljs-comment">// Wheel event handler for scrolling</span>
  <span class="hljs-keyword">const</span> handleWheel = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-comment">// e.preventDefault();</span>
    <span class="hljs-keyword">if</span> (e.deltaY &gt; <span class="hljs-number">0</span> &amp;&amp; currentIndex &lt; cards.length - <span class="hljs-number">1</span>) {
      setCurrentIndex(currentIndex + <span class="hljs-number">1</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (e.deltaY &lt; <span class="hljs-number">0</span> &amp;&amp; currentIndex &gt; <span class="hljs-number">0</span>) {
      setCurrentIndex(currentIndex - <span class="hljs-number">1</span>);
    }
  };

  <span class="hljs-keyword">const</span> getCardStyle = <span class="hljs-function"><span class="hljs-params">index</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> offset = (index - currentIndex) * <span class="hljs-number">100</span> + (isDragging ? -dragOffset / <span class="hljs-number">5</span> : <span class="hljs-number">0</span>);
    <span class="hljs-keyword">let</span> opacity = <span class="hljs-number">1</span>;

    <span class="hljs-keyword">if</span> (isDragging) {
      <span class="hljs-keyword">if</span> (dragOffset &gt; <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// Dragging up</span>
        <span class="hljs-keyword">if</span> (index === cards.length - <span class="hljs-number">1</span>) {
          opacity = <span class="hljs-number">1</span>; <span class="hljs-comment">// Keep first card fully opaque when scrolling up</span>
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (index === currentIndex) {
          opacity = <span class="hljs-number">0.7</span>;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (index === currentIndex + <span class="hljs-number">1</span>) {
          opacity = <span class="hljs-number">1</span>;
        } <span class="hljs-keyword">else</span> {
          opacity = <span class="hljs-number">0.5</span>;
        }
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (dragOffset &lt; <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// Dragging down</span>
        <span class="hljs-keyword">if</span> (index === <span class="hljs-number">0</span>) {
          opacity = <span class="hljs-number">1</span>; <span class="hljs-comment">// Keep last card fully opaque when scrolling down</span>
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (index === currentIndex) {
          opacity = <span class="hljs-number">0.7</span>;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (index === currentIndex - <span class="hljs-number">1</span>) {
          opacity = <span class="hljs-number">1</span>;
        } <span class="hljs-keyword">else</span> {
          opacity = <span class="hljs-number">0.5</span>;
        }
      }
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-comment">// Not dragging</span>
      opacity = index === currentIndex ? <span class="hljs-number">1</span> : <span class="hljs-number">0.5</span>;
    }

    <span class="hljs-keyword">return</span> {
      opacity,
      <span class="hljs-attr">transform</span>: <span class="hljs-string">`translateY(<span class="hljs-subst">${offset}</span>%)`</span>,
      <span class="hljs-attr">transition</span>: isDragging ? <span class="hljs-string">'none'</span> : <span class="hljs-string">'transform 200ms ease-in, opacity 500ms ease-in'</span>,
    };
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">className</span>=<span class="hljs-string">"tiktok-cards-container"</span>
      <span class="hljs-attr">ref</span>=<span class="hljs-string">{containerRef}</span>
      <span class="hljs-attr">onWheel</span>=<span class="hljs-string">{handleWheel}</span>
      <span class="hljs-attr">onTouchStart</span>=<span class="hljs-string">{handleTouchStart}</span>
      <span class="hljs-attr">onTouchMove</span>=<span class="hljs-string">{handleTouchMove}</span>
      <span class="hljs-attr">onTouchEnd</span>=<span class="hljs-string">{handleTouchEnd}</span>
      <span class="hljs-attr">onMouseDown</span>=<span class="hljs-string">{handleMouseDown}</span>
      <span class="hljs-attr">onMouseMove</span>=<span class="hljs-string">{handleMouseMove}</span>
      <span class="hljs-attr">onMouseUp</span>=<span class="hljs-string">{handleMouseUp}</span>
      <span class="hljs-attr">onMouseLeave</span>=<span class="hljs-string">{handleMouseUp}</span>
      <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">height</span> }}
    &gt;</span>
      {cards.map((card, index) =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"tiktok-card"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{getCardStyle(index)}</span>&gt;</span>
          {card}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
});
</code></pre>
<p>And the style:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.tiktok-cards-container</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">80vh</span>;
  <span class="hljs-attribute">overflow</span>: hidden;
  <span class="hljs-attribute">position</span>: relative;
  <span class="hljs-attribute">cursor</span>: grab;
}

<span class="hljs-selector-class">.tiktok-cards-container</span><span class="hljs-selector-pseudo">:active</span> {
  <span class="hljs-attribute">cursor</span>: grabbing;
}

<span class="hljs-selector-class">.tiktok-card</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">position</span>: absolute;
  <span class="hljs-attribute">top</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">justify-content</span>: center;
  <span class="hljs-attribute">align-items</span>: center;
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">24px</span>;
  <span class="hljs-attribute">background-color</span>: <span class="hljs-built_in">var</span>(--color-background);
  <span class="hljs-attribute">user-select</span>: none;
}
</code></pre>
<p>It works well on laptop with mouse drag and mouse wheel, and of course on your phone.</p>
<p>Let me know if you have any question, and give my app a try, it’s free and open source: <a target="_blank" href="https://notenote.cc?ref=blog-tiktok-swiper">notenote.cc</a></p>
]]></content:encoded></item><item><title><![CDATA[Build the simplest react router]]></title><description><![CDATA[react-router becomes more and more powerful, and complicated, with a lot of fancy features that I don't need.
So I switched to wouter, it's light weight, easy to use, I enjoyed it for a long time.
Then recently, I thought, why not building my own rou...]]></description><link>https://blog.peng37.com/build-the-simplest-react-router</link><guid isPermaLink="true">https://blog.peng37.com/build-the-simplest-react-router</guid><category><![CDATA[wouter]]></category><category><![CDATA[react router]]></category><category><![CDATA[buildinpublic]]></category><category><![CDATA[spa]]></category><category><![CDATA[router]]></category><category><![CDATA[simple]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Mon, 02 Sep 2024 11:09:58 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://reactrouter.com/en/main">react-router</a> becomes more and more powerful, and complicated, with a lot of fancy features that I don't need.</p>
<p>So I switched to <a target="_blank" href="https://github.com/molefrog/wouter">wouter</a>, it's light weight, easy to use, I enjoyed it for a long time.</p>
<p>Then recently, I thought, why not building my own router, to make it even simpler.</p>
<p>It turns out to be very easy! Here is the full source code, no dependencies, &lt; 100 lines of code, including empty spaces:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useCallback, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">let</span> renderPage = <span class="hljs-function">() =&gt;</span> {};

<span class="hljs-keyword">const</span> handlePopState = <span class="hljs-function">() =&gt;</span> {
  renderPage();
};
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">"popstate"</span>, handlePopState);

<span class="hljs-comment">// Navigate with function</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> navigateTo = <span class="hljs-function">(<span class="hljs-params">to</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (isUrlChanged(to)) {
    <span class="hljs-built_in">window</span>.history.pushState({}, <span class="hljs-string">""</span>, to);
    renderPage();
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> replaceTo = <span class="hljs-function">(<span class="hljs-params">to</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (isUrlChanged(to)) {
    <span class="hljs-built_in">window</span>.history.replaceState({}, <span class="hljs-string">""</span>, to);
    renderPage();
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> goBack = <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">window</span>.history.back();

<span class="hljs-comment">// Navigate with component</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> BabyLink = React.memo(<span class="hljs-function">(<span class="hljs-params">{ to, children }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> handleClick = useCallback(
    <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
      e.preventDefault();
      navigateTo(to);
    },
    [to]
  );

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">{to}</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleClick}</span>&gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span></span>
  );
});

<span class="hljs-comment">/**
 * Main component to organize the routes
 * @param {{[route: string]: React.Component}} routes
 * @param {string} defaultRoute
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> BabyRoutes = React.memo(<span class="hljs-function">(<span class="hljs-params">{ routes, defaultRoute = <span class="hljs-string">"/"</span> }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [page, setPage] = useState(getPageComponent(routes));

  useEffect(<span class="hljs-function">() =&gt;</span> {
    renderPage = <span class="hljs-function">() =&gt;</span> {
      setPage(getPageComponent(routes));
    };
    renderPage();
  }, [routes]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!page) {
      navigateTo(defaultRoute);
    }
  }, [defaultRoute, page]);

  <span class="hljs-keyword">return</span> page;
});

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPageComponent</span>(<span class="hljs-params">routes</span>) </span>{
  <span class="hljs-keyword">const</span> { pathname, search } = <span class="hljs-built_in">window</span>.location;

  <span class="hljs-keyword">if</span> (routes[pathname]) {
    <span class="hljs-keyword">const</span> Component = routes[pathname];
    <span class="hljs-keyword">const</span> queryParams = parseSearch(search);
    <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Component</span> <span class="hljs-attr">queryParams</span>=<span class="hljs-string">{queryParams}</span> /&gt;</span></span>;
  }

  <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parseSearch</span>(<span class="hljs-params">search</span>) </span>{
  <span class="hljs-keyword">const</span> obj = {};

  <span class="hljs-keyword">if</span> (search) {
    <span class="hljs-keyword">const</span> searchParams = <span class="hljs-keyword">new</span> URLSearchParams(search);
    searchParams.forEach(<span class="hljs-function">(<span class="hljs-params">value, key</span>) =&gt;</span> {
      obj[key] = value;
    });
  }

  <span class="hljs-keyword">return</span> obj;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isUrlChanged</span>(<span class="hljs-params">to</span>) </span>{
  <span class="hljs-keyword">const</span> current = <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">window</span>.location.pathname}</span><span class="hljs-subst">${<span class="hljs-built_in">window</span>.location.search}</span>`</span>;
  <span class="hljs-keyword">return</span> current !== to;
}
</code></pre>
<p>I have some examples in its repo: <a target="_blank" href="https://github.com/penghuili/react-baby-router">react-baby-router</a>, and also published it as a lib in <a target="_blank" href="https://www.npmjs.com/package/react-baby-router">npm</a>.</p>
<p>See how I am using it in <a target="_blank" href="https://github.com/penghuili/notenotecc">notenote.cc</a>, let me know if you have any questions.</p>
]]></content:encoded></item><item><title><![CDATA[Browser scrollbar, position fixed, dropdown menu, and jumps]]></title><description><![CDATA[Firstly, let's see a small problem:

This is our situation:

I am using @radix-ui/themes for all the components;

The header is fixed positioned;

When the dropdown menu opens, it adds an overlay, which hides the scrollbar;

So the header's width is ...]]></description><link>https://blog.peng37.com/browser-scrollbar-position-fixed-dropdown-menu-and-jumps</link><guid isPermaLink="true">https://blog.peng37.com/browser-scrollbar-position-fixed-dropdown-menu-and-jumps</guid><category><![CDATA[CSS]]></category><category><![CDATA[position fixed]]></category><category><![CDATA[dropdown]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Sat, 31 Aug 2024 19:35:13 GMT</pubDate><content:encoded><![CDATA[<p>Firstly, let's see a small problem:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725130002256/998bea53-afb5-458f-b498-ebcb9b0a0031.gif" alt class="image--center mx-auto" /></p>
<p>This is our situation:</p>
<ul>
<li><p>I am using <a target="_blank" href="https://www.radix-ui.com/themes/docs/components/theme">@radix-ui/themes</a> for all the components;</p>
</li>
<li><p>The header is fixed positioned;</p>
</li>
<li><p>When the dropdown menu opens, it adds an overlay, which hides the scrollbar;</p>
</li>
<li><p>So the header's width is bigger, then it moves to right a bit;</p>
</li>
<li><p>If you observe it carefully, the image also moves a bit;</p>
</li>
</ul>
<p>It's not a big problem, but we can fix it.</p>
<p>The idea is, always set the header's width to be <code>window width - scrollbar width</code>, no matter there is scrollbar or not.</p>
<p>But how to calculate the width of the scrollbar? Different browsers have different scrollbars.</p>
<p>Easy. See this:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getScrollbarWidth</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> scrollDiv = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'div'</span>);
  scrollDiv.style.width = <span class="hljs-string">'100px'</span>;
  scrollDiv.style.height = <span class="hljs-string">'100px'</span>;
  scrollDiv.style.overflow = <span class="hljs-string">'scroll'</span>;
  scrollDiv.style.position = <span class="hljs-string">'absolute'</span>;
  scrollDiv.style.top = <span class="hljs-string">'-9999px'</span>;

  <span class="hljs-built_in">document</span>.body.appendChild(scrollDiv);
  <span class="hljs-keyword">const</span> scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
  <span class="hljs-built_in">document</span>.body.removeChild(scrollDiv);

  <span class="hljs-keyword">return</span> scrollbarWidth;
}
</code></pre>
<p>We show the scrollbar for a div, and the trick is, <code>offsetWidth</code> includes the bar, but <code>clientWidth</code> doesn't.</p>
<p>Then we can get the width without scrollbar:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> widthWithoutScrollbar = <span class="hljs-built_in">window</span>.innerWidth - getScrollbarWidth()
</code></pre>
<p>After we use this width for body and the fixed header, everything is as still as night:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725132732692/8ccfe6b4-8f8a-445d-9dc3-b15b4d56a127.gif" alt class="image--center mx-auto" /></p>
<p>Enjoy! And try <a target="_blank" href="https://notenote.cc?ref=blog-scrollbar">notenote.cc</a></p>
]]></content:encoded></item><item><title><![CDATA[I built the simplest react state management library (usecat)]]></title><description><![CDATA[There are so many state management libs for react, but they are too complex / not easy to use for me. I only need these:

Create state for something;

Update the state out of component;

Get the state out of component;

A hook to read state within co...]]></description><link>https://blog.peng37.com/i-built-the-simplest-react-state-management-library-usecat</link><guid isPermaLink="true">https://blog.peng37.com/i-built-the-simplest-react-state-management-library-usecat</guid><category><![CDATA[React]]></category><category><![CDATA[React state management]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Fri, 30 Aug 2024 13:51:09 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725025858698/d7833c0d-b70e-465f-ac5b-6f31dc3ed64d.png" alt class="image--center mx-auto" /></p>
<p>There are so many state management libs for react, but they are too complex / not easy to use for me. I only need these:</p>
<ul>
<li><p>Create state for something;</p>
</li>
<li><p>Update the state out of component;</p>
</li>
<li><p>Get the state out of component;</p>
</li>
<li><p>A hook to read state within component;</p>
</li>
</ul>
<p>So I created the lib myself, it's dead simple but very flexible, I have a cat, so I call the lib usecat, <a target="_blank" href="https://github.com/penghuili/usecat">github link here</a>. It's so simple that I can list the whole source code here:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect, useReducer } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">const</span> cats = [];

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createCat</span>(<span class="hljs-params">initialValue</span>) </span>{
  <span class="hljs-keyword">let</span> value = initialValue;
  <span class="hljs-keyword">const</span> listeners = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>();

  <span class="hljs-keyword">const</span> get = <span class="hljs-function">() =&gt;</span> value;

  <span class="hljs-keyword">const</span> set = <span class="hljs-function">(<span class="hljs-params">newValue</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (newValue !== value) {
      <span class="hljs-keyword">const</span> oldValue = value;
      value = newValue;
      listeners.forEach(<span class="hljs-function">(<span class="hljs-params">listener</span>) =&gt;</span> listener(oldValue, newValue));
    }
  };

  <span class="hljs-keyword">const</span> reset = <span class="hljs-function">() =&gt;</span> set(initialValue);

  <span class="hljs-keyword">const</span> subscribe = <span class="hljs-function">(<span class="hljs-params">listener</span>) =&gt;</span> {
    listeners.add(listener);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> listeners.delete(listener);
  };

  <span class="hljs-keyword">const</span> cat = { get, set, reset, subscribe };
  cats.push(cat);

  <span class="hljs-keyword">return</span> cat;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useCat</span>(<span class="hljs-params">cat, selector = (value) =&gt; value</span>) </span>{
  <span class="hljs-keyword">const</span> [, forceUpdate] = useReducer(<span class="hljs-function">(<span class="hljs-params">x</span>) =&gt;</span> x + <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> handler = <span class="hljs-function">(<span class="hljs-params">oldValue, newValue</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> oldSlice = selector(oldValue);
      <span class="hljs-keyword">const</span> newSlice = selector(newValue);
      <span class="hljs-keyword">if</span> (oldSlice !== newSlice) {
        forceUpdate();
      }
    };
    <span class="hljs-keyword">const</span> unsubscribe = cat.subscribe(handler);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> unsubscribe();
  }, [cat]);

  <span class="hljs-keyword">return</span> selector(cat.get());
}
</code></pre>
<h2 id="heading-usage-is-also-simple">Usage is also simple</h2>
<p>Create some cats:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { createCat } <span class="hljs-keyword">from</span> <span class="hljs-string">'usecat'</span>;

<span class="hljs-keyword">const</span> isLoadingTodosCat = createCat(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> todosCat = createCat([]);
</code></pre>
<p>Get and set values out of component:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { isLoadingTodosCat, todosCat } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats'</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchTodos</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> currentTodos = todosCat.get();
  <span class="hljs-keyword">if</span> (currentTodos?.length) {
    <span class="hljs-keyword">return</span>;
  }

  isLoadingTodosCat.set(<span class="hljs-literal">true</span>);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'your-fancy-api'</span>);
    <span class="hljs-keyword">const</span> todos = <span class="hljs-keyword">await</span> response.json();
    todosCat.set(todos);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-comment">// handle error yourself ;)</span>
  }

  isLoadingTodosCat.set(<span class="hljs-literal">false</span>);
}
</code></pre>
<p>Use hook to get value updates:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useCat } <span class="hljs-keyword">from</span> <span class="hljs-string">'usecat'</span>;
<span class="hljs-keyword">import</span> { isLoadingTodosCat, todosCat } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats'</span>
<span class="hljs-keyword">import</span> { fetchTodos } <span class="hljs-keyword">from</span> <span class="hljs-string">'./network'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyComponent</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> isLoading = useCat(isLoadingTodosCat);
  <span class="hljs-keyword">const</span> todos = useCat(todosCat);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchTodos();
  }, [])

  <span class="hljs-keyword">return</span> (
     <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
        {isLoading &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>loading ...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>}
        {todos.map(todo =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{todo.id}</span>&gt;</span>{todo.title}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        ))}
      <span class="hljs-tag">&lt;/&gt;</span></span>
  )
}
</code></pre>
<p>I am using this simple thing to build all my projects, like <a target="_blank" href="https://notenote.cc?ref=blog-cat">notenote.cc</a>, check its <a target="_blank" href="https://github.com/penghuili/notenotecc">source code</a>, it's open.</p>
]]></content:encoded></item><item><title><![CDATA[How to add blurred background to your image with CSS?]]></title><description><![CDATA[I am building notenote.cc, it's a Instagram style note taking app. I want to show photos that users pick from their device in a square frame. Something like this:

After some perplexity and Google, I have a solution.
First try: use css.glass directly...]]></description><link>https://blog.peng37.com/how-to-add-blurred-background-to-your-image-with-css</link><guid isPermaLink="true">https://blog.peng37.com/how-to-add-blurred-background-to-your-image-with-css</guid><category><![CDATA[CSS]]></category><category><![CDATA[images]]></category><category><![CDATA[instagram]]></category><dc:creator><![CDATA[Peng]]></dc:creator><pubDate>Fri, 30 Aug 2024 07:31:14 GMT</pubDate><content:encoded><![CDATA[<p>I am building notenote.cc, it's a Instagram style note taking app. I want to show photos that users pick from their device in a square frame. Something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725001142014/78e8f6b8-7833-426c-9abc-f9ca9f01fa48.png" alt class="image--center mx-auto" /></p>
<p>After some perplexity and Google, I have a solution.</p>
<h3 id="heading-first-try-use-cssglass-directly">First try: use css.glass directly</h3>
<p>I found <a target="_blank" href="https://css.glass/">this website</a>, it has very good blurred glass effect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725001418857/6546adc2-bc27-41fb-83ca-988395ae582d.png" alt class="image--center mx-auto" /></p>
<p>I dragged the sliders, and got this css:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.image-container</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">600px</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">600px</span>;

  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">justify-content</span>: center;
  <span class="hljs-attribute">align-items</span>: center;

  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">rgba</span>(<span class="hljs-number">139</span>, <span class="hljs-number">137</span>, <span class="hljs-number">137</span>, <span class="hljs-number">0.52</span>);
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">4px</span> <span class="hljs-number">30px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.1</span>);
  <span class="hljs-attribute">backdrop-filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">20px</span>);
  <span class="hljs-attribute">-webkit-backdrop-filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">20px</span>);
}

<span class="hljs-selector-class">.image-container</span> <span class="hljs-selector-tag">img</span> {
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">max-height</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">object-fit</span>: contain;
}
</code></pre>
<p>Use it for an image and its wrapper:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.webp"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">"Your image"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>But the background is just plain grey, no blur at all, the effect is not good:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725002079506/8cde8322-ca2a-44fc-8129-7c7e29327f09.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-second-try-add-a-background-image-behind-the-blur-css">Second try: add a background image behind the blur CSS</h3>
<p>The blur effect looks nice at the css.glass website, because there is something (orange color and text) behind. So let's show the image behind the blur.</p>
<p>Now the html looks like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-blur"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.webp"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">"Your image"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>image-container will have the image as background, image-blur will have the blur effect, and img show the full image.</p>
<p>CSS:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.image-container</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">600px</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">600px</span>;

  <span class="hljs-attribute">background-image</span>: <span class="hljs-built_in">url</span>(<span class="hljs-string">'image.webp'</span>);
  <span class="hljs-attribute">background-size</span>: cover;
}

<span class="hljs-selector-class">.image-container</span> <span class="hljs-selector-class">.image-blur</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">100%</span>;

  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">justify-content</span>: center;
  <span class="hljs-attribute">align-items</span>: center;

  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">rgba</span>(<span class="hljs-number">139</span>, <span class="hljs-number">137</span>, <span class="hljs-number">137</span>, <span class="hljs-number">0.52</span>);
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">4px</span> <span class="hljs-number">30px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.1</span>);
  <span class="hljs-attribute">backdrop-filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">20px</span>);
  <span class="hljs-attribute">-webkit-backdrop-filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">20px</span>);
}

<span class="hljs-selector-class">.image-container</span> <span class="hljs-selector-tag">img</span> {
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">max-height</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">object-fit</span>: contain;
}
</code></pre>
<p><code>background-size: cover</code> makes sure that the image covers the whole container, which means only part of the image is the background, but it's fine.</p>
<p>Now the effect is very nice:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725002642775/334d5eaf-7fd6-487f-a401-ceb09e421f7b.png" alt class="image--center mx-auto" /></p>
<p>Thanks for reading and give <a target="_blank" href="https://notenote.cc?ref=blog-imgbg">notenote.cc</a> a try.</p>
]]></content:encoded></item></channel></rss>