<?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[The Build Log by Stefan Skorpen]]></title><description><![CDATA[I'm a full stack developer that likes to code, so I want to share some tips from what I learn by coding in public!]]></description><link>https://blog.skorpen.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 13:54:22 GMT</lastBuildDate><atom:link href="https://blog.skorpen.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Install huggingface cli on Windows 11]]></title><description><![CDATA[First you need to install python, by going to the official python page and download the installer for windows.
When you have python ready, open your terminal app or the command line as administrator.Search for terminal or cmd in the search menu, righ...]]></description><link>https://blog.skorpen.com/install-huggingface-cli-on-windows-11</link><guid isPermaLink="true">https://blog.skorpen.com/install-huggingface-cli-on-windows-11</guid><category><![CDATA[huggingface]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 12 May 2025 19:55:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747079609620/f701aee2-c5b0-44fe-9f2d-b79ae71b3d8a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>First you need to install python, by going to the official python page and download <a target="_blank" href="https://www.python.org/downloads/">the installer for windows</a>.</p>
<p>When you have python ready, open your terminal app or the command line as administrator.<br />Search for terminal or cmd in the search menu, right click the app and select open as administrator.</p>
<p>Install huggingface by typing in “pip install -U "huggingface_hub[cli]"</p>
<p>If the installer says something like “Defaulting to user installation because normal site-packages is not writeable“ then you are in normal mode for the terminal, and not using it as an administrator.</p>
<p>(some also have success by making creating a virtual enviroment for their python project with “python -m venv .env”)</p>
<p>After the huggingface cli is installed, then you have to add the path to the installed files to your PATH variables.</p>
<p>If you dont add the installpath to the PATH then you often get this error:<br />“'huggingface-cli' is not recognized as an internal or external command, operable program or batch file.”</p>
<p>The programs were installed in this location:</p>
<p>“C:\Users\%username%\AppData\Roaming\Python\Python313\Scripts\“</p>
<p>You have to find your real username first, go to “c:\users” and look what users you have created in your windows installation.</p>
<p>Then right click the start menu icon and choose “System”, then look for “Advanced system settings”<br />Then click “Environment Variables”<br />At the bottom part locate the “path” in the list and click edit.</p>
<p>Choose “new” and enter “C:\Users\your_username_here\AppData\Roaming\Python\Python313\Scripts\”<br />make sure to put in your own username.</p>
<p>Restart windows to make sure that the setting is applied.</p>
<p>After the reboot you can open your terminal or cmd app and type in “huggingface-cli —help” and you should see something like the article image as the response.</p>
]]></content:encoded></item><item><title><![CDATA[Find the best prices for image transformations]]></title><description><![CDATA[I have been researching image transformation solutions lately, as I was curious if Cloudinary was the best option for my side project.
I’m still on the free plan for Cloudinary, and I have gotten some extra credits from them for retweeting and doing ...]]></description><link>https://blog.skorpen.com/find-the-best-prices-for-image-transformations</link><guid isPermaLink="true">https://blog.skorpen.com/find-the-best-prices-for-image-transformations</guid><category><![CDATA[image processing]]></category><category><![CDATA[cost-optimisation]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[calculator]]></category><category><![CDATA[Provider]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 18 Nov 2024 19:50:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731959245925/b5253f2a-b6b4-4a66-b0c0-87e980cb8c1b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been researching image transformation solutions lately, as I was curious if Cloudinary was the best option for my side project.</p>
<p>I’m still on the free plan for Cloudinary, and I have gotten some extra credits from them for retweeting and doing some other small advertising tasks for them.</p>
<p>Its kind of hard to figure out exactly how much usage you can get from the free tier, but these are my calculations:</p>
<p>How much can Cloudinary free tier serve with the 25 credit quota:</p>
<p>5000 images à 2MB (10GB of quota) +<br />5000 transformations (5 of quota) +<br />150.000 requests à 70Kb (10 of quota)</p>
<p>If this is month 1, then on month 2 expecting the same amount of transformations and request you would not be able to upload any more images.</p>
<p>But with these numbers I would still be safe on the free plan for the foreseeable future.</p>
<p>ImageKit are kind of similar to Cloudinary and also has a quota based plan system. On their free plan you get:</p>
<p>25GB requests bandwidth<br />5GB image storage quota<br />Unlimited transformations</p>
<p>And with the following amount of images and traffic you would exceed the quota for a month: 2500 images à 2MB (5GB)</p>
<p>375.000 requests à 70Kb (25GB of quota)</p>
<p>So with both these services you would be hard limited by the amount of uploaded images. Not so much by the transformations and traffic.</p>
<p>A new actor in this field is Cloudflare, they advertise with 5000 free transformations each month, but they downplay that they charge for all traffic. So as far as I can see you wont be on a total free plan with them.</p>
<p>And the last provider I considered turned out the be the cheapest by far, not surprising since I guess both Cloudinary and ImageKit bases their platforms on top of AWS.</p>
<p>The pricing of AWS is always tricky to figure out, but I made an attempt at it, where I tried to compare all these 4 services like for like.</p>
<p>Cost calculations premise of the paid versions:</p>
<p>100.000 original images, average size 400Kb - 40GB<br />20% of the library is renewed every month<br />1 million image requests per month, average size 22Kb - 22GB</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td>Store 100.000 images</td><td>Transform 20% monthly</td><td>1 million requests per month</td><td>Total</td><td>Quota used</td></tr>
</thead>
<tbody>
<tr>
<td>AWS</td><td>$2.03 (S3)</td><td>$2.59</td><td>$1.78</td><td>$7.85</td><td></td></tr>
<tr>
<td>Cloudflare</td><td>$5</td><td>$7.5 (first 5k free)</td><td>$10 ($1 per 100k)</td><td>$22.5</td><td></td></tr>
<tr>
<td>Cloudinary</td><td>40 / 256GB used</td><td>20k / 256k used</td><td>22 / 256GB used</td><td>$89</td><td>88 of 256</td></tr>
<tr>
<td>ImageKit</td><td>40 / 225GB used</td><td>Unlimited</td><td>22 / 225GB used</td><td>$89</td><td>62 of 225</td></tr>
</tbody>
</table>
</div><p>So you can see that with these calculations AWS would be 10x cheaper than both Cloudinary and ImageKit, but both these have quite a bit more room in their quotas.</p>
<p>Cloudflare is also a good choice, and it is significantly easier to use than AWS because with AWS you have to setup all the infrastructure yourself. There are good guides for this, but its definitely not for everyone.</p>
<p>Cloudinary offers a good website with a backend for viewing and managing your images, so thats lacking from Cloudflare as far as I can tell.</p>
<p>And both Cloudinary and ImageKit handles videos out of the box, so thats something to consider if you rely on video support.</p>
<p>I also tried calculating the cost of the Cloudinary free tier if hosted on AWS:</p>
<p>Storing of the images in 2 S3 buckets: (5000 images x 2000 Kb + 5000 images x 70 Kb) / 1024 / 1024 x 0.023 = 0.22$</p>
<p>Transforming: seems to be 0.37$</p>
<p>So a total of $0.59 per month.</p>
<p>Some other comparisons between the four providers:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td>Max file size</td><td>Max megapixel</td><td>Max video size</td></tr>
</thead>
<tbody>
<tr>
<td>Imagekit free</td><td>20MB</td><td>25MP</td><td>100MB</td></tr>
<tr>
<td>Imagekit</td><td>40MB</td><td>100MP</td><td>2GB</td></tr>
<tr>
<td>Cloudinary free</td><td>10MB</td><td>25MP</td><td>100MB</td></tr>
<tr>
<td>Cloudimary plus</td><td>20MB</td><td>25MP</td><td>2GB</td></tr>
<tr>
<td>Cloudflare</td><td>10MB</td><td>12MP</td></tr>
</tbody>
</table>
</div><p>AWS and Cloudinary both support these common filetypes: PNG, GIF, JPEG, WebP, HEIC, AVIF, SVG. As well as some animated versions of the same.</p>
<p>While Cloudflare doesnt support HEIC and AVIF, and ImageKit doesnt support HEIC (the apple format)</p>
<p>After all this calculating I decided to make it into a webpage so others can calculate this for themselves. I also added more providers, and more calculators for other SaaS stuff aswell. You can find the best prices for PostgreSQL databases, transactional email, web analytics and authentication providers.</p>
<p>Go to <a target="_blank" href="https://saasprices.net/images">https://saasprices.net/images</a> to see the calculator</p>
]]></content:encoded></item><item><title><![CDATA[How to capture errors and exceptions with Sentry]]></title><description><![CDATA[Keeping track of errors in production is essential to maintaining a healthy application. While console logs and error messages might help during development, you need a more robust solution for production environments. This is where Sentry comes in –...]]></description><link>https://blog.skorpen.com/how-to-capture-errors-and-exceptions-with-sentry</link><guid isPermaLink="true">https://blog.skorpen.com/how-to-capture-errors-and-exceptions-with-sentry</guid><category><![CDATA[error handling]]></category><category><![CDATA[Error Tracking]]></category><category><![CDATA[sentry]]></category><category><![CDATA[exceptionhandling]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Wed, 30 Oct 2024 20:11:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732218509890/3f63deef-77d5-4cef-8add-ff22861dfe59.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Keeping track of errors in production is essential to maintaining a healthy application. While console logs and error messages might help during development, you need a more robust solution for production environments. This is where Sentry comes in – a powerful error tracking service that helps you monitor and fix crashes in real time.</p>
<h2 id="heading-why-sentry">Why Sentry?</h2>
<ul>
<li><p><strong>Real-time error tracking</strong>: Get instant notifications when errors occur</p>
</li>
<li><p><strong>Detailed error context</strong>: Capture stack traces, user information, and environment data</p>
</li>
<li><p><strong>Issue grouping</strong>: Similar errors are automatically grouped for easier debugging</p>
</li>
<li><p><strong>Performance monitoring</strong>: Track application performance alongside errors</p>
</li>
<li><p><strong>Cross-platform support</strong>: Works with multiple frameworks and languages</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<h3 id="heading-1-installation">1. Installation</h3>
<p>First, install the Sentry SDK in your JavaScript project:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Using npm</span>
npm install @sentry/browser

<span class="hljs-comment"># Using yarn</span>
yarn add @sentry/browser
</code></pre>
<h3 id="heading-2-basic-configuration">2. Basic Configuration</h3>
<p>Initialize Sentry as early as possible in your application:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Sentry <span class="hljs-keyword">from</span> <span class="hljs-string">"@sentry/browser"</span>;

Sentry.init({
  <span class="hljs-attr">dsn</span>: <span class="hljs-string">"your-dsn-here"</span>,
  <span class="hljs-comment">// Set tracesSampleRate to 1.0 to capture 100% of transactions</span>
  <span class="hljs-attr">tracesSampleRate</span>: <span class="hljs-number">1.0</span>,
  <span class="hljs-attr">environment</span>: process.env.NODE_ENV
});
</code></pre>
<h3 id="heading-3-capturing-errors">3. Capturing Errors</h3>
<h4 id="heading-automatic-error-capturing">Automatic Error Capturing</h4>
<p>Once initialized, Sentry automatically captures unhandled exceptions and promise rejections:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// This error will be automatically captured</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"This is an example error"</span>);
</code></pre>
<h4 id="heading-manual-error-capturing">Manual Error Capturing</h4>
<p>For more control, you can manually capture errors:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">try</span> {
  someRiskyOperation();
} <span class="hljs-keyword">catch</span> (error) {
  Sentry.captureException(error);
}

<span class="hljs-comment">// Or capture a custom message</span>
Sentry.captureMessage(<span class="hljs-string">"Something went wrong"</span>, <span class="hljs-string">"error"</span>);
</code></pre>
<h3 id="heading-4-adding-context">4. Adding Context</h3>
<p>Make your error reports more useful by adding extra context:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Add user context</span>
Sentry.setUser({
  <span class="hljs-attr">id</span>: <span class="hljs-string">"123"</span>,
  <span class="hljs-attr">email</span>: <span class="hljs-string">"user@example.com"</span>,
  <span class="hljs-attr">username</span>: <span class="hljs-string">"johnDoe"</span>
});

<span class="hljs-comment">// Add tags for better filtering</span>
Sentry.setTag(<span class="hljs-string">"page_locale"</span>, <span class="hljs-string">"de-at"</span>);

<span class="hljs-comment">// Add extra context</span>
Sentry.setExtra(<span class="hljs-string">"character_count"</span>, <span class="hljs-number">5</span>);
</code></pre>
<h3 id="heading-5-custom-error-breadcrumbs">5. Custom Error Breadcrumbs</h3>
<p>Breadcrumbs help you understand what happened before an error:</p>
<pre><code class="lang-javascript">Sentry.addBreadcrumb({
  <span class="hljs-attr">category</span>: <span class="hljs-string">"auth"</span>,
  <span class="hljs-attr">message</span>: <span class="hljs-string">"Attempting user login"</span>,
  <span class="hljs-attr">level</span>: <span class="hljs-string">"info"</span>
});
</code></pre>
<h2 id="heading-best-practices">Best Practices</h2>
<ol>
<li><strong>Filter Sensitive Data</strong></li>
</ol>
<pre><code class="lang-javascript">Sentry.init({
  <span class="hljs-attr">dsn</span>: <span class="hljs-string">"your-dsn-here"</span>,
  beforeSend(event) {
    <span class="hljs-comment">// Don't send passwords or personal info</span>
    <span class="hljs-keyword">if</span> (event.request?.headers?.[<span class="hljs-string">"Authorization"</span>]) {
      <span class="hljs-keyword">delete</span> event.request.headers[<span class="hljs-string">"Authorization"</span>];
    }
    <span class="hljs-keyword">return</span> event;
  }
});
</code></pre>
<ol start="2">
<li><strong>Set Release Information</strong></li>
</ol>
<pre><code class="lang-javascript">Sentry.init({
  <span class="hljs-attr">dsn</span>: <span class="hljs-string">"your-dsn-here"</span>,
  <span class="hljs-attr">release</span>: <span class="hljs-string">"my-app@1.0.0"</span>,
  <span class="hljs-attr">environment</span>: process.env.NODE_ENV
});
</code></pre>
<ol start="3">
<li><strong>Configure Error Sampling</strong></li>
</ol>
<pre><code class="lang-javascript">Sentry.init({
  <span class="hljs-attr">dsn</span>: <span class="hljs-string">"your-dsn-here"</span>,
  <span class="hljs-attr">sampleRate</span>: <span class="hljs-number">0.5</span> <span class="hljs-comment">// Only send 50% of errors</span>
});
</code></pre>
<h2 id="heading-framework-specific-integration">Framework-Specific Integration</h2>
<h3 id="heading-react">React</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Sentry <span class="hljs-keyword">from</span> <span class="hljs-string">"@sentry/react"</span>;

<span class="hljs-comment">// Error boundary component</span>
<span class="hljs-keyword">const</span> FallbackComponent = <span class="hljs-function">() =&gt;</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>An error has occurred<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Sentry.withErrorBoundary(YourComponent, {
  <span class="hljs-attr">fallback</span>: FallbackComponent
});
</code></pre>
<h3 id="heading-vue">Vue</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Sentry <span class="hljs-keyword">from</span> <span class="hljs-string">"@sentry/vue"</span>;

Sentry.init({
  Vue,
  <span class="hljs-attr">dsn</span>: <span class="hljs-string">"your-dsn-here"</span>
});
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Implementing Sentry in your JavaScript application is a crucial step toward maintaining a reliable production environment. With its powerful features and easy integration, you can quickly start capturing and analyzing errors to improve your application's stability.</p>
<p>Remember to:</p>
<ul>
<li><p>Initialize Sentry as early as possible in your application</p>
</li>
<li><p>Add relevant context to your error reports</p>
</li>
<li><p>Configure error sampling based on your needs</p>
</li>
<li><p>Filter out sensitive information</p>
</li>
<li><p>Use framework-specific integrations when available</p>
</li>
</ul>
<p>By following these guidelines, you'll have a robust error tracking system that helps you identify and fix issues before they impact your users.</p>
]]></content:encoded></item><item><title><![CDATA[How to create backend events with PostHog]]></title><description><![CDATA[It's important to keep track of events in your webapp so you can see what features are used and how they are used. While frontend event tracking is common, capturing backend events can provide deeper insights into your application's behavior, perform...]]></description><link>https://blog.skorpen.com/how-to-create-backend-events-with-posthog</link><guid isPermaLink="true">https://blog.skorpen.com/how-to-create-backend-events-with-posthog</guid><category><![CDATA[events]]></category><category><![CDATA[backend]]></category><category><![CDATA[posthog]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Wed, 30 Oct 2024 20:06:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732219026370/6ff89cc7-cebd-4994-ab66-421e60221365.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's important to keep track of events in your webapp so you can see what features are used and how they are used. While frontend event tracking is common, capturing backend events can provide deeper insights into your application's behavior, performance, and user patterns. With PostHog, it's quite easy to create events in your backend.</p>
<h2 id="heading-setting-up-posthog">Setting Up PostHog</h2>
<p>First, install the PostHog Node.js library:</p>
<pre><code class="lang-bash">npm install posthog-node
<span class="hljs-comment"># or</span>
yarn add posthog-node
</code></pre>
<h2 id="heading-initializing-posthog">Initializing PostHog</h2>
<p>Initialize PostHog with your project API key:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { PostHog } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'posthog-node'</span>)

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> PostHog(
    <span class="hljs-string">'phc_YOUR_API_KEY'</span>,
    { 
        <span class="hljs-attr">host</span>: <span class="hljs-string">'https://app.posthog.com'</span> <span class="hljs-comment">// Optional: Use your self-hosted instance URL if applicable</span>
    }
)
</code></pre>
<h2 id="heading-capturing-basic-events">Capturing Basic Events</h2>
<p>The simplest way to capture an event is using the <code>capture</code> method:</p>
<pre><code class="lang-javascript">client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'subscription_created'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">planType</span>: <span class="hljs-string">'premium'</span>,
        <span class="hljs-attr">billingInterval</span>: <span class="hljs-string">'monthly'</span>,
        <span class="hljs-attr">amount</span>: <span class="hljs-number">29.99</span>
    }
})
</code></pre>
<h2 id="heading-best-practices-for-backend-events">Best Practices for Backend Events</h2>
<h3 id="heading-1-consistent-event-naming">1. Consistent Event Naming</h3>
<p>Use a consistent naming convention for your events. A common pattern is:</p>
<ul>
<li><p>Noun_Verb for state changes: <code>subscription_created</code>, <code>payment_failed</code></p>
</li>
<li><p>Verb_Noun for actions: <code>generate_report</code>, <code>send_notification</code></p>
</li>
</ul>
<h3 id="heading-2-rich-properties">2. Rich Properties</h3>
<p>Include relevant properties that will be useful for analysis:</p>
<pre><code class="lang-javascript">client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'api_request_completed'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">endpoint</span>: <span class="hljs-string">'/api/v1/users'</span>,
        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
        <span class="hljs-attr">responseTimeMs</span>: <span class="hljs-number">235</span>,
        <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
        <span class="hljs-attr">userAgent</span>: req.headers[<span class="hljs-string">'user-agent'</span>],
        <span class="hljs-attr">ipAddress</span>: req.ip
    }
})
</code></pre>
<h3 id="heading-3-batch-processing">3. Batch Processing</h3>
<p>For high-volume events, use batch capturing to improve performance:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> events = [
    {
        <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
        <span class="hljs-attr">event</span>: <span class="hljs-string">'item_purchased'</span>,
        <span class="hljs-attr">properties</span>: { <span class="hljs-attr">itemId</span>: <span class="hljs-string">'456'</span>, <span class="hljs-attr">price</span>: <span class="hljs-number">29.99</span> }
    },
    {
        <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
        <span class="hljs-attr">event</span>: <span class="hljs-string">'checkout_completed'</span>,
        <span class="hljs-attr">properties</span>: { <span class="hljs-attr">total</span>: <span class="hljs-number">29.99</span>, <span class="hljs-attr">itemsCount</span>: <span class="hljs-number">1</span> }
    }
]

<span class="hljs-keyword">await</span> client.batchCapture(events)
</code></pre>
<h2 id="heading-advanced-features">Advanced Features</h2>
<h3 id="heading-1-feature-flags">1. Feature Flags</h3>
<p>You can evaluate feature flags in your backend code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> flagEnabled = <span class="hljs-keyword">await</span> client.isFeatureEnabled(<span class="hljs-string">'new_billing_flow'</span>, <span class="hljs-string">'user_123'</span>)
<span class="hljs-keyword">if</span> (flagEnabled) {
    <span class="hljs-comment">// Roll out new billing logic</span>
}
</code></pre>
<h3 id="heading-2-group-analytics">2. Group Analytics</h3>
<p>Track events at an organization level:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Identify a group</span>
<span class="hljs-keyword">await</span> client.groupIdentify({
    <span class="hljs-attr">groupType</span>: <span class="hljs-string">'company'</span>,
    <span class="hljs-attr">groupKey</span>: <span class="hljs-string">'company_123'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'Acme Corp'</span>,
        <span class="hljs-attr">industry</span>: <span class="hljs-string">'Technology'</span>,
        <span class="hljs-attr">employeeCount</span>: <span class="hljs-number">500</span>
    }
})

<span class="hljs-comment">// Capture event with group information</span>
<span class="hljs-keyword">await</span> client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'project_created'</span>,
    <span class="hljs-attr">properties</span>: { <span class="hljs-attr">projectName</span>: <span class="hljs-string">'New Project'</span> },
    <span class="hljs-attr">groups</span>: { <span class="hljs-attr">company</span>: <span class="hljs-string">'company_123'</span> }
})
</code></pre>
<h3 id="heading-3-error-handling">3. Error Handling</h3>
<p>Implement robust error handling to prevent event capture failures from affecting your application:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> client.capture({
        <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
        <span class="hljs-attr">event</span>: <span class="hljs-string">'important_action'</span>,
        <span class="hljs-attr">properties</span>: { <span class="hljs-attr">key</span>: <span class="hljs-string">'value'</span> }
    })
} <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to capture PostHog event:'</span>, error)
    <span class="hljs-comment">// Consider adding to a retry queue</span>
}
</code></pre>
<h2 id="heading-common-patterns-and-use-cases">Common Patterns and Use Cases</h2>
<p>Here are some typical backend events you might want to track:</p>
<ol>
<li><strong>Authentication Events</strong></li>
</ol>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'login_attempted'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'password'</span>,
        <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">userAgent</span>: req.headers[<span class="hljs-string">'user-agent'</span>],
        <span class="hljs-attr">ipAddress</span>: req.ip
    }
})
</code></pre>
<ol start="2">
<li><strong>Background Job Events</strong></li>
</ol>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'system'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'scheduled_job_completed'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">jobName</span>: <span class="hljs-string">'daily_report_generation'</span>,
        <span class="hljs-attr">durationSeconds</span>: <span class="hljs-number">145</span>,
        <span class="hljs-attr">recordsProcessed</span>: <span class="hljs-number">1000</span>,
        <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>
    }
})
</code></pre>
<ol start="3">
<li><strong>API Usage Events</strong></li>
</ol>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> client.capture({
    <span class="hljs-attr">distinctId</span>: <span class="hljs-string">'user_123'</span>,
    <span class="hljs-attr">event</span>: <span class="hljs-string">'api_key_generated'</span>,
    <span class="hljs-attr">properties</span>: {
        <span class="hljs-attr">keyType</span>: <span class="hljs-string">'read_only'</span>,
        <span class="hljs-attr">expirationDays</span>: <span class="hljs-number">30</span>,
        <span class="hljs-attr">permissions</span>: [<span class="hljs-string">'read_users'</span>, <span class="hljs-string">'read_projects'</span>]
    }
})
</code></pre>
<h2 id="heading-shutdown">Shutdown</h2>
<p>When your application is shutting down, make sure to close the PostHog client to ensure all events are sent:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> client.shutdown()
</code></pre>
<h2 id="heading-using-with-expressjs">Using with Express.js</h2>
<p>Here's an example of integrating PostHog with an Express.js middleware:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>)
<span class="hljs-keyword">const</span> app = express()

<span class="hljs-comment">// Middleware to track API requests</span>
app.use(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> startTime = <span class="hljs-built_in">Date</span>.now()

    <span class="hljs-comment">// Continue with the request</span>
    next()

    <span class="hljs-comment">// After the request is completed</span>
    res.on(<span class="hljs-string">'finish'</span>, <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">await</span> client.capture({
                <span class="hljs-attr">distinctId</span>: req.user?.id || <span class="hljs-string">'anonymous'</span>,
                <span class="hljs-attr">event</span>: <span class="hljs-string">'api_request'</span>,
                <span class="hljs-attr">properties</span>: {
                    <span class="hljs-attr">path</span>: req.path,
                    <span class="hljs-attr">method</span>: req.method,
                    <span class="hljs-attr">statusCode</span>: res.statusCode,
                    <span class="hljs-attr">duration</span>: <span class="hljs-built_in">Date</span>.now() - startTime,
                    <span class="hljs-attr">userAgent</span>: req.headers[<span class="hljs-string">'user-agent'</span>]
                }
            })
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to capture API request event:'</span>, error)
        }
    })
})
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Backend event tracking with PostHog in Node.js provides valuable insights into your application's behavior and usage patterns. By following these best practices and patterns, you can build a robust event tracking system that helps you make data-driven decisions about your product.</p>
<p>Remember to:</p>
<ul>
<li><p>Use consistent naming conventions</p>
</li>
<li><p>Include relevant properties with each event</p>
</li>
<li><p>Handle errors gracefully</p>
</li>
<li><p>Use batch processing for high-volume events</p>
</li>
<li><p>Properly shut down the client when your application terminates</p>
</li>
<li><p>Leverage advanced features like feature flags and group analytics when appropriate</p>
</li>
</ul>
<p>For more information, check out the <a target="_blank" href="https://posthog.com/docs/libraries/node">PostHog Node.js documentation</a> for detailed API references and additional features.</p>
]]></content:encoded></item><item><title><![CDATA[Find and replace bad words and obscenities with React]]></title><description><![CDATA[If you allow your users to create content in your webapp, or if you make your own comment component, you would want to moderate the text that the user types in.
In my side project I want the users to create products with descriptions, as well as user...]]></description><link>https://blog.skorpen.com/obscenity-filter</link><guid isPermaLink="true">https://blog.skorpen.com/obscenity-filter</guid><category><![CDATA[censor]]></category><category><![CDATA[React]]></category><category><![CDATA[Moderation]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Fri, 11 Oct 2024 10:00:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728315879087/ac254a2d-afa0-41ae-94eb-e99bd4be2a26.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you allow your users to create content in your webapp, or if you make your own comment component, you would want to moderate the text that the user types in.</p>
<p>In my side project I want the users to create products with descriptions, as well as usernames and slugs for urls. I check all of these for profanities or swear words as best as I can.</p>
<p>I found this handy library called “<a target="_blank" href="https://www.npmjs.com/package/obscenity">obscenity</a>”, and it already has a large library of forbidden words. This is perfect for this use case and this way I dont have to come up with all the bad words myself!</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
    RegExpMatcher,
    TextCensor,
    englishDataset,
    englishRecommendedTransformers,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'obscenity'</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badWordsDetector</span>(<span class="hljs-params">input: string</span>) </span>{
<span class="hljs-comment">// the input example is: 'fuck you little bitch'</span>

    <span class="hljs-keyword">const</span> matcher = <span class="hljs-keyword">new</span> RegExpMatcher({
        ...englishDataset.build(),
        ...englishRecommendedTransformers,
    });

    <span class="hljs-keyword">const</span> censor = <span class="hljs-keyword">new</span> TextCensor();
    <span class="hljs-keyword">const</span> matches = matcher.getAllMatches(input);

    <span class="hljs-keyword">return</span> censor.applyTo(input, matches);

    <span class="hljs-comment">// the censored text then returns: '%@$% you little **%@%'</span>

}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[The easiest way to add SVG icons in React]]></title><description><![CDATA[SVG images can be used a couple different ways in react, this is my favorite option.
I create a file for the svg, and make it into a JSX component.
export default function FacebookSvg() {
    return (<div>
        <svg ... />
    </div>
    )
}

Simp...]]></description><link>https://blog.skorpen.com/the-easiest-way-to-add-svg-icons-in-react</link><guid isPermaLink="true">https://blog.skorpen.com/the-easiest-way-to-add-svg-icons-in-react</guid><category><![CDATA[SVG]]></category><category><![CDATA[React]]></category><category><![CDATA[icon]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Tue, 08 Oct 2024 10:05:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728313962252/6d07a7aa-9201-4275-a01f-f644b2654301.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SVG images can be used a couple different ways in react, this is my favorite option.</p>
<p>I create a file for the svg, and make it into a JSX component.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FacebookSvg</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">...</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    )
}
</code></pre>
<p>Simple Icons is a good site for finding brand icons like Facebook. <a target="_blank" href="https://simpleicons.org/?q=facebbok">https://simpleicons.org/?q=facebbok</a></p>
<p>Just search for the brand and click the image to copy the svg code.</p>
<p>We have to do some edits to make this code behave the way I like, such as replacing or adding the height and width properties, and change the kebab-case properties to camelCase so it works for React.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// add width and height</span>
&lt;svg
width=<span class="hljs-string">"100%"</span>
height=<span class="hljs-string">"100%"</span>
...
/&gt;
</code></pre>
<p>Setting the width and height to 100% makes you able to scale the svg by setting a size on the wrapping container in HTML.</p>
<pre><code class="lang-jsx">&lt;div style=<span class="hljs-string">"width: 30px; height: 30px;"</span>&gt;
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">FacebookSVG</span> /&gt;</span></span>
&lt;/div&gt;
</code></pre>
<pre><code class="lang-jsx"><span class="hljs-comment">// replace kebab-case with camelCase</span>
clip-path becomes clipPath
<span class="hljs-attr">xlink</span>:type becomes xlinkType
etc
</code></pre>
<p>The icons from Simple Icons all have white color, but if you want to set another color you can pass that as a prop to the SVG</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FacebookSvg</span>(<span class="hljs-params">fillColor: string </span>) </span>{
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">svg</span> 
            <span class="hljs-attr">height</span>=<span class="hljs-string">"100%"</span>
            <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span>
            <span class="hljs-attr">role</span>=<span class="hljs-string">"img"</span>
            <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span> 
            <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Facebook<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">path</span> 
                <span class="hljs-attr">fill</span>=<span class="hljs-string">{fillColor}</span>
                // <span class="hljs-attr">or</span> <span class="hljs-attr">do:</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"currentColor"</span>
                <span class="hljs-attr">d</span>=<span class="hljs-string">"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"</span>/&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span></span>)
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Create QR codes in React.js]]></title><description><![CDATA[QR codes are an essential part of the first MVP for my side project.
My customers can create QR codes to link back to their profiles, embedding the image in their SoMe posts or videos.
The project is made in next.js so I went for the react-qr-code li...]]></description><link>https://blog.skorpen.com/create-qr-codes-in-reactjs</link><guid isPermaLink="true">https://blog.skorpen.com/create-qr-codes-in-reactjs</guid><category><![CDATA[Qrcode]]></category><category><![CDATA[React]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[side project]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 07 Oct 2024 14:50:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728312355463/75af342e-df4c-4752-b8ec-a1c3de77aa77.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>QR codes are an essential part of the first MVP for my side project.</p>
<p>My customers can create QR codes to link back to their profiles, embedding the image in their SoMe posts or videos.</p>
<p>The project is made in next.js so I went for the react-qr-code library.</p>
<p>You just have to give the QRCode component a string value with the info for the QR code, and most of the props are optional.</p>
<p>But you can change the colors, size and complexity for the QR code.<br />The level is different variants of error checking, so when you use a smaller QR code image it might be best to go for the largest “L” level, and move towards the “M” level the larger the image you create.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> QRCode <span class="hljs-keyword">from</span> <span class="hljs-string">"react-qr-code"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">QRCodeComponent</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> url = <span class="hljs-string">`https://affill.io/<span class="hljs-subst">${username}</span>/<span class="hljs-subst">${slug}</span>`</span>;

  <span class="hljs-keyword">const</span> [size, setSize] = useState(<span class="hljs-number">128</span>);
  <span class="hljs-keyword">const</span> [level, setLevel] = useState&lt;<span class="hljs-string">"L"</span> | <span class="hljs-string">"M"</span> | <span class="hljs-string">"Q"</span> | <span class="hljs-string">"H"</span>&gt;(<span class="hljs-string">"M"</span>);
  <span class="hljs-keyword">const</span> [backgroundColor, setBackgroundColor] = useState(<span class="hljs-string">"#ffffff"</span>);
  <span class="hljs-keyword">const</span> [color, setColor] = useState(<span class="hljs-string">"#000000"</span>);

  <span class="hljs-keyword">return</span> (<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">QRCode</span>
      <span class="hljs-attr">size</span>=<span class="hljs-string">{size}</span>
      <span class="hljs-attr">value</span>=<span class="hljs-string">{url}</span>
      <span class="hljs-attr">viewBox</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">0</span> <span class="hljs-attr">0</span> ${<span class="hljs-attr">size</span>} ${<span class="hljs-attr">size</span>}`}
      <span class="hljs-attr">id</span>=<span class="hljs-string">"QRCode"</span>
      <span class="hljs-attr">level</span>=<span class="hljs-string">{level}</span>
      <span class="hljs-attr">bgColor</span>=<span class="hljs-string">{backgroundColor}</span>
      <span class="hljs-attr">fgColor</span>=<span class="hljs-string">{color}</span>
  /&gt;</span></span>)
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728312416010/d1ba343e-3550-4e0e-82eb-dd8f670ac499.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Add a trigger in postgres to make a slug]]></title><description><![CDATA[A post in Affill.io, the SaaS I’m making, can have a customizable slug as its URL. But only if the post is made from a subscribed account, if its a free account the slug will be the posts id.
If I just make a new row for the free accounts post I woul...]]></description><link>https://blog.skorpen.com/add-a-trigger-in-postgres</link><guid isPermaLink="true">https://blog.skorpen.com/add-a-trigger-in-postgres</guid><category><![CDATA[SQL]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[supabase]]></category><category><![CDATA[trigger]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Sat, 05 Oct 2024 10:46:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728125008666/49d842b3-09b6-46e7-ae8a-007c12c194f2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A post in Affill.io, the SaaS I’m making, can have a customizable slug as its URL. But only if the post is made from a subscribed account, if its a free account the slug will be the posts id.</p>
<p>If I just make a new row for the free accounts post I would have to make the slug column empty since I dont know what the next id would be.</p>
<p>So how can I use the database id as the slug in the new database row if I dont know what the id will be?</p>
<p>The answer I found for this is database triggers. I use supabase as the database host for this project, and I can either enter plain SQL to make the trigger and the function, or I can use their UI in the admin part of supabase.</p>
<p>The PL SQL code for the function to get the id of the new post would be something like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">FUNCTION</span> create_post_id_as_slug() <span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">trigger</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> NEW.paid <span class="hljs-keyword">is</span> <span class="hljs-literal">FALSE</span> <span class="hljs-keyword">THEN</span>
    NEW.slug := NEW.id;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
    RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>Here I have first added a column called “paid”, and I do a check in javascript before inserting the row to the database. If the user has a subscription I set the paid property of the javascript object to true, if its a free account it gets set to false.</p>
<p>“NEW” in the function is the data for the row we are inserting, so it contains all the columns you defined in the insert query.</p>
<p>We check if the “paid” is false, and if so - we set the “slug” to be equal to the “id” that the postgres database has now generated for this new row.</p>
<p>Now we have to make the trigger that makes postgres run this function. The trigger is simple and looks like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> post_slug_trigger
<span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ON</span> <span class="hljs-string">"Post"</span>
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> create_post_id_as_slug()
</code></pre>
<p>Triggers can be set to run either BEFORE or AFTER the action.<br />And actions can be insert, update, delete on the row.<br />And triggers can run for each row, or after the whole statement. Here we want to run it per row. This means that if we do 10 inserts, the trigger is run for every insert - total 10 times.</p>
<p>If its set to run after the statement and we say update 10 rows with one query, the trigger would then run only once.</p>
<p>So now I can get these kind of urls for paid users: <a target="_blank" href="https://affill.io/skorpio/lego-collection">https://affill.io/skorpio/lego-collection</a></p>
<p>And free users gets the post id as its url/slug like this: <a target="_blank" href="https://affill.io/example/1003">https://affill.io/example/1003</a></p>
<p>In the supabase UI you can find functions and triggers under the “Database” menu choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728124193908/ddbe44d2-fba3-4666-a8dd-171886d8f600.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728124201263/7dc99d6a-238b-4e6f-9587-e40e0d3451cd.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Typesense and docker compose with SSL certificates]]></title><description><![CDATA[I needed fast and robust search for the website I'm working on Affill.io
So I decided to go for the opensource tool Typesense, and I wanted to self host it.
I went for a small CX11 vps from Hetzner, and after following their guide on using docker com...]]></description><link>https://blog.skorpen.com/typesense-and-docker-compose-with-ssl-certificates</link><guid isPermaLink="true">https://blog.skorpen.com/typesense-and-docker-compose-with-ssl-certificates</guid><category><![CDATA[typesense]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[symlinks]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 15 Apr 2024 18:52:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713207010418/afc16c7e-4485-4f2e-b7e3-f30f6b6fadeb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I needed fast and robust search for the website I'm working on <a target="_blank" href="https://affill.io">Affill.io</a></p>
<p>So I decided to go for the opensource tool Typesense, and I wanted to self host it.</p>
<p>I went for a small CX11 vps from Hetzner, and after following their guide on using <a target="_blank" href="https://typesense.org/docs/guide/install-typesense.html#docker-compose">docker compose</a> I got it up and running. I dont have much data yet so the 2GB RAM is enough for now.</p>
<p>But I wanted to have my own domain for the search and get a SSL certificate from Let's encrypt.<br />It's been a while since I worked with linux, but the excellent <a target="_blank" href="https://certbot.eff.org/">howto from certbot</a> on how to get the certificate generated helped me get up and running.</p>
<p>Now all that was left was to get typesense to work with my certificates, I made the <code>docker-compose.yml</code> file and tried to start the container with <code>docker compose up -d</code></p>
<p>But the server just kept on restarting</p>
<pre><code class="lang-bash">NAME                    IMAGE                        COMMAND                  SERVICE     CREATED          STATUS                        PORTS
typesense-typesense-1   typesense/typesense:0.25.2   <span class="hljs-string">"/opt/typesense-serv…"</span>   typesense   41 seconds ago   Restarting (1) 1 second ago
</code></pre>
<p>I ran <code>docker compose logs</code> and could see that typesense couldnt find my certification files.</p>
<pre><code class="lang-bash">typesense-1  | E20240415 18:01:05.855815     1 http_server.cpp:1069] An error occurred <span class="hljs-keyword">while</span> trying to load server certificate file: /cert/fullchain.pem
typesense-1  | E20240415 18:01:05.861860     1 http_server.cpp:175] Failed to listen on 0.0.0.0:8108 - No such file or directory
</code></pre>
<p>The error was that I tried to map the domain folder of letsencrypt</p>
<pre><code class="lang-bash">- /etc/letsencrypt/live/www.example.com:/cert
</code></pre>
<p>When mounting this folder the symlink wouldnt work, so I had to change the folder mount to be the /etc/letsencrypt folder instead.</p>
<p>This is my final docker-compose.yml file:</p>
<pre><code class="lang-bash">version: <span class="hljs-string">'3.4'</span>
services:
  typesense:
    image: typesense/typesense:0.25.2
    restart: on-failure
    ports:
      - <span class="hljs-string">"443:8108"</span>
    volumes:
      - ./typesense-data:/data
      - ./<span class="hljs-built_in">log</span>:/<span class="hljs-built_in">log</span>
      - /etc/letsencrypt:/cert
    <span class="hljs-built_in">command</span>: <span class="hljs-string">'--data-dir /data --log-dir /log --enable-cors --ssl-certificate /cert/live/www.example.com/fullchain.pem --ssl-certificate-key /cert/live/www.example.com/privkey.pem</span>
</code></pre>
<p>And now everything works!</p>
]]></content:encoded></item><item><title><![CDATA[Change wheel motor on the Gardena Sileno+ 1600]]></title><description><![CDATA[My robot lawnmower started running in a circle, and the left wheel made a metallic noise when turning. So I figured I had to change the motor.
I couldnt find any information about how to disassemble or repair this lawnmower, so I figured I could just...]]></description><link>https://blog.skorpen.com/change-wheel-motor-on-the-gardena-sileno-1600</link><guid isPermaLink="true">https://blog.skorpen.com/change-wheel-motor-on-the-gardena-sileno-1600</guid><category><![CDATA[repair]]></category><category><![CDATA[DIY]]></category><category><![CDATA[motor]]></category><category><![CDATA[Gardena]]></category><category><![CDATA[lawnmower]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Sat, 14 Oct 2023 17:47:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697305548268/58fa440e-ba48-4feb-9dd0-784bd16f310a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My <a target="_blank" href="https://www.gardena.com/no/produkter/plen-pleie/robotklipper/smart-sileno-1-600-m2-sett/967264605/">robot lawnmower</a> started running in a circle, and the left wheel made a metallic noise when turning. So I figured I had to change the motor.</p>
<p>I couldnt find any information about how to disassemble or repair this lawnmower, so I figured I could just wing it. And this blog post is made to help out anyone else needing to repair their Gardena lawnmower.</p>
<p>I found the part number to be the 587 44 94-02 motor kit (<a target="_blank" href="https://www.gplshop.co.uk/en/wheelmotor">looks like this</a>), I ordered a new <a target="_blank" href="https://www.gplshop.co.uk/en/seal-chassi">rubber sealing strip</a> aswell. These are original parts that can be used both for Gardena and Husqvarna. In Norway the best place to get these parts seems to be <a target="_blank" href="https://www.gardenaservice.net/">Gartnerservice</a></p>
<p>This is how I disassembled the lawnmower:</p>
<p>Step 1 - disconnect the battery<br />Turn off the lawnmower, open the battery cover by removing the 4 torx bolts</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697291784101/744ec7a9-a870-40ec-8a1a-43e73e363b7a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697291711830/39726e72-ea8d-4474-8f1e-6b989909cec6.png" alt class="image--center mx-auto" /></p>
<p>Close the cover to avoid getting dirt inside the battery compartment.</p>
<p>Step 2 - remove the rear cover<br />Two torx bolts holds the rear cover attached.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697291849434/0634dafd-e7f6-4378-9529-3dab6a422316.png" alt class="image--center mx-auto" /></p>
<p>Step 3 - remove the top plastic cover(s)<br />The light gray top cover is attached by plastic clips.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697291959486/78f2ab12-0e32-447f-9ac8-1c3ca7f677b8.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304995531/58abb8e4-07cc-4474-b65c-0b5424284095.png" alt class="image--center mx-auto" /></p>
<p>Step 4 - release top chassis from the shock absorbers<br />The shock absorber gaskets should be slid out of their sockets. They can be quite stuck, I had to lube with some WD-40 and use quite some power to get them slid into the open position. It might be best to start with the front one, but this is attached to the shock detector so be a bit careful</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304380672/9a7c554e-34fc-4c59-9d5a-c135aabffc85.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304445164/dc7f317e-2af0-4788-9fa8-0b042404b03a.png" alt class="image--center mx-auto" /></p>
<p>Step 5 - disconnect the charging cable<br />Remember to turn off the lawnmower and disconnect the battery before doing this step.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304546576/e50ed68a-a02d-4ba7-a517-f420a6cf6dd7.png" alt class="image--center mx-auto" /></p>
<p>Step 6 - open main chassis<br />Remove the marked torx bolts.<br />Remember the top of the main chassis has the controls and the display, these are connected with wires so be careful when lifting the top chassis.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304630152/895fefa1-df2f-4530-8d87-146eabfcf301.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304697514/3bbc33f0-7820-4d40-8119-d410c8ea394b.png" alt class="image--center mx-auto" /></p>
<p>Step 7 - disconnect wheel motor<br />Unclip the cable</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304783640/efc789be-5d81-4844-8f59-065edabdd545.png" alt class="image--center mx-auto" /></p>
<p>Step 8 - remove wheel motor<br />Remove wheel cover, loosen wheel nut.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304833789/dfaefc71-797b-4cb9-8168-d1fa268dfe94.png" alt class="image--center mx-auto" /></p>
<p>Remove the 4 torx bolts, slide the motor out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304880518/6cb07cdb-ffe4-42ed-a4d4-cc981ccee13e.png" alt class="image--center mx-auto" /></p>
<p>Step 9 - replace sealing strip<br />Overlap the strip a bit at the starting point, make sure to press the strip gently down into the groove.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697304955474/c9ba5118-7248-4c69-89ad-633e26b72d70.png" alt class="image--center mx-auto" /></p>
<p>Overview picture of the inside of the lawnmower</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697305098184/d975f0c9-693b-4ac9-8183-261f54d707df.png" alt class="image--center mx-auto" /></p>
<p>Took about an hour to do, most of the time was fidgeting with the shock absorbers being stuck. New motor works fine, lawnmower is back to working 24/7</p>
]]></content:encoded></item><item><title><![CDATA[Fix Shadcn UI styles and components not working]]></title><description><![CDATA[I was adding Shadcn UI to my existing app, and used the automatic setup with npx shadcn-ui@latest init
This installs the dependencies for me, and creates the setup files automatically.
When I installed the Button component and the Sheet component I c...]]></description><link>https://blog.skorpen.com/fix-shadcn-ui-styles-and-components-not-working</link><guid isPermaLink="true">https://blog.skorpen.com/fix-shadcn-ui-styles-and-components-not-working</guid><category><![CDATA[shadcn]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[shadcn ui]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Sat, 02 Sep 2023 12:26:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693656974625/a43a8abd-cc5f-4f7a-b736-d2a05a4f3a3f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was adding Shadcn UI to my existing app, and used the automatic setup with <code>npx shadcn-ui@latest init</code></p>
<p>This installs the dependencies for me, and creates the setup files automatically.</p>
<p>When I installed the Button component and the Sheet component I couldnt get them to work properly, the styles didnt apply and the Sheet didnt show on the screen when I clicked the Open button in the top right of the image.</p>
<p>As you see in the header image the buttons with the text "Test" and "Open" and the sun icon are not supposed to look like this.</p>
<p>And clicking the sun icon or the "Open" button changes the html if inspected, but doesnt show anything for the user.</p>
<p>The problem was that the automatic setup wanted to use aliasing for the components folder. So it created the <code>@/components/</code> folder.</p>
<p>But you have to add these folder paths to the <code>tailwind.config.js</code> file aswell.</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = {
  darkMode: [<span class="hljs-string">"class"</span>],
  content: [
    <span class="hljs-string">'./pages/**/*.{ts,tsx}'</span>,
    <span class="hljs-string">'./components/**/*.{ts,tsx}'</span>,
    <span class="hljs-string">'./@/components/**/*.{ts,tsx}'</span>,
    <span class="hljs-string">'./app/**/*.{ts,tsx}'</span>,
    <span class="hljs-string">'./src/**/*.{ts,tsx}'</span>,
    ],
</code></pre>
<p>So the fix is in the line 6: <code>'./@/components/**/*.{ts,tsx}',</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693657341100/254afeb2-0194-401d-b81d-f996b847ecf4.png" alt class="image--center mx-auto" /></p>
<p>And now the styles work and the menus show like they are supposed to.</p>
]]></content:encoded></item><item><title><![CDATA[Reset password via email with Supabase and Sveltekit]]></title><description><![CDATA[Supabase offer a method to reset your password via email. Most guides I found online skips the part where you detect the event from supabase and securely check that the user is allowed to change password.
In the documentation it says that you can use...]]></description><link>https://blog.skorpen.com/reset-password-via-email-with-supabase-and-sveltekit</link><guid isPermaLink="true">https://blog.skorpen.com/reset-password-via-email-with-supabase-and-sveltekit</guid><category><![CDATA[supabase]]></category><category><![CDATA[Sveltekit]]></category><category><![CDATA[password]]></category><category><![CDATA[zod]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 10 Apr 2023 09:25:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/-eiswENkF9c/upload/dd92cf70682e17a69df93b6480859dfd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Supabase offer a method to reset your password via email. Most guides I found online skips the part where you detect the event from supabase and securely check that the user is allowed to change password.</p>
<p>In the <a target="_blank" href="https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail">documentation</a> it says that you can use the resetPasswordForEmail method to generate a link and send it to the user via email:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth
  .resetPasswordForEmail(<span class="hljs-string">'user@email.com'</span>, {
    <span class="hljs-attr">redirectTo</span>: <span class="hljs-string">"http://localhost:5137/login/newpassword"</span>
  })
</code></pre>
<p>This triggers Supabase to send an email to the user with an URL with tokens.</p>
<p>(You can change the email template for this in your supabase dashboard, you can also change the generated redirect url in the same place, eg if you want to add the token directly to the url.)</p>
<p>This standard URL looks kinda like this:<br /><code>SITE_URL&gt;#access_token=x&amp;refresh_token=y&amp;expires_in=z&amp;token_type=bearer&amp;type=recovery</code></p>
<p>This link sends the user to Supabases server, they authenticate and logs the user in, and then redirects the user to the URL we provided (http://localhost:5137/login/newpassword)</p>
<p>The security in this is that only the user should have access to their own email, and thus noone else should get the URL from supabase with the tokens.</p>
<p>Supabase then checks that the tokens in the URL are correct and then redirects the user back to us.</p>
<p>We then have to verify that the <code>onAuthStateChange</code> event is of the type <code>"PASSWORD_RECOVERY"</code>, and if it is, we redirect the user to the set new password form.</p>
<p>So lets do some coding, first we make the forgot password page with an forgot_password action.</p>
<p>This is the +page.svelte for "/loing/forgotpassword"</p>
<pre><code class="lang-typescript">&lt;script lang=<span class="hljs-string">"ts"</span>&gt;
  <span class="hljs-keyword">import</span> { enhance } <span class="hljs-keyword">from</span> <span class="hljs-string">"$app/forms"</span>;
  <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> form;
&lt;/script&gt;

&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"w-[512px] p-2 flex flex-col gap-2"</span>&gt;
    &lt;h5 <span class="hljs-keyword">class</span>=<span class="hljs-string">"font-bold text-2xl"</span>&gt;Forgot password?&lt;/h5&gt;
    &lt;p&gt;Enter your email to reset your password.&lt;/p&gt;
    &lt;form method=<span class="hljs-string">"POST"</span> action=<span class="hljs-string">"?/forgot_password"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col gap-2"</span> use:enhance&gt;
        &lt;label <span class="hljs-keyword">for</span>=<span class="hljs-string">"email"</span>&gt;Email:&lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span> name=<span class="hljs-string">"email"</span> id=<span class="hljs-string">"email"</span> required /&gt;&lt;/label&gt;
        {#<span class="hljs-keyword">if</span> form?.errors?.email}
            &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-red-500"</span>&gt;{form?.errors?.email[<span class="hljs-number">0</span>]}&lt;/span&gt;
        {/<span class="hljs-keyword">if</span>}

        &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-blue-300 text-black font-semibold text-lg border border-solid border-black p-1 w-44 mt-4"</span>&gt;Send&lt;/button&gt;
    &lt;/form&gt;
    {#<span class="hljs-keyword">if</span> form &amp;&amp; form.success === <span class="hljs-literal">true</span> } 
        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-green-300 p-2"</span>&gt;
            &lt;p&gt;Check your email <span class="hljs-keyword">for</span> the reset-password email <span class="hljs-keyword">from</span> supabase&lt;/p&gt;
        &lt;/div&gt;
    {/<span class="hljs-keyword">if</span>}
    {#<span class="hljs-keyword">if</span> form &amp;&amp; form.success === <span class="hljs-literal">false</span> } 
        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-red-300 p-2"</span>&gt;
            &lt;p&gt;Could not find a user <span class="hljs-keyword">with</span> <span class="hljs-built_in">this</span> email.&lt;/p&gt;
        &lt;/div&gt;
    {/<span class="hljs-keyword">if</span>}

&lt;/div&gt;
</code></pre>
<p>I get the supabase client and the session from locals, see <a target="_blank" href="https://www.npmjs.com/package/@supabase/auth-helpers-sveltekit">supabase sveltekit auth helpers</a> how this is set up.<br />I use <a target="_blank" href="https://www.npmjs.com/package/zod">Zod</a> for validating form inputs, this is the +page.server.ts for forgotpassword:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> changeEmailSchema = z.object({
    email: z.string().email(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> actions = {
    forgot_password: <span class="hljs-keyword">async</span> (event) =&gt; {
        <span class="hljs-keyword">const</span> { request, locals } = event;
        <span class="hljs-keyword">const</span> { supabaseClient } = locals;

        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> request.formData();
        <span class="hljs-keyword">const</span> obj = <span class="hljs-built_in">Object</span>.fromEntries(data);

        <span class="hljs-keyword">const</span> { email } = obj;

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> result = changeEmailSchema.parse(obj);

            <span class="hljs-keyword">const</span> { data: user, error } = <span class="hljs-keyword">await</span> supabaseClient.auth.resetPasswordForEmail(result.email, {
                redirectTo: <span class="hljs-string">'http://localhost:5173/login/newpassword'</span>,
            });

            <span class="hljs-keyword">if</span> (error) {
                <span class="hljs-keyword">return</span> {
                    errors: [
                        { field: <span class="hljs-string">'email'</span>, message: <span class="hljs-string">'Something went wrong'</span> },
                    ],
                    data: obj,
                    success: <span class="hljs-literal">false</span>,
                };
            }

            <span class="hljs-keyword">return</span> {
                data: user,
                errors: [],
                success: <span class="hljs-literal">true</span>,
            };
        } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
            <span class="hljs-keyword">const</span> { fieldErrors: errors } = error.flatten();

            <span class="hljs-keyword">return</span> {
                errors: errors,
                data: obj,
                success: <span class="hljs-literal">false</span>,
            };
        }
    },
};
</code></pre>
<p>This action generates the email from supabase, and the redirect link that sends the user back to us after supabase authenticated them from the tokens.</p>
<p>Now we have to catch the "PASSWORD_RECOVERY" event from supabase.<br />I use the <a target="_blank" href="https://www.npmjs.com/package/@supabase/auth-helpers-sveltekit">Supabase auth helpers</a> package, and the event listener with the auth helper is located in my +layout.svelte file in the root of my routes folder.</p>
<p>This is only a snippet from the file, follow the instructions from the auth helpers package for the full setup.</p>
<pre><code class="lang-typescript">onMount(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> {
            data: { subscription },
        } = supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
            <span class="hljs-keyword">if</span> (event === <span class="hljs-string">"PASSWORD_RECOVERY"</span>) {
                <span class="hljs-comment">// const { url } = $page;</span>
                <span class="hljs-comment">// const { hash} = url;</span>

                <span class="hljs-comment">// const token = hash.split('&amp;')[0].slice(14);</span>
                <span class="hljs-comment">// redirect user to the page where it creates a new password</span>
                <span class="hljs-keyword">throw</span> redirect(<span class="hljs-number">302</span>, <span class="hljs-string">'/login/newpassword?token='</span> + session?.access_token);
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-comment">// default action</span>
                invalidate(<span class="hljs-string">'supabase:auth'</span>);
            }
        });

        <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
            subscription.unsubscribe();
        };
    });
</code></pre>
<p>I am not sure if Supabase has changed the order of events when you are reading this, <a target="_blank" href="https://github.com/orgs/supabase/discussions/3360">there is a discussion/PR</a> where they are thinking of NOT logging the user in before sending the "PASSWORD_RECOVERY" event. If they have changed this, you will not have a session to get the token from. Just remove the token from the url if this is the case.</p>
<p>This <code>onAuthStateChange</code> detects the password recovery and we then redirect the user to our new "change password page".</p>
<p>+page.svelte for /login/newpassword</p>
<pre><code class="lang-typescript">&lt;script <span class="hljs-keyword">type</span>=<span class="hljs-string">"ts"</span>&gt;
  <span class="hljs-keyword">import</span> { page } <span class="hljs-keyword">from</span> <span class="hljs-string">'$app/stores'</span>;
  <span class="hljs-keyword">import</span> { enhance } <span class="hljs-keyword">from</span> <span class="hljs-string">"$app/forms"</span>;

    <span class="hljs-keyword">export</span> <span class="hljs-keyword">let</span> form;

    <span class="hljs-keyword">const</span> { url } = $page;
    <span class="hljs-keyword">const</span> { searchParams } = url;

    <span class="hljs-keyword">const</span> token = searchParams.get(<span class="hljs-string">'token'</span>);
&lt;/script&gt;

&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"w-[512px]"</span>&gt;
    &lt;span&gt;Change password&lt;/span&gt;
        &lt;form method=<span class="hljs-string">"POST"</span> action=<span class="hljs-string">"?/new_password"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col gap-2"</span> use:enhance&gt;
            &lt;label <span class="hljs-keyword">for</span>=<span class="hljs-string">"password"</span>&gt;New password:&lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span> name=<span class="hljs-string">"password"</span> id=<span class="hljs-string">"password"</span> required /&gt;&lt;/label&gt;
            {#<span class="hljs-keyword">if</span> form?.errors?.password}
                &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-red-500"</span>&gt;{form?.errors?.password[<span class="hljs-number">0</span>]}&lt;/span&gt;
            {/<span class="hljs-keyword">if</span>}
            &lt;label <span class="hljs-keyword">for</span>=<span class="hljs-string">"confirm_password"</span>&gt;Confirm password:&lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span> name=<span class="hljs-string">"confirm_password"</span> id=<span class="hljs-string">"confirm_password"</span> required /&gt;&lt;/label&gt;
            {#<span class="hljs-keyword">if</span> form?.errors?.confirm_password}
                &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-red-500"</span>&gt;{form?.errors?.confirm_password[<span class="hljs-number">0</span>]}&lt;/span&gt;
            {/<span class="hljs-keyword">if</span>}
            &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"hidden"</span> name=<span class="hljs-string">"token"</span> value={token} /&gt;
            {#<span class="hljs-keyword">if</span> form?.errors?.token}
                &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-red-500"</span>&gt;{form?.errors?.token[<span class="hljs-number">0</span>]}&lt;/span&gt;
            {/<span class="hljs-keyword">if</span>}
            &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-blue-300 text-black font-semibold text-lg border border-solid border-black p-1 w-44 mt-4"</span>&gt;Send&lt;/button&gt;
        &lt;/form&gt;

    {#<span class="hljs-keyword">if</span> form &amp;&amp; form.success === <span class="hljs-literal">true</span> }
        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-green-300 p-2"</span>&gt;
            &lt;p&gt;Your password was changed.&lt;/p&gt;
        &lt;/div&gt;
    {/<span class="hljs-keyword">if</span>}

    {#<span class="hljs-keyword">if</span> form &amp;&amp; form.success === <span class="hljs-literal">false</span> }
        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-red-300 p-2"</span>&gt;
            &lt;p&gt;Could not change password.&lt;/p&gt;
        &lt;/div&gt;
    {/<span class="hljs-keyword">if</span>}
&lt;/div&gt;
</code></pre>
<p>+page.server.ts for "/login/newpassword"</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { error } <span class="hljs-keyword">from</span> <span class="hljs-string">'@sveltejs/kit'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> changePasswordSchema = z
    .object({
        password: z.string().min(<span class="hljs-number">8</span>, { message: <span class="hljs-string">'Password must contain at least 8 characters'</span> }),
        confirm_password: z.string().min(<span class="hljs-number">8</span>, { message: <span class="hljs-string">'Password must contain at least 8 characters'</span> }),
        token: z.string(),
    })
    .superRefine(<span class="hljs-function">(<span class="hljs-params">data, ctx</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (data.password !== data.confirm_password) {
            ctx.addIssue({
                code: <span class="hljs-string">'custom'</span>,
                message: <span class="hljs-string">'Passwords must match'</span>,
                path: [<span class="hljs-string">'confirm_password'</span>],
            });
        }
    });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> actions = {
    new_password: <span class="hljs-keyword">async</span> (event) =&gt; {
        <span class="hljs-keyword">const</span> { request, locals } = event;
        <span class="hljs-keyword">const</span> { supabaseClient, getSession } = locals;
        <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getSession();

        <span class="hljs-comment">// user is logged in from the supabase reset password flow - from +layout.svelte onMount</span>
        <span class="hljs-keyword">if</span> (!session) {
            <span class="hljs-keyword">throw</span> error(<span class="hljs-number">401</span>, { message: <span class="hljs-string">'not authorized'</span> });
        }

        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> request.formData();
        <span class="hljs-keyword">const</span> obj = <span class="hljs-built_in">Object</span>.fromEntries(data);

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> result = changePasswordSchema.parse(obj);

            <span class="hljs-keyword">if</span> (result) {
<span class="hljs-comment">// supabase logged the user in, so we can change the users password</span>
                <span class="hljs-keyword">const</span> { data: user, error } = <span class="hljs-keyword">await</span> supabaseClient.auth.updateUser({
                    password: result.password,
                });

                <span class="hljs-keyword">if</span> (error) {
                    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'supa change pw error'</span>, error);
                    <span class="hljs-keyword">return</span> {
                        errors: [
                            { field: <span class="hljs-string">'password'</span>, message: <span class="hljs-string">'Something went wrong, cant update password'</span> },
                        ],
                        data: {},
                        success: <span class="hljs-literal">false</span>,
                    };
                }

                <span class="hljs-keyword">if</span> (user) {
                    <span class="hljs-keyword">return</span> {
                        data: user,
                        errors: [],
                        success: <span class="hljs-literal">true</span>,
                    };
                }
            }
        } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">const</span> { fieldErrors: errors } = error.flatten();
                <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'catch error'</span>, errors);

                <span class="hljs-keyword">return</span> {
                    errors: errors,
                    data: obj,
                    success: <span class="hljs-literal">false</span>,
                };
            } <span class="hljs-keyword">catch</span> (error2) {
                <span class="hljs-built_in">console</span>.log(error);
            }
        }
    },
};
</code></pre>
<p>The user has now changed the password, and is logged in.</p>
<p>You could now redirect them to the frontpage, or where ever you want them to go.</p>
]]></content:encoded></item><item><title><![CDATA[Conditional Connect in Prisma]]></title><description><![CDATA[I want to create a product with Prisma ORM, and this product has multiple connections in my database:
Schema:
model Product {
  id String @id @default(cuid())

  published DateTime?
  creatorId String

  userId Int
  user   UserInfo @relation("produc...]]></description><link>https://blog.skorpen.com/conditional-connect-in-prisma</link><guid isPermaLink="true">https://blog.skorpen.com/conditional-connect-in-prisma</guid><category><![CDATA[prisma]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[React]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Tue, 11 Oct 2022 18:58:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/W6l35A_rxxU/upload/v1665513460198/MaS2LNBx5A.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I want to create a product with Prisma ORM, and this product has multiple connections in my database:</p>
<p>Schema:</p>
<pre><code>model Product {
  id <span class="hljs-built_in">String</span> @id @<span class="hljs-keyword">default</span>(cuid())

  published DateTime?
  creatorId <span class="hljs-built_in">String</span>

  userId Int
  user   UserInfo @relation(<span class="hljs-string">"products"</span>, <span class="hljs-attr">fields</span>: [userId], <span class="hljs-attr">references</span>: [id], <span class="hljs-attr">onDelete</span>: Cascade)

  imageId <span class="hljs-built_in">String</span>?       @unique
  image   ProductImage? @relation(<span class="hljs-string">"productImage"</span>, <span class="hljs-attr">fields</span>: [imageId], <span class="hljs-attr">references</span>: [id])

  affiliateId <span class="hljs-built_in">String</span>?
  affiliate   Affiliate? @relation(<span class="hljs-string">"productAffiliate"</span>, <span class="hljs-attr">fields</span>: [affiliateId], <span class="hljs-attr">references</span>: [id])

  brandId <span class="hljs-built_in">String</span>?
  brand   Brand?  @relation(<span class="hljs-string">"productBrand"</span>, <span class="hljs-attr">fields</span>: [brandId], <span class="hljs-attr">references</span>: [id])

  post  Post[]
  tagId <span class="hljs-built_in">String</span>?
  tag   ProductTag? @relation(<span class="hljs-string">"mainProductTag"</span>, <span class="hljs-attr">fields</span>: [tagId], <span class="hljs-attr">references</span>: [id])

  tags  ProductTag[]  @relation(<span class="hljs-string">"productTags"</span>)
  lists ProductList[] @relation(<span class="hljs-string">"productList"</span>)

  title       <span class="hljs-built_in">String</span>?
  slug        <span class="hljs-built_in">String</span>  @<span class="hljs-keyword">default</span>(<span class="hljs-string">""</span>)
  description <span class="hljs-built_in">String</span>?
  url         <span class="hljs-built_in">String</span>?
  favorite    <span class="hljs-built_in">Boolean</span> @<span class="hljs-keyword">default</span>(<span class="hljs-literal">false</span>)
  sort        Int     @<span class="hljs-keyword">default</span>(<span class="hljs-number">10</span>)
  discount    Int     @<span class="hljs-keyword">default</span>(<span class="hljs-number">0</span>)

  price  Float   @<span class="hljs-keyword">default</span>(<span class="hljs-number">0</span>)
  valuta <span class="hljs-built_in">String</span>?

  createdAt DateTime  @<span class="hljs-keyword">default</span>(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}
</code></pre><p>When I create a product I may or may not have all the connected data, like brand, affiliate, tag and so on.</p>
<p>To do this we can leverage the undefined variable like this: (<a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined">link to Prisma documentation</a>)</p>
<pre><code><span class="hljs-keyword">let</span> product = <span class="hljs-keyword">await</span> prisma.product.create({
      <span class="hljs-attr">data</span>: {
        title,
        description,
        url,
        <span class="hljs-attr">creatorId</span>: session.user.id,
        <span class="hljs-attr">published</span>: published ? <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() : <span class="hljs-literal">undefined</span>,
        <span class="hljs-attr">favorite</span>: favorite ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>,
        <span class="hljs-attr">user</span>: {
          <span class="hljs-attr">connect</span>: {
            <span class="hljs-attr">id</span>: <span class="hljs-built_in">Number</span>(session.user.userInfo),
          },
        },
        <span class="hljs-attr">image</span>: {
          <span class="hljs-attr">connect</span>: {
            <span class="hljs-attr">id</span>: image,
          },
        },
        <span class="hljs-attr">affiliate</span>: affiliate
          ? {
              <span class="hljs-attr">connect</span>: {
                <span class="hljs-attr">id</span>: affiliate,
              },
            }
          : <span class="hljs-literal">undefined</span>,
        <span class="hljs-attr">brand</span>: brand
          ? {
              <span class="hljs-attr">connect</span>: {
                <span class="hljs-attr">id</span>: brand,
              },
            }
          : <span class="hljs-literal">undefined</span>,
        <span class="hljs-attr">tag</span>: tag
          ? {
              <span class="hljs-attr">connect</span>: {
                <span class="hljs-attr">id</span>: tag,
              },
            }
          : <span class="hljs-literal">undefined</span>,
        <span class="hljs-attr">post</span>: post
          ? {
              <span class="hljs-attr">connect</span>: {
                <span class="hljs-attr">id</span>: post,
              },
            }
          : <span class="hljs-literal">undefined</span>,
      },
    });
</code></pre><p>For affiliate: If the affiliate variable is truthy, we do the connect, else we give the 'undefined' value.</p>
]]></content:encoded></item><item><title><![CDATA[Use react-aria with react-hook-form and Zod]]></title><description><![CDATA[I am making a form for my new project, and I want the form to have good accessability, validation, and ready made components that I just have to style a bit with Tailwind.
For accessability I found that react-aria from Adobe is pretty good, I am used...]]></description><link>https://blog.skorpen.com/use-react-aria-with-react-hook-form-and-zod</link><guid isPermaLink="true">https://blog.skorpen.com/use-react-aria-with-react-hook-form-and-zod</guid><category><![CDATA[react-hook-form]]></category><category><![CDATA[React]]></category><category><![CDATA[Validation]]></category><category><![CDATA[react-aria]]></category><category><![CDATA[zod]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Thu, 22 Sep 2022 11:06:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1663844629719/7UVhRpweC.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am making a form for my new project, and I want the form to have good accessability, validation, and ready made components that I just have to style a bit with Tailwind.</p>
<p>For accessability I found that <a target="_blank" href="https://react-spectrum.adobe.com/react-aria/index.html">react-aria</a> from Adobe is pretty good, I am used to working with <a target="_blank" href="https://chakra-ui.com/">Chakra UI</a> and <a target="_blank" href="https://mui.com">Material UI</a> (MUI) so the component style is making me feel at home.</p>
<p><a target="_blank" href="https://www.npmjs.com/package/react-hook-form">react-hook-form</a> with <a target="_blank" href="https://www.npmjs.com/package/zod">Zod</a> gives me easy validation and typing of my form fields.
Logrocket got a good blog on using these two together <a target="_blank" href="https://blog.logrocket.com/react-hook-form-complete-guide/">here</a>.</p>
<pre><code><span class="hljs-string">"react-aria"</span>: <span class="hljs-string">"^3.19.0"</span>,
<span class="hljs-string">"@hookform/error-message"</span>: <span class="hljs-string">"^2.0.0"</span>,
<span class="hljs-string">"@hookform/resolvers"</span>: <span class="hljs-string">"^2.9.8"</span>,
<span class="hljs-string">"react-hook-form"</span>: <span class="hljs-string">"^7.35.0"</span>,
<span class="hljs-string">"zod"</span>: <span class="hljs-string">"^3.19.1"</span>
</code></pre><p>But trying to use react-aria with react-hook-form and zod wasnt straight forward. The solution wasnt that complex, but I had to google alot and try many different thing before I understood what the problem was. Scroll to the bottom if you want the solution straight away.</p>
<p>I followed some guides and tried with the example code from react-aria and the example code for react-hook-form with Zod. 
Looking like this:</p>
<pre><code>TextField.tsx
<span class="hljs-keyword">import</span> { useTextField } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-aria"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TextField</span>(<span class="hljs-params">props</span>) </span>{
  <span class="hljs-keyword">let</span> { label } = props;
  <span class="hljs-keyword">let</span> ref = useRef();
  <span class="hljs-keyword">let</span> { labelProps, inputProps, descriptionProps, errorMessageProps } =
    useTextField(props, ref);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>", <span class="hljs-attr">flexDirection:</span> "<span class="hljs-attr">column</span>", <span class="hljs-attr">width:</span> <span class="hljs-attr">200</span> }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span> {<span class="hljs-attr">...labelProps</span>}&gt;</span>{label}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span> {<span class="hljs-attr">...inputProps</span>} <span class="hljs-attr">ref</span>=<span class="hljs-string">{ref}</span> /&gt;</span>
      {props.description &amp;&amp; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> {<span class="hljs-attr">...descriptionProps</span>} <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}&gt;</span>
          {props.description}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      )}
      {props.errorMessage &amp;&amp; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> {<span class="hljs-attr">...errorMessageProps</span>} <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">color:</span> "<span class="hljs-attr">red</span>", <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}&gt;</span>
          {props.errorMessage}
        <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><pre><code>Form.tsx

<span class="hljs-keyword">import</span> TextField <span class="hljs-keyword">from</span> <span class="hljs-string">'components/utils/TextField'</span>;
<span class="hljs-keyword">import</span> { useForm, SubmitHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-hook-form"</span>;
<span class="hljs-keyword">import</span> { z, object, TypeOf } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> { zodResolver } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hookform/resolvers/zod"</span>;
<span class="hljs-keyword">import</span> { ErrorMessage } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hookform/error-message"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TestAria</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> zodSchema = object({
    <span class="hljs-attr">title</span>: z.string().min(<span class="hljs-number">4</span>, { <span class="hljs-attr">message</span>: <span class="hljs-string">"Must be 5 or more characters long"</span> }),
    <span class="hljs-attr">description</span>: z
      .string()
      .min(<span class="hljs-number">4</span>, { <span class="hljs-attr">message</span>: <span class="hljs-string">"Must be 5 or more characters long"</span> }),
  });
  type RegisterInput = TypeOf&lt;<span class="hljs-keyword">typeof</span> zodSchema&gt;;

  <span class="hljs-keyword">const</span> {
    register,
    <span class="hljs-attr">formState</span>: { errors, isSubmitSuccessful },
    handleSubmit,
  } = useForm&lt;RegisterInput&gt;({
    <span class="hljs-attr">resolver</span>: zodResolver(zodSchema),
  });

  <span class="hljs-keyword">const</span> onSubmitHandler: SubmitHandler&lt;RegisterInput&gt; = <span class="hljs-function">(<span class="hljs-params">values</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log({ values });
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{handleSubmit(onSubmitHandler)}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">TextField</span>
          <span class="hljs-attr">label</span>=<span class="hljs-string">"Title"</span>
          {<span class="hljs-attr">...register</span>("<span class="hljs-attr">title</span>")}
          <span class="hljs-attr">validationState</span>=<span class="hljs-string">{!!errors.title?.message}</span>
          <span class="hljs-attr">errorMessage</span>=<span class="hljs-string">{errors.title?.message</span> ?? ""}
        /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">"description"</span>&gt;</span>Description<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span>
            <span class="hljs-attr">rows</span>=<span class="hljs-string">{5}</span>
            <span class="hljs-attr">aria-label</span>=<span class="hljs-string">"description"</span>
            {<span class="hljs-attr">...register</span>("<span class="hljs-attr">description</span>")}
          /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">ErrorMessage</span> <span class="hljs-attr">errors</span>=<span class="hljs-string">{errors}</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</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">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Create<span class="hljs-tag">&lt;/<span class="hljs-name">button</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">form</span>&gt;</span></span>
  );
}
</code></pre><p>In Form.tsx I got the title input that is a react-aria component, and the description input that is a regular html input that works fine with react-hook-form and zod for comparison.</p>
<p>This didnt work, errormessages started popping up when I typed into the title input.</p>
<p>I am using Next.js and the first error that popped up in my console was this:</p>
<pre><code>Warning: Prop <span class="hljs-string">`id`</span> did not match. Server: <span class="hljs-string">"react-aria-5"</span> Client: <span class="hljs-string">"react-aria-2"</span>
label
</code></pre><p>This ment that the server side rendered part of Next.js and the client side rendered part got different ids on my title input. Googling the error said to use SSRProvider from react-aria to fix this.</p>
<pre><code>_app.tsx

<span class="hljs-keyword">import</span> { SSRProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-aria"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyApp</span>(<span class="hljs-params">{ Component, pageProps: { ...pageProps } }: AppProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">SSRProvider</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Component</span> {<span class="hljs-attr">...pageProps</span>} /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">SSRProvider</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> MyApp;
</code></pre><p>Ok so that errormessage was gone, but a new one showed up:</p>
<pre><code>Warning: <span class="hljs-built_in">Function</span> components cannot be given refs. Attempts to access <span class="hljs-built_in">this</span> ref will fail. Did you mean to use React.forwardRef()?
</code></pre><p>So to use react-aria uncontrolled (not using react state) they use ref to keep control of which input field we are trying to type into. So the parent (Form.tsx) has to send in a ref for the child (TextField.tsx) to apply to the input field.</p>
<p>After googling and finding some issues on react-arias github page I tried different things mentioned in the <a target="_blank" href="https://github.com/adobe/react-spectrum/issues/834">issue-post</a>, but what ended up working was using forwardRef (like the errormessage said :)) like this:</p>
<pre><code><span class="hljs-keyword">import</span> { useTextField } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-aria"</span>;
<span class="hljs-keyword">import</span> { forwardRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">const</span> TextField = forwardRef(<span class="hljs-function">(<span class="hljs-params">props: any, ref: any</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> { label } = props;
  <span class="hljs-comment">// let ref = useRef();</span>
  <span class="hljs-keyword">let</span> { labelProps, inputProps, descriptionProps, errorMessageProps } =
    useTextField(props, ref);
  <span class="hljs-built_in">console</span>.log({ inputProps }, { ref });

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>", <span class="hljs-attr">flexDirection:</span> "<span class="hljs-attr">column</span>", <span class="hljs-attr">width:</span> <span class="hljs-attr">200</span> }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span> {<span class="hljs-attr">...labelProps</span>}&gt;</span>{label}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span> {<span class="hljs-attr">...inputProps</span>} <span class="hljs-attr">ref</span>=<span class="hljs-string">{ref}</span> /&gt;</span>
      {props.description &amp;&amp; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> {<span class="hljs-attr">...descriptionProps</span>} <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}&gt;</span>
          {props.description}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      )}
      {props.errorMessage &amp;&amp; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> {<span class="hljs-attr">...errorMessageProps</span>} <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">color:</span> "<span class="hljs-attr">red</span>", <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}&gt;</span>
          {props.errorMessage}
        <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>
  );
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> TextField;
</code></pre><p>Well trying again and a new error message:</p>
<pre><code>Uncaught (<span class="hljs-keyword">in</span> promise) <span class="hljs-built_in">TypeError</span>: target is <span class="hljs-literal">undefined</span>
    onChange index.esm.mjs:<span class="hljs-number">1738</span>
</code></pre><p>It seemed to me that the change in the input didnt get registered or registered wrongly.
Once again I had to google and found that react-hook-form has several other methods and properties in the {...register("title")} function.</p>
<pre><code><span class="hljs-keyword">const</span> {
    onChange,
    onBlur,
    name,
    ref,
  } = register(<span class="hljs-string">"title"</span>);
</code></pre><p>There we can see the ref that gets created and sent to the react-aria, along with the name and interestingly a onChange.</p>
<p>I console logged what react-hook-form got from the onChange</p>
<pre><code>&lt;TextField
            label=<span class="hljs-string">"Title"</span>
            {...register(<span class="hljs-string">"title"</span>)}
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log({e})}
          /&gt;
</code></pre><p>What returned was just the string, if I typed "abc" the return was "abc".</p>
<p>I expected that the onChange from react-hook-form needs the format event.target.value (in TypeScript: ChangeEvent), and the error message said "target is undefined".</p>
<p>So I tried making a change to the onChange:</p>
<pre><code><span class="hljs-keyword">const</span> onChangeHandler = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> e = { <span class="hljs-attr">target</span>: { event } };
    <span class="hljs-keyword">return</span> e;
  };

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TextField</span>
            <span class="hljs-attr">label</span>=<span class="hljs-string">"Title"</span>
            {<span class="hljs-attr">...register</span>("<span class="hljs-attr">title</span>")}
            <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> register("title").onChange(onChangeHandler(e))}
          /&gt;</span>
</code></pre><p>This way the onChange for react-hook-form would get the format event.target.value</p>
<p>And now everything worked!</p>
<p>Edit: I also found <a target="_blank" href="https://github.com/adobe/react-spectrum/issues/2239">this</a> bugreport that describes the problem, and a <a target="_blank" href="https://github.com/adobe/react-spectrum/issues/1860#issuecomment-849833808">link</a> to a discussion for why they wont change the behaviour of onChange.</p>
]]></content:encoded></item><item><title><![CDATA[Making Material UIs Drawer component work with onMouseOver]]></title><description><![CDATA[As standard the Drawer component uses List and ListItem components to make a menu.
And the default mode is opening the submenu by clicking on the ListItem.
But I wanted to make the submenus open when I hover the mouse over them.
Red is anchorEl (main...]]></description><link>https://blog.skorpen.com/making-material-uis-drawer-component-work-with-onmouseover</link><guid isPermaLink="true">https://blog.skorpen.com/making-material-uis-drawer-component-work-with-onmouseover</guid><category><![CDATA[React]]></category><category><![CDATA[Material Design]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Tue, 03 May 2022 12:56:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651582068942/cUMW5ZSuw.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As standard the Drawer component uses List and ListItem components to make a menu.
And the default mode is opening the submenu by clicking on the ListItem.</p>
<p>But I wanted to make the submenus open when I hover the mouse over them.</p>
<p>Red is anchorEl (mainmenu item)
Yellow is menu (submenu)
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651581639961/dPaDjhKNC.png" alt="ref1.png" /></p>
<p>My starter code:</p>
<pre><code><span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&lt;</span>div<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span>Drawer
        variant<span class="hljs-operator">=</span><span class="hljs-string">"permanent"</span>
        className<span class="hljs-operator">=</span><span class="hljs-string">"menu-list"</span>
       <span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span>List
          component<span class="hljs-operator">=</span><span class="hljs-string">"nav"</span>
          aria<span class="hljs-operator">-</span>labelledby<span class="hljs-operator">=</span><span class="hljs-string">"nested-list-subheader"</span>
          subheader<span class="hljs-operator">=</span>{
            <span class="hljs-operator">&lt;</span>div className<span class="hljs-operator">=</span>{classes.subHeader}<span class="hljs-operator">&gt;</span>
              <span class="hljs-operator">&lt;</span>img src<span class="hljs-operator">=</span>{OMSLogo} className<span class="hljs-operator">=</span>{classes.logo} alt<span class="hljs-operator">=</span><span class="hljs-string">"site logo"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
            <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
          }
          className<span class="hljs-operator">=</span>{classes.root}
        <span class="hljs-operator">&gt;</span>
          {modules.map((module, k) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> (
            <span class="hljs-operator">&lt;</span>ManagerMenuModule module<span class="hljs-operator">=</span>{module} key<span class="hljs-operator">=</span>{k} classes<span class="hljs-operator">=</span>{{ nested: classes.nested }} <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
          ))}
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>List<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Drawer<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
  );
</code></pre><p>And the ManagerMenuModule:</p>
<pre><code>  const [smallMenuOpen, setSmallMenuOpen] <span class="hljs-operator">=</span> React.useState(<span class="hljs-literal">false</span>);
  const [anchorEl, setAnchorEl] <span class="hljs-operator">=</span> React.useState&lt;any<span class="hljs-operator">&gt;</span>(null);

<span class="hljs-keyword">return</span> (
        <span class="hljs-operator">&lt;</span>div id<span class="hljs-operator">=</span><span class="hljs-string">"simple-menu"</span><span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>Popper
            open<span class="hljs-operator">=</span>{smallMenuOpen}
            anchorEl<span class="hljs-operator">=</span>{anchorEl}
            placement<span class="hljs-operator">=</span>{<span class="hljs-string">"right-start"</span>}
            transition
            onMouseLeave<span class="hljs-operator">=</span>{(e) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> handleSmallMenuClose()}
            className<span class="hljs-operator">=</span>{classes.popper}
          <span class="hljs-operator">&gt;</span>
            {({ TransitionProps }) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> (
              <span class="hljs-operator">&lt;</span>ClickAwayListener onClickAway<span class="hljs-operator">=</span>{handleSmallMenuClose}<span class="hljs-operator">&gt;</span>
                <span class="hljs-operator">&lt;</span>Zoom {...TransitionProps}<span class="hljs-operator">&gt;</span>
                  <span class="hljs-operator">&lt;</span>Paper elevation<span class="hljs-operator">=</span>{<span class="hljs-number">3</span>}<span class="hljs-operator">&gt;</span>{smallSubMenu(props.module.module_name, <span class="hljs-literal">true</span>)}<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Paper<span class="hljs-operator">&gt;</span>
                <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Zoom<span class="hljs-operator">&gt;</span>
              <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>ClickAwayListener<span class="hljs-operator">&gt;</span>
            )}
          <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Popper<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>Tooltip placement<span class="hljs-operator">=</span><span class="hljs-string">"right"</span> arrow title<span class="hljs-operator">=</span>{props.module.module_name} enterDelay<span class="hljs-operator">=</span>{<span class="hljs-number">1000</span>}<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>ListItem className<span class="hljs-operator">=</span>{listItemClasses()} button key<span class="hljs-operator">=</span>{props.module.module_id} onClick<span class="hljs-operator">=</span>{handleSmallMenuOpen}
                      onMouseOver<span class="hljs-operator">=</span>{handleSingleModuleSmallMenu}
                      onMouseLeave<span class="hljs-operator">=</span>{(e) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> handleClose(e)}

                      <span class="hljs-operator">&gt;</span>
            <span class="hljs-operator">&lt;</span>ListItemIcon className<span class="hljs-operator">=</span>{listItemIconClasses()}<span class="hljs-operator">&gt;</span>{renderIcon(props.module.icon)}<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>ListItemIcon<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>ListItem<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Tooltip<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div <span class="hljs-operator">&gt;</span>
      );
</code></pre><pre><code> const handleSingleModuleSmallMenu <span class="hljs-operator">=</span> (<span class="hljs-function"><span class="hljs-keyword">event</span>) =&gt; </span>{
    setAnchorEl(<span class="hljs-keyword">event</span>.currentTarget);
    setSmallMenuOpen(<span class="hljs-literal">true</span>);
  };
</code></pre><pre><code>  const handleClose <span class="hljs-operator">=</span> (e) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (e.currentTarget.localName <span class="hljs-operator">!</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">"ul"</span>) {
      const menu <span class="hljs-operator">=</span> document.getElementById(<span class="hljs-string">"simple-menu"</span>);
      const menuBoundary <span class="hljs-operator">=</span> {
        left: (anchorEl?.offsetLeft  ?? <span class="hljs-number">0</span>) <span class="hljs-operator">+</span> (anchorEl?.offsetWidth ?? <span class="hljs-number">0</span>),
        top: anchorEl?.offsetTop ?? <span class="hljs-number">0</span>,
        right: ((anchorEl?.offsetLeft  ?? <span class="hljs-number">0</span>) <span class="hljs-operator">+</span> (anchorEl?.offsetWidth ?? <span class="hljs-number">0</span>)) <span class="hljs-operator">+</span> (menu?.offsetWidth  ?? <span class="hljs-number">0</span>),
        bottom: (menu?.offsetHeight ?? <span class="hljs-number">0</span>) <span class="hljs-operator">+</span> (anchorEl.offsetTop ?? <span class="hljs-number">0</span>)
      };
      <span class="hljs-comment">// console.log("coords", menuBoundary.left, menuBoundary.top, menuBoundary.right, menuBoundary.bottom, e.clientX, e.clientY)</span>
      <span class="hljs-keyword">if</span> (
        e.clientX <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> menuBoundary.left <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
        e.clientX <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> menuBoundary.right <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
        e.clientY <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> menuBoundary.bottom <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
        e.clientY <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> menuBoundary.top
      ) {
        <span class="hljs-keyword">return</span>;
      }
    }

    setSmallMenuOpen(<span class="hljs-literal">false</span>);
  }
</code></pre><p>So just by setting <strong>onMouseOver={handleSingleModuleSmallMenu}</strong> on the ListItem the menu item shows, but if you dont tell it when to close you end up with all menu items open.</p>
<p>If you just do <strong>onMouseLeave={() =&gt; setSmallMenuOpen(false)}</strong> you cant move the mouse to the submenu, because it closes the menu as soon as your mouse leaves the main menu icon/text.</p>
<p>So we make the <strong>handleClose</strong> method to decide when to close the submenu.</p>
<p>First we save the main menu item, so we know where on the page we are.
<strong>setAnchorEl(event.currentTarget)</strong></p>
<p>Next we have given the <strong><div></div></strong> surrounding each ListItem an id of <strong>"simple-menu"</strong>, we use that to select the submenu element with its coordinates.
<strong>const menu = document.getElementById("simple-menu");</strong></p>
<p>We then define the divs outer limits, so we know when we move the mouse outside of these bounds - we can close the menu.</p>
<pre><code>const menuBoundary <span class="hljs-operator">=</span> {
        left: anchorEl.offsetLeft <span class="hljs-operator">+</span> anchorEl.offsetWidth,
        top: anchorEl.offsetTop,
        right: anchorEl.offsetLeft <span class="hljs-operator">+</span> anchorEl.offsetWidth <span class="hljs-operator">+</span> menu.offsetWidth,
        bottom: menu.offsetHeight <span class="hljs-operator">+</span> anchorEl.offsetTop
      };
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651581617919/OWm2APzEB.png" alt="ref2.png" /></p>
<p>*Left is: where does anchorEl start + anchorEl width. Marked pink on picture.</p>
<p>Right is: where does anchorEl start + anchorEl width + submenu width. Marked red+green on image.</p>
<p>Top is: where does anchorEl start. Marked red on image.</p>
<p>Bottom is: where does anchorEl start + submenu height. Marked blue on image.*</p>
<p>The submenu shows up on the right side of the mainmenu. So we use the <strong>anchorEl</strong> ref to have a startingpoint. Everything is in relative position to this ref.</p>
<p>Last we just have to check if the coordinates of the <strong>onMouseLeave</strong> event is inside the submenu or not. If it is inside submenu, we dont close.</p>
<p>This only handles the <strong>onMouseLeave</strong> on the <strong>ListItem</strong>, which is the mainmenu.</p>
<p>But we also have to listen for onMouseLeave on the shown submenu, this is being handled by setting <strong>onMouseLeave={() =&gt; handleSmallMenuClose()}</strong> on the parent element, which in this case is the <strong>Popper</strong> .</p>
]]></content:encoded></item><item><title><![CDATA[NextAuth and Prisma: extend the user object in the session]]></title><description><![CDATA[Update 22.09.22:
I found an easier way to fix some of the problems with adding to the session object:
This official guide shows how to alter the session schema in prisma to add additional fields. It doesnt solve everything, so if I wanted the full us...]]></description><link>https://blog.skorpen.com/nextauth-and-prisma-extend-the-user-object-in-the-session</link><guid isPermaLink="true">https://blog.skorpen.com/nextauth-and-prisma-extend-the-user-object-in-the-session</guid><category><![CDATA[React]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Sat, 16 Apr 2022 10:28:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651579650271/ijDvdK9So.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Update 22.09.22:
I found an easier way to fix some of the problems with adding to the session object:
<a target="_blank" href="https://next-auth.js.org/tutorials/role-based-login-strategy">This official guide</a> shows how to alter the session schema in prisma to add additional fields. It doesnt solve everything, so if I wanted the full userDetails from the db I still have to use the method below. But if I only wanted the userDetails.id I could use this official way.</p>
<p>When you use NextAuth with the Prisma adapter you get the user object automatically attached to the session.</p>
<p>But I have additional relations on my User model in the Prisma schema:</p>
<pre><code>model User {
  id            <span class="hljs-built_in">String</span>    @id @<span class="hljs-keyword">default</span>(cuid())
  name          <span class="hljs-built_in">String</span>?
  email         <span class="hljs-built_in">String</span>?   @unique
  emailVerified DateTime?
  image         <span class="hljs-built_in">String</span>?
  accounts      Account[]
  sessions      Session[]
  accountType    Role[] <span class="hljs-comment">// custom relation containing roles</span>
  userDetails    UserDetails <span class="hljs-comment">// custom table containing additional info about the user</span>
}
</code></pre><p>So how to get this added to the user object in the session?</p>
<p>I tried changing the callbacks in [...nextauth].ts:</p>
<pre><code>callbacks: {
    <span class="hljs-keyword">async</span> session({ session, token, user }) {
      <span class="hljs-comment">// Send properties to the client, like an access_token from a provider.</span>
      session.user = user;

      <span class="hljs-keyword">let</span> userDetails = <span class="hljs-keyword">await</span> prisma.userDetails.findUnique({
        <span class="hljs-attr">where</span>: {
          <span class="hljs-attr">userId</span>: user.id,
        },
      });
      session.userdetails = userDetails;

      <span class="hljs-keyword">return</span> session;
    },
  },
</code></pre><p>This works, but the session callback now have an additional object, like this:</p>
<pre><code>session: 
{
    <span class="hljs-attr">user</span>: {user},
    <span class="hljs-attr">session</span>: {session},
    <span class="hljs-attr">userdetails</span>: {userDetails}
}
</code></pre><p>While I want it like this: </p>
<pre><code>session: 
{
    <span class="hljs-attr">user</span>: {user}, <span class="hljs-comment">// userDetails and accountType added to this object</span>
    <span class="hljs-attr">session</span>: {session},
}
</code></pre><p>The PrismaAdapter is opensource and I found it on github: <a target="_blank" href="https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts">https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts</a></p>
<p>I then added this function to my [...nextauth].ts file, but changed the <strong>getSessionAndUser</strong> function.</p>
<p>So instead of importing the PrismaAdapter, you add the function yourself.
You could of course put it in its own file and import that. </p>
<p>Now the file looks kinda like this, the change is the "include" portion.</p>
<pre><code><span class="hljs-comment">// [...nextauth].ts</span>
<span class="hljs-comment">// import { PrismaAdapter } from "@next-auth/prisma-adapter"; // not importing this anymore</span>

<span class="hljs-comment">// added this to the file:</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PrismaAdapter</span>(<span class="hljs-params">p: PrismaClient</span>): <span class="hljs-title">Adapter</span> </span>{
  <span class="hljs-keyword">return</span> {
   ... <span class="hljs-comment">// not showing all the functions, only the changed part:</span>

    <span class="hljs-keyword">async</span> getSessionAndUser(sessionToken) {
      <span class="hljs-keyword">const</span> userAndSession = <span class="hljs-keyword">await</span> p.session.findUnique({
        <span class="hljs-attr">where</span>: { sessionToken },
        <span class="hljs-attr">include</span>: {
          <span class="hljs-attr">user</span>: {
            <span class="hljs-attr">include</span>: {
              <span class="hljs-attr">accountType</span>: <span class="hljs-literal">true</span>,
              <span class="hljs-attr">userDetails</span>: <span class="hljs-literal">true</span>,
            },
          },
        },
      });
      <span class="hljs-keyword">if</span> (!userAndSession) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">const</span> { user, ...session } = userAndSession;
      <span class="hljs-keyword">return</span> { user, session };
    },

    ...
}
</code></pre><p>Now I get accountType and userDetails included in my {user} in the session!</p>
]]></content:encoded></item><item><title><![CDATA[Increasing the size on an embedded tweet with react-twitter-embed]]></title><description><![CDATA[Using react-twitter-embed to embed a tweet makes the initial tweet look a bit small:

import { TwitterTweetEmbed } from "react-twitter-embed";

type IProps = {
  id: string;
};
const Tweet = (props: IProps) => {
  return (
      <TwitterTweetEmbed tw...]]></description><link>https://blog.skorpen.com/increasing-the-size-on-an-embedded-tweet-with-react-twitter-embed</link><guid isPermaLink="true">https://blog.skorpen.com/increasing-the-size-on-an-embedded-tweet-with-react-twitter-embed</guid><category><![CDATA[React]]></category><category><![CDATA[Twitter]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Tue, 12 Apr 2022 13:10:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649768798642/cKHtouBv0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Using react-twitter-embed to embed a tweet makes the initial tweet look a bit small:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649768798642/cKHtouBv0.png" alt="Skjermbilde 2022-04-12 150017.png" /></p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">TwitterTweetEmbed</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"react-twitter-embed"</span>;

<span class="hljs-keyword">type</span> IProps <span class="hljs-operator">=</span> {
  id: <span class="hljs-keyword">string</span>;
};
const Tweet <span class="hljs-operator">=</span> (props: IProps) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  <span class="hljs-keyword">return</span> (
      <span class="hljs-operator">&lt;</span>TwitterTweetEmbed tweetId<span class="hljs-operator">=</span>{props.id} <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
  );
};
</code></pre><p>TwitterTweetEmbed takes an option object, but that did not increase the size like I wanted.</p>
<pre><code><span class="hljs-operator">&lt;</span>TwitterTweetEmbed tweetId<span class="hljs-operator">=</span>{props.id} options<span class="hljs-operator">=</span>{{width: <span class="hljs-number">550</span>}} <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
</code></pre><p>But wrapping the TwitterTweetEmbed in a div with a set width solved the issue:</p>
<pre><code>const Tweet <span class="hljs-operator">=</span> (props: IProps) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&lt;</span>div style<span class="hljs-operator">=</span>{{ width: <span class="hljs-string">"600px"</span> }}<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span>TwitterTweetEmbed tweetId<span class="hljs-operator">=</span>{props.id} <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
  );
};
</code></pre><p>The result then looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649768914964/owSgwQ2Ed.png" alt="Skjermbilde 2022-04-12 150111.png" /></p>
]]></content:encoded></item><item><title><![CDATA[JSX element type X does not have any construct or call signatures.ts(2604)]]></title><description><![CDATA[I wanted to make a styled button with Material UI 5.6.0, but encountered a new error that I couldn't find a solution to on Google.
My custom styled components button:
import { Button, styled } from '@mui/material';

const ColorButton  = styled(Button...]]></description><link>https://blog.skorpen.com/jsx-element-type-does-not-have-any-construct-or-call-signaturests2604</link><guid isPermaLink="true">https://blog.skorpen.com/jsx-element-type-does-not-have-any-construct-or-call-signaturests2604</guid><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Stefan Skorpen]]></dc:creator><pubDate>Mon, 11 Apr 2022 11:22:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651579687084/Y_K_fh9Xj.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to make a styled button with Material UI 5.6.0, but encountered a new error that I couldn't find a solution to on Google.</p>
<p>My custom styled components button:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">Button</span>, <span class="hljs-title">styled</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'@mui/material'</span>;

const ColorButton  <span class="hljs-operator">=</span> styled(Button) (({ theme }) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> ({
        color: theme.palette.getContrastText(<span class="hljs-string">'#fff'</span>),
        backgroundColor: <span class="hljs-string">'#00535a'</span>,
        <span class="hljs-string">'&amp;:hover'</span>: {
          backgroundColor: <span class="hljs-string">'#00535a'</span>,
        },
      }));
</code></pre><p>When compiling it gave the following error:
<strong>JSX element type 'ColorButton' does not have any construct or call signatures.ts(2604)</strong></p>
<p>I tried importing Button from Material UI like this:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">Button</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'@mui/material/Button'</span>;
</code></pre><p>I tried importing styled components a few different ways:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">styled</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'@mui/material/styles'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">styled</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'styled-components'</span>;
</code></pre><p>tried adding resolution to package.json:</p>
<pre><code class="lang-{">   "dependencies": {
    "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
   },
  "resolutions": {
    "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
  },
 }
</code></pre>
<p>added path to tsconfig.json:</p>
<pre><code class="lang-{">   "compilerOptions": {
    "paths": {
     "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"]
    }
   },
 }
</code></pre>
<p>But the solution was way simpler, just type the new styled component as a React.Component</p>
<pre><code>const ColorButton: React.Component  <span class="hljs-operator">=</span> styled(Button) (({ theme }) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> ({
        color: theme.palette.getContrastText(<span class="hljs-string">'#fff'</span>),
        backgroundColor: <span class="hljs-string">'#00535a'</span>,
        <span class="hljs-string">'&amp;:hover'</span>: {
          backgroundColor: <span class="hljs-string">'#00535a'</span>,
        },
      }));
</code></pre>]]></content:encoded></item></channel></rss>